CI / build-and-test (push) Successful in 14m43s
7.3: capturedAt/source au registre + 6 adaptateurs + module provenance.ts + ?debug=1 (search-transport.mjs). 7.4: ProviderHealthService + badge source degradee. 7.6: squelettes par provider + snapshots progressifs + transport NDJSON /api/search. 8.1: ProviderAdapter unifie (search enveloppe + channelContent/channelMeta/capabilities) via getProviderAdapter + test de contrat offline.
112 lines
5.0 KiB
JavaScript
112 lines
5.0 KiB
JavaScript
/**
|
|
* Phase 8.4 — même test de forme `Suggestion`, sur fixtures gelées, pour les 6 providers.
|
|
*
|
|
* Les fixtures viennent de `npm run fixtures:record`, qui appelle les 6
|
|
* adaptateurs RÉELS : elles décrivent la sortie d'aujourd'hui, pas une
|
|
* intention. Ce test les rejoue à chaque PR, donc il détecte deux dérives :
|
|
* - le contrat a changé et un adaptateur ne suit plus ;
|
|
* - un adaptateur émet un champ qui viole le contrat.
|
|
*
|
|
* Il sert aussi de garde-fou « le gel n'est pas pourri » : un fichier de
|
|
* fixtures vide ou sans providers serait un test vert vide de sens.
|
|
*/
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { SUGGESTION_CONTRACT_VERSION, SUGGESTION_V2_FIELDS } from '../providers/registry.mjs';
|
|
import { suggestionShapeErrors } from './lib/suggestion-shape.mjs';
|
|
|
|
const FIXTURES = path.join(import.meta.dirname, 'fixtures/provider-suggestions.json');
|
|
const ALL = ['yt', 'dm', 'tw', 'pt', 'od', 'ru'];
|
|
|
|
const raw = fs.readFileSync(FIXTURES, 'utf8');
|
|
const data = JSON.parse(raw);
|
|
|
|
const errorsOf = (s, label) => {
|
|
const acc = [];
|
|
// Même sémantique que `expect` : on ne retient que les prédicats faux.
|
|
suggestionShapeErrors(s, label, (cond, msg) => { if (!cond) acc.push(msg); });
|
|
return acc;
|
|
};
|
|
|
|
test('8.4 — les fixtures sont gelées depuis le contrat v2', () => {
|
|
assert.equal(data._contractVersion, SUGGESTION_CONTRACT_VERSION,
|
|
'fixtures gelées sous une autre version de contrat : régénérer (`npm run fixtures:record`)');
|
|
assert.ok(data._recordedAt, 'date de gel absente');
|
|
assert.ok(data._query, 'requête de gel absente');
|
|
});
|
|
|
|
test('8.4 — les 6 providers sont présents dans le gel', () => {
|
|
for (const id of ALL) {
|
|
assert.ok(data.providers[id], `provider « ${id} » absent des fixtures`);
|
|
assert.ok(Array.isArray(data.providers[id].items), `items de « ${id} » n'est pas un tableau`);
|
|
}
|
|
});
|
|
|
|
test('8.4 — la forme de chaque Suggestion gelée respecte le contrat', () => {
|
|
let checked = 0;
|
|
for (const [id, entry] of Object.entries(data.providers)) {
|
|
for (const [i, item] of entry.items.entries()) {
|
|
const errs = errorsOf(item, `${id}[${i}]`);
|
|
assert.deepEqual(errs, [], `forme invalide :\n ${errs.join('\n ')}`);
|
|
checked++;
|
|
}
|
|
}
|
|
// Le gel a couvert des providers réels : sinon la suite ne prouve rien.
|
|
assert.ok(checked >= 6, `seulement ${checked} items vérifiés, le gel est vide ou cassé`);
|
|
});
|
|
|
|
test('8.4 — aucun provider ne renvoie un objet vide déguisé', () => {
|
|
// Un provider sans résultat doit être un tableau VIDE, jamais `null`, `{}`
|
|
// ou un groupe à trous : ces valeurs cassent le front bien plus qu'une
|
|
// simple absence.
|
|
for (const [id, entry] of Object.entries(data.providers)) {
|
|
for (const [i, item] of entry.items.entries()) {
|
|
assert.equal(typeof item, 'object', `${id}[${i}] n'est pas un objet`);
|
|
assert.notEqual(Array.isArray(item), true, `${id}[${i}] est un tableau`);
|
|
assert.ok(item.title, `${id}[${i}] sans titre`);
|
|
assert.ok(item.id, `${id}[${i}] sans id`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('8.4 — les champs du contrat v2 sont tous connus des fixtures', () => {
|
|
// Sens de lecture : un champ émis par un adaptateur mais absent de
|
|
// SUGGESTION_V2_FIELDS n'est ni documenté ni lu par le front. Le gel le
|
|
// révèle (c'est le but de 8.5) ; on ne casse pas le test pour autant, on
|
|
// exige que la dérive soit visible dans le compte rendu.
|
|
// SUGGESTION_V2_FIELDS liste les AJOUTS v2 ; le contrat de base (v1) n'y est
|
|
// pas, donc on l'ajoute explicitement — sinon `duration`/`width`/`viewCount`
|
|
// paraîtraient être des champs inconnus.
|
|
const known = new Set([
|
|
...SUGGESTION_V2_FIELDS,
|
|
'id', 'title', 'url', 'thumbnail', 'uploaderName', 'type',
|
|
'duration', 'width', 'height', 'viewCount', 'viewersRaw',
|
|
]);
|
|
const seen = new Set();
|
|
for (const entry of Object.values(data.providers)) {
|
|
for (const item of entry.items) for (const k of Object.keys(item)) seen.add(k);
|
|
}
|
|
const undocumented = [...seen].filter((k) => !known.has(k)).sort();
|
|
// `badges` (YouTube) et `uploadedDate` (Dailymotion) sortent du contrat v2 :
|
|
// on les liste explicitement comme tolérés, pour ne pas les découvrir par
|
|
// hasard dans six mois.
|
|
const TOLERATED = new Set(['badges', 'uploadedDate']);
|
|
const unexpected = undocumented.filter((k) => !TOLERATED.has(k));
|
|
assert.deepEqual(unexpected, [],
|
|
`champs émis mais absents du contrat v2 : ${unexpected.join(', ')} — les ajouter à SUGGESTION_V2_FIELDS ou à TOLERATED`);
|
|
});
|
|
|
|
test('8.4 — la couverture gelée est explicite sur ses trous', () => {
|
|
// `tw` (sans identifiants) et `ru` (Cloudflare) n'ont pas pu être gelés.
|
|
// Un trou de couverture doit être ASSUMÉ dans le fichier, pas passer pour
|
|
// une couverture complète.
|
|
for (const [id, entry] of Object.entries(data.providers)) {
|
|
if (entry.items.length === 0) {
|
|
assert.ok(entry.note, `« ${id} » est vide sans note : la couverture manquante serait invisible`);
|
|
}
|
|
}
|
|
});
|