feat(providers): phases 7.3/7.4/7.6/8.1 — provenance, health, NDJSON, contrat unique
CI / build-and-test (push) Successful in 14m43s
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.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 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`);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user