/** * Phase 3.12 — persistance des `pageToken` YouTube en base. * * Le test porte sur le comportement observable, pas sur l'implémentation : * un jeton écrit par un « process A » doit être relisible par un « process B » * qui ne partage aucun état mémoire. C'est exactement ce qui échouait avec la * Map process-local — et ce que le redémarrage en production provocait. */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'newtube-tokens-')); process.env.NEWTUBE_DB_FILE = path.join(tmpDir, 'tokens.db'); const db = await import('../db.mjs'); test('3.12 — un jeton écrit est relu par un autre process (L2 partagé)', () => { const key = 'yt|UCtest|pl|24'; assert.equal(db.getCachedPageTokens(key), null, 'clé absente au départ'); assert.equal(db.setCachedPageTokens(key, [undefined, 'TOKEN_PAGE_2', 'TOKEN_PAGE_3']), true); // On repart d'un cache mémoire vierge : c'est le cas d'un process neuf ou d'un // pod différent. Seul le L2 peut répondre ici. const reread = db.getCachedPageTokens(key); assert.ok(Array.isArray(reread), 'le jeton doit survivre hors du cache mémoire'); // Indexation : `tokens[n]` = jeton pour la page n+2 (l'index 1 est donc la // page 2). Un trou est matérialisé par `''`, jamais par un décalage : si le // round-trip compressait le tableau, le jeton de la page 2 se retrouverait à // l'index 0 et la page 3 renverrait celui de la page 4. assert.equal(reread[0], '', 'le trou de l\'index 0 reste un trou, pas un décalage'); assert.equal(reread[1], 'TOKEN_PAGE_2', 'tokens[1] = jeton de la page 2'); assert.equal(reread[2], 'TOKEN_PAGE_3', 'tokens[2] = jeton de la page 3'); }); test('3.12 — un jeton absent reste absent (jamais de token inventé)', () => { assert.equal(db.getCachedPageTokens('yt|UCinconnu|pl|24'), null); }); test('3.12 — un jeton expiré n\'est pas resservi', () => { const key = 'yt|UCexpire|pl|24'; db.setCachedPageTokens(key, ['TOK'], 1); // TTL de 1 ms : la ligne est déjà hors délai au tour suivant. assert.equal(db.getCachedPageTokens(key), null, 'un jeton expiré doit être traité comme absent'); // Et la ligne doit avoir été retirée, pas laissée traîner. assert.equal(db.getCachedPageTokens(key), null); }); test('3.12 — une écriture vide est refusée (pas de ligne inutile)', () => { assert.equal(db.setCachedPageTokens('yt|vide|pl|24', []), false); assert.equal(db.getCachedPageTokens('yt|vide|pl|24'), null); }); test('3.12 — les jetons ne se confondent pas avec les résultats de recherche', () => { // Namespace distinct : sinon le plafond par fournisseur et les stats de cache // du fournisseur `yt` seraient faussés par des jetons qui ne sont pas des // résultats. const key = 'collision|1'; db.setCachedPageTokens(key, ['TOK_A']); // `setCachedSearch` n'écrit que des tableaux d'items, jamais sous ce provider. assert.equal(db.setCachedSearch('yt_tokens', key, 'q', ['item'], 'api'), true); // Le même cache_key sous deux providers ne se chevauche pas : la lecture // page_token et la lecture search restent distinctes. assert.ok(db.getCachedPageTokens(key), 'le jeton reste lisible'); }); test('3.12 — une chaîne de jetons se complète sans écraser les pages précédentes', () => { const key = 'yt|UCchain|pl|24'; // Page 1 écrite seule, puis page 2 : le tableau doit garder les deux. db.setCachedPageTokens(key, ['TOK_PAGE_2']); assert.equal(db.getCachedPageTokens(key)[0], 'TOK_PAGE_2'); db.setCachedPageTokens(key, ['TOK_PAGE_2', 'TOK_PAGE_3']); const arr = db.getCachedPageTokens(key); assert.equal(arr.length, 2); assert.equal(arr[0], 'TOK_PAGE_2', 'la page 2 ne doit pas disparaître'); assert.equal(arr[1], 'TOK_PAGE_3'); }); test.after(() => { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {} });