import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { loadMetadataHistory, metadataHistoryPath, patchMetadataInfo, withMetadataHistory, } from "./metadataHistory-server"; // Run with: // pnpm --filter yt-dlp-transcript-common exec tsx --test lib/metadataHistory-server.test.ts async function withDir(fn: (dir: string) => Promise): Promise { const dir = await mkdtemp(path.join(os.tmpdir(), "metadata-history-")); try { await fn(dir); } finally { await rm(dir, { recursive: true, force: true }); } } const INFO = "metadata.info.json"; const meta = (title: string, view_count = 1) => JSON.stringify({ id: "v1", title, view_count, formats: [{ url: `u-${title}` }] }); test("a rewrite that changed the file appends one entry", async () => { await withDir(async (dir) => { await writeFile(path.join(dir, INFO), meta("old", 10)); const out = await withMetadataHistory( dir, { by: "prefetch", requestedBy: "umtool" }, async () => { await writeFile(path.join(dir, INFO), meta("new", 12)); return 42; }, ); assert.equal(out, 42); const h = await loadMetadataHistory(dir); assert.equal(h?.entries.length, 1); const e = h!.entries[0]; assert.equal(e.by, "prefetch"); assert.equal(e.requestedBy, "umtool"); assert.deepEqual(e.changed.title, { from: "old", to: "new" }); assert.deepEqual(e.counters.view_count, [10, 12]); assert.deepEqual(e.volatile, ["formats"]); assert.ok(e.from.at, "the old file's mtime is recorded"); // A second rewrite appends, newest last. await withMetadataHistory(dir, { by: "audio-check" }, async () => { await writeFile(path.join(dir, INFO), meta("newer", 12)); }); const h2 = await loadMetadataHistory(dir); assert.deepEqual( h2?.entries.map((x) => x.by), ["prefetch", "audio-check"], ); assert.equal("requestedBy" in h2!.entries[1], false); // Written through the sidecar: indented JSON with a trailing newline, and // no temp file left behind. const raw = await readFile(metadataHistoryPath(dir), "utf8"); assert.ok(raw.startsWith("{\n") && raw.endsWith("}\n")); assert.deepEqual((await readdir(dir)).sort(), [ "metadata.history.json", "metadata.info.json", ]); }); }); test("an identical rewrite records nothing", async () => { await withDir(async (dir) => { await writeFile(path.join(dir, INFO), meta("same")); await withMetadataHistory(dir, { by: "prefetch" }, async () => { await writeFile(path.join(dir, INFO), meta("same")); }); assert.equal(await loadMetadataHistory(dir), null); assert.deepEqual(await readdir(dir), [INFO]); }); }); test("no prior file records nothing: a first write is not a rewrite", async () => { await withDir(async (dir) => { await withMetadataHistory(dir, { by: "prefetch" }, async () => { await writeFile(path.join(dir, INFO), meta("first")); }); assert.equal(await loadMetadataHistory(dir), null); assert.deepEqual(await readdir(dir), [INFO]); }); }); test("a run that throws still records the rewrite, and its error comes through", async () => { await withDir(async (dir) => { await writeFile(path.join(dir, INFO), meta("before")); await assert.rejects( withMetadataHistory(dir, { by: "download-one" }, async () => { await writeFile(path.join(dir, INFO), meta("after")); throw new Error("yt-dlp exited 1"); }), /yt-dlp exited 1/, ); const h = await loadMetadataHistory(dir); assert.equal(h?.entries.length, 1); assert.equal(h!.entries[0].by, "download-one"); assert.deepEqual(h!.entries[0].changed.title, { from: "before", to: "after" }); }); }); test("a failure to record is logged, never thrown", async () => { await withDir(async (dir) => { await writeFile(path.join(dir, INFO), meta("a")); // A DIRECTORY where the history file goes: the atomic rename onto it fails. await import("node:fs/promises").then((fs) => fs.mkdir(metadataHistoryPath(dir)), ); let log = ""; const out = await withMetadataHistory( dir, { by: "prefetch", onLog: (s) => (log += s) }, async () => { await writeFile(path.join(dir, INFO), meta("b")); return "ran"; }, ); assert.equal(out, "ran"); assert.match(log, /Could not record the metadata history/); }); }); test("patchMetadataInfo merges in place, appends new keys in order, and records the rewrite", async () => { await withDir(async (dir) => { const original = { id: "v1", title: "v1", formats: [{ url: "u" }], _version: { v: 1 } }; await writeFile(path.join(dir, INFO), JSON.stringify(original)); const out = await patchMetadataInfo( dir, { title: "Real", description: "About", upload_date: "20240305" }, { by: "feed-backfill", requestedBy: "test" }, ); assert.deepEqual(out, { written: true }); const text = await readFile(path.join(dir, INFO), "utf8"); const after = JSON.parse(text); assert.deepEqual(Object.keys(after), ["id", "title", "formats", "_version", "description", "upload_date"]); assert.equal(after.title, "Real"); assert.deepEqual(after.formats, original.formats); assert.equal(text.endsWith("\n"), false, "compact, no trailing newline"); const h = await loadMetadataHistory(dir); assert.equal(h?.entries.length, 1); assert.equal(h!.entries[0].by, "feed-backfill"); assert.equal(h!.entries[0].requestedBy, "test"); assert.deepEqual(h!.entries[0].changed.title, { from: "v1", to: "Real" }); assert.deepEqual(h!.entries[0].added, { description: "About", upload_date: "20240305" }); // No temp file is left behind. assert.deepEqual((await readdir(dir)).sort(), [INFO, "metadata.history.json"].sort()); }); }); test("patchMetadataInfo: a patch that changes nothing writes nothing", async () => { await withDir(async (dir) => { const text = JSON.stringify({ id: "v1", title: "Same" }); await writeFile(path.join(dir, INFO), text); const out = await patchMetadataInfo(dir, { title: "Same", description: undefined }, { by: "feed-backfill" }); assert.deepEqual(out, { written: false }); assert.equal(await readFile(path.join(dir, INFO), "utf8"), text); assert.equal(await loadMetadataHistory(dir), null); }); }); test("patchMetadataInfo refuses a missing file or one that is not a JSON object", async () => { await withDir(async (dir) => { await assert.rejects(patchMetadataInfo(dir, { title: "x" }, { by: "feed-backfill" }), /ENOENT/); await writeFile(path.join(dir, INFO), "[1,2]"); await assert.rejects(patchMetadataInfo(dir, { title: "x" }, { by: "feed-backfill" }), /not a JSON object/); await writeFile(path.join(dir, INFO), "{torn"); await assert.rejects(patchMetadataInfo(dir, { title: "x" }, { by: "feed-backfill" }), /not JSON/); }); });