import { test } from "node:test"; import assert from "node:assert/strict"; import { METADATA_HISTORY_CAP, METADATA_HISTORY_MAX_VALUE_BYTES, appendMetadataHistoryEntry, buildMetadataHistoryEntry, coerceMetadataHistory, diffMetadata, isValueDigest, normalizeForDiff, type MetadataHistoryEntry, } from "./metadataHistory"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test lib/metadataHistory.test.ts // A small but real-shaped info json: content, counters, and the volatile keys // that move on every fetch (signed format URLs, epoch). function info(over: Record = {}): Record { return { id: "dbnS-cBgStY", title: "The original title", description: "First description.", duration: 3600, view_count: 100, like_count: 7, formats: [{ format_id: "18", url: "https://example/sig=aaa" }], thumbnails: [{ url: "https://i.ytimg.com/a.jpg" }], subtitles: {}, automatic_captions: { en: [{ url: "https://example/cap=1" }] }, epoch: 1_700_000_000, _version: { version: "2026.09.01" }, ...over, }; } const text = (o: unknown) => JSON.stringify(o); function entry( before: Record, after: Record, ): MetadataHistoryEntry | null { return buildMetadataHistoryEntry({ before: { text: text(before), mtime: new Date("2026-09-01T00:00:00.000Z") }, after: { text: text(after) }, at: "2026-09-26T12:00:00.000Z", by: "prefetch", }); } test("a formats-only rewrite is an all-volatile entry: nothing changed, nothing stored", () => { const e = entry( info(), info({ formats: [{ format_id: "18", url: "https://example/sig=bbb" }], epoch: 1_700_000_999, }), ); assert.ok(e); assert.deepEqual(e.changed, {}); assert.deepEqual(e.added, {}); assert.deepEqual(e.removed, {}); assert.deepEqual(e.counters, {}); assert.deepEqual(e.volatile, ["epoch", "formats"]); // The volatile VALUES are never stored — only that they moved. assert.ok(!JSON.stringify(e).includes("sig=bbb")); assert.equal(e.by, "prefetch"); assert.equal(e.from.at, "2026-09-01T00:00:00.000Z"); assert.notEqual(e.from.sha256, e.to.sha256); assert.equal(e.to.bytes, Buffer.byteLength(text(info({ formats: [{ format_id: "18", url: "https://example/sig=bbb" }], epoch: 1_700_000_999, })))); // Small: the operator asked to be told every rewrite, and this is what it costs. assert.ok(JSON.stringify(e).length < 400, String(JSON.stringify(e).length)); }); test("a title change is changed.title, both sides whole", () => { const e = entry(info(), info({ title: "A new title" })); assert.ok(e); assert.deepEqual(e.changed, { title: { from: "The original title", to: "A new title" }, }); assert.deepEqual(e.volatile, []); }); test("a new caption language is changed.subtitles_langs, though subtitles itself is volatile", () => { const e = entry( info(), info({ subtitles: { en: [{ url: "https://example/sub=1" }] } }), ); assert.ok(e); assert.deepEqual(e.changed, { subtitles_langs: { from: [], to: ["en"] }, }); assert.deepEqual(e.volatile, ["subtitles"]); }); test("a caption map appearing where there was none is added._langs", () => { const before = info(); delete before.subtitles; const e = entry(before, info({ subtitles: { de: [] } })); assert.ok(e); assert.deepEqual(e.added, { subtitles_langs: ["de"] }); }); test("counters that moved land in counters only, [from, to]", () => { const e = entry(info(), info({ view_count: 150, like_count: 7 })); assert.ok(e); assert.deepEqual(e.counters, { view_count: [100, 150] }); assert.deepEqual(e.changed, {}); // A counter the old file lacked reads from null. const e2 = entry(info(), info({ comment_count: 3 })); assert.deepEqual(e2?.counters, { comment_count: [null, 3] }); }); test("added and removed content keys", () => { const before = info({ availability: "public" }); const after = info({ chapters: [{ title: "Intro", start_time: 0 }] }); const e = entry(before, after); assert.ok(e); assert.deepEqual(e.added, { chapters: [{ title: "Intro", start_time: 0 }] }); assert.deepEqual(e.removed, { availability: "public" }); }); test("key order alone is not a change, at any depth", () => { const d = diffMetadata( { title: "t", chapters: [{ a: 1, b: 2 }], formats: [{ x: 1, y: 2 }] }, { chapters: [{ b: 2, a: 1 }], formats: [{ y: 2, x: 1 }], title: "t" }, ); assert.deepEqual(d, { changed: {}, added: {}, removed: {}, counters: {}, volatile: [], }); }); test("a stored value over 16 KB is replaced by its { sha256, bytes }", () => { const long = "x".repeat(METADATA_HISTORY_MAX_VALUE_BYTES + 10); const e = entry(info(), info({ description: long })); assert.ok(e); const { from, to } = e.changed.description as { from: unknown; to: unknown }; assert.equal(from, "First description."); assert.ok(isValueDigest(to), JSON.stringify(to).slice(0, 80)); assert.equal((to as { bytes: number }).bytes, long.length + 2); // its JSON: quotes // And a value at the cap is kept whole. const atCap = "y".repeat(METADATA_HISTORY_MAX_VALUE_BYTES - 2); const e2 = entry(info(), info({ description: atCap })); assert.equal((e2?.changed.description as { to: unknown }).to, atCap); }); test("byte-identical → no entry", () => { assert.equal(entry(info(), info()), null); }); test("an unparseable side records the rewrite with no diff", () => { const e = buildMetadataHistoryEntry({ before: { text: "{ torn" }, after: { text: text(info()) }, at: "2026-09-26T12:00:00.000Z", by: "audio-check", requestedBy: "mcp", }); assert.ok(e); assert.deepEqual(e.unparseable, ["from"]); assert.deepEqual(e.changed, {}); assert.deepEqual(e.added, {}); assert.equal(e.requestedBy, "mcp"); assert.equal("at" in e.from, false); }); test("the cap drops the oldest, newest stays last", () => { let h = null as ReturnType | null; const mk = (i: number) => ({ ...entry(info(), info({ title: `t${i}` }))!, at: `2026-09-26T12:00:${String(i % 60).padStart(2, "0")}.000Z`, }) as MetadataHistoryEntry; for (let i = 0; i < METADATA_HISTORY_CAP + 5; i++) { h = appendMetadataHistoryEntry(h, mk(i)); } assert.equal(h!.entries.length, METADATA_HISTORY_CAP); assert.deepEqual( (h!.entries[0].changed.title as { to: string }).to, "t5", ); assert.deepEqual( (h!.entries.at(-1)!.changed.title as { to: string }).to, `t${METADATA_HISTORY_CAP + 4}`, ); // A smaller cap, as passed. const small = appendMetadataHistoryEntry(h, mk(999), 3); assert.equal(small.entries.length, 3); }); test("normalizeForDiff splits content, counters and volatile fingerprints", () => { const n = normalizeForDiff(info()); assert.deepEqual(Object.keys(n.counters).sort(), ["like_count", "view_count"]); assert.deepEqual(n.content.automatic_captions_langs, ["en"]); assert.deepEqual(n.content.subtitles_langs, []); assert.equal("formats" in n.content, false); assert.match(n.volatile.formats, /^[0-9a-f]{64}$/); }); test("coerceMetadataHistory keeps good entries and drops torn ones", () => { const good = entry(info(), info({ title: "x" }))!; assert.equal(coerceMetadataHistory(null), null); assert.equal(coerceMetadataHistory({ entries: "no" }), null); assert.deepEqual(coerceMetadataHistory({ entries: [good, { at: 1 }, null] }), { entries: [good], }); });