import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, readFile, writeFile, readdir } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { SIDECAR_FILENAMES, sidecar, sidecarField } from "./sidecar-server"; import { SUB_FILE_RE } from "./videoStatus"; // Every sidecar module, so the enumeration below sees every declaration. import "./archiveOrg-server"; import "./attribution-server"; import "./diarization-server"; import "./digest-server"; import "./metadataHistory-server"; import "./transcriptPin-server"; import "./wayback-server"; import { availabilitySidecar, loadAvailability, writeAvailability, } from "./availability-server"; import { loadDownloadOutcome, writeDownloadOutcome, } from "./downloadOutcome-server"; import { loadTranscribeOutcome, writeTranscribeOutcome, } from "./transcribeOutcome-server"; import { isDoNotClean, loadDoNotClean, setDoNotClean, } from "./doNotClean-server"; import { isExcludedFromTruncatedCheck, loadExcludeTruncatedCheck, setExcludedFromTruncatedCheck, } from "./excludeTruncatedCheck-server"; import type { AvailabilityRecord } from "./availability"; import type { DownloadOutcomeRecord } from "./downloadOutcome"; import type { TranscribeOutcomeRecord } from "./transcribeOutcome"; async function scratch(): Promise { return mkdtemp(path.join(os.tmpdir(), "sidecar-")); } test("every declared sidecar filename escapes SUB_FILE_RE, and all twelve are declared", () => { assert.deepEqual([...SIDECAR_FILENAMES].sort(), [ "ai-digest.json", "ai-digest.overrides.json", "archiveorg.json", "attribution.json", "availability.json", "diarization.json", "do-not-clean.json", "download-outcome.json", "exclude-truncated-check.json", "metadata.history.json", "transcribe-outcome.json", "transcript-pin.json", "wayback.json", ]); for (const name of SIDECAR_FILENAMES) { assert.ok(!SUB_FILE_RE.test(name), name); } }); test("sidecar() refuses a transcript.. name at declaration", () => { assert.throws( () => sidecar("transcript.speakers.json", sidecarField((v) => v)), /SUB_FILE_RE/, ); assert.throws(() => sidecar("a/b.json", sidecarField((v) => v)), /bare filename/); }); test("absent, unparseable and coercion-rejected all load as null", async () => { const dir = await scratch(); assert.equal(await loadAvailability(dir), null); await writeFile(availabilitySidecar.path(dir), "{ not json"); assert.equal(await loadAvailability(dir), null); await writeFile(availabilitySidecar.path(dir), JSON.stringify({ availability: "nope", checkedAt: "x" })); assert.equal(await loadAvailability(dir), null); await writeFile(availabilitySidecar.path(dir), "null"); assert.equal(await loadAvailability(dir), null); }); test("a coercion that throws reads as null, not as an exception", async () => { const dir = await scratch(); const s = sidecar( "throwing-test.json", sidecarField((): null => { throw new Error("boom"); }), ); await writeFile(s.path(dir), "{}"); assert.equal(await s.load(dir), null); }); test("doNotClean / excludeTruncatedCheck: a malformed marker still reads as present", async () => { const dir = await scratch(); for (const [name, load, is] of [ ["do-not-clean.json", loadDoNotClean, isDoNotClean], ["exclude-truncated-check.json", loadExcludeTruncatedCheck, isExcludedFromTruncatedCheck], ] as const) { assert.equal(await load(dir), null); assert.equal(await is(dir), false); await writeFile(path.join(dir, name), JSON.stringify({ nope: 1 })); assert.deepEqual(await load(dir), { setAt: "" }); await writeFile(path.join(dir, name), "{ torn"); assert.equal(await load(dir), null); } }); test("setDoNotClean / setExcludedFromTruncatedCheck: write, load, remove", async () => { const dir = await scratch(); await setDoNotClean(dir, true, "kept"); const dnc = await loadDoNotClean(dir); assert.equal(dnc?.note, "kept"); assert.ok(dnc?.setAt); await setDoNotClean(dir, false); assert.equal(await loadDoNotClean(dir), null); await setExcludedFromTruncatedCheck(dir, true); const etc = await loadExcludeTruncatedCheck(dir); assert.ok(etc?.setAt); assert.equal("note" in (etc ?? {}), false); await setExcludedFromTruncatedCheck(dir, false); assert.equal(await loadExcludeTruncatedCheck(dir), null); assert.deepEqual(await readdir(dir), []); }); test("availability / download-outcome / transcribe-outcome round-trip, indented with a newline", async () => { const dir = await scratch(); const avail: AvailabilityRecord = { checkedAt: "2026-01-01T00:00:00.000Z", availability: "public", history: [{ availability: "public", observedAt: "2026-01-01T00:00:00.000Z", source: "check" }], } as AvailabilityRecord; await writeAvailability(dir, avail); assert.deepEqual(await loadAvailability(dir), avail); assert.equal( await readFile(path.join(dir, "availability.json"), "utf8"), JSON.stringify(avail, null, 2) + "\n", ); const dlo = { status: "ok", videoId: "v1", startedAt: "2026-01-01T00:00:00.000Z", finishedAt: "2026-01-01T00:01:00.000Z", attempts: [], } as unknown as DownloadOutcomeRecord; await writeDownloadOutcome(dir, dlo); assert.deepEqual(await loadDownloadOutcome(dir), dlo); const tro = { videoId: "v1", transcribedAt: "2026-01-01T00:00:00.000Z", } as unknown as TranscribeOutcomeRecord; await writeTranscribeOutcome(dir, tro); assert.deepEqual(await loadTranscribeOutcome(dir), tro); }); test("a load keeps keys the shape check does not name (no strip on a sidecar)", async () => { const dir = await scratch(); const raw = { videoId: "v1", transcribedAt: "2026-01-01T00:00:00.000Z", extra: { kept: true }, }; await writeFile(path.join(dir, "transcribe-outcome.json"), JSON.stringify(raw)); assert.deepEqual(await loadTranscribeOutcome(dir), raw); });