feat(suggest): enrichissement web gratuit (Bing+Google) et titres Odysee Lighthouse

This commit is contained in:
2026-09-26 13:45:23 -04:00
parent 4578f5b887
commit d9c8a2e14e
5 changed files with 203 additions and 7 deletions
+42 -3
View File
@@ -21,6 +21,7 @@ const yt = (await import('../providers/youtube.mjs')).default;
const { parseYoutubeSuggestResponse } = await import('../providers/youtube.mjs');
const dm = (await import('../providers/dailymotion.mjs')).default;
const { dedupeSuggestGroups } = await import('../suggest.mjs');
const { parseOsjsonList, parseLighthouseList, humanizeClaimName } = await import('../suggest-web.mjs');
// Bare JSON payload
expect(
@@ -68,6 +69,25 @@ logOk('suggest min-length guard (no network)');
logOk('dedupeSuggestGroups cross-provider (no duplicates)');
}
// Web enrichment parsers (offline): Bing/Google OSJSON + Odysee Lighthouse
{
expect(
JSON.stringify(parseOsjsonList('["tut",["tutoriel angular","tutoriel android"],[],{}]')) ===
JSON.stringify(['tutoriel angular', 'tutoriel android']),
'OSJSON payload parses',
);
expect(parseOsjsonList('garbage').length === 0, 'OSJSON garbage yields []');
expect(parseOsjsonList('["tut","notalist"]').length === 0, 'OSJSON non-list yields []');
logOk('parseOsjsonList (Bing/Google)');
expect(humanizeClaimName('tutoriel-express_nettoyage--moulinet') === 'tutoriel express nettoyage moulinet', 'claim slug humanized');
expect(
JSON.stringify(parseLighthouseList([{ name: 'tutoriel-express' }, { name: 'x' }, { name: '' }], 10)) ===
JSON.stringify(['tutoriel express']),
'lighthouse list keeps readable titles only',
);
logOk('parseLighthouseList (Odysee)');
}
// ---------- 2) API contract against a real isolated server ----------
const PORT = await new Promise((resolve) => {
const srv = net.createServer();
@@ -114,12 +134,14 @@ try {
expect(res.status === 400, `expected 400, got ${res.status}`);
});
await scenario('providers=yt returns only yt suggestions', async () => {
await scenario('providers=yt returns only yt suggestions (+optional web enrichment)', async () => {
const res = await fetch(`${baseUrl}/api/search/suggest?q=tutoriel&providers=yt&limit=5`);
expect(res.status === 200, `expected 200, got ${res.status}`);
const body = await res.json();
expect(body.q === 'tutoriel', 'q echoed');
expect(JSON.stringify(Object.keys(body.groups)) === JSON.stringify(['yt']), `only yt group (got ${Object.keys(body.groups)})`);
const keys = Object.keys(body.groups);
expect(keys.includes('yt'), `yt group present (got ${keys})`);
expect(keys.every((k) => k === 'yt' || k === 'web'), `only yt (+web) groups (got ${keys})`);
expect(Array.isArray(body.groups.yt), 'yt group is an array');
expect(body.groups.yt.length <= 5, 'limit respected');
});
@@ -129,7 +151,24 @@ try {
expect(res.status === 200, `expected 200, got ${res.status}`);
const body = await res.json();
const keys = Object.keys(body.groups).sort();
expect(JSON.stringify(keys) === JSON.stringify(['dm', 'od', 'pt', 'ru', 'tw', 'yt']), `full registry fallback (got ${keys})`);
const withoutWeb = keys.filter((k) => k !== 'web');
expect(JSON.stringify(withoutWeb) === JSON.stringify(['dm', 'od', 'pt', 'ru', 'tw', 'yt']), `full registry fallback (got ${keys})`);
});
await scenario('no duplicate occurrence across groups (web enrichment included)', async () => {
const res = await fetch(`${baseUrl}/api/search/suggest?q=tutoriel&limit=10`);
expect(res.status === 200, `expected 200, got ${res.status}`);
const body = await res.json();
const seen = new Set();
for (const list of Object.values(body.groups)) {
for (const raw of Array.isArray(list) ? list : []) {
const k = String(raw || '').trim().toLowerCase().replace(/\s+/g, ' ');
expect(!seen.has(k), `duplicate occurrence across groups: "${raw}"`);
seen.add(k);
}
}
const total = Object.values(body.groups).reduce((n, l) => n + (Array.isArray(l) ? l.length : 0), 0);
expect(total > 0, 'enrichment yields at least one suggestion');
});
} finally {
server.kill();