// Takes: what take.json may say, which preview a request may open, and the // verdicts file another agent reads back. // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { isTakeId, listTakes, parseTake, previewFile, readVerdicts, setTakeVerdict, verdictsFileOf, } from "./takes.mjs"; const good = (over = {}) => ({ id: "slow-burn", group: "finale", order: 3, label: "Slow burn", kind: "similar", summary: "Same words; slower beat", changes: ["beat 1.35 s (was 1.05)"], preview: "preview.mp4", seconds: 38.2, builtAt: "2026-10-06T23:10:00Z", ...over, }); async function project() { const dir = await mkdtemp(path.join(tmpdir(), "takes-")); return dir; } async function writeTake(projectDir, id, doc, { preview = true } = {}) { const dir = path.join(projectDir, "takes", id); await mkdir(dir, { recursive: true }); await writeFile(path.join(dir, "take.json"), typeof doc === "string" ? doc : JSON.stringify(doc)); if (preview) await writeFile(path.join(dir, "preview.mp4"), "not really an mp4"); return dir; } test("take ids are one lowercase url segment", () => { for (const ok of ["a", "slow-burn", "take-01", "0", "a".repeat(64)]) assert.equal(isTakeId(ok), true, ok); for (const bad of ["", "-a", "Slow", "a_b", "a.b", "a/b", "..", "a".repeat(65), null, 3]) { assert.equal(isTakeId(bad), false, String(bad)); } }); test("parseTake accepts the contract and fills the optional fields", () => { const r = parseTake("slow-burn", good()); assert.ok("take" in r); assert.deepEqual(r.take, { id: "slow-burn", group: "finale", order: 3, label: "Slow burn", kind: "similar", summary: "Same words; slower beat", changes: ["beat 1.35 s (was 1.05)"], preview: "preview.mp4", seconds: 38.2, builtAt: "2026-10-06T23:10:00Z", }); const min = parseTake("x", { id: "x", group: "g", order: 0, label: "X", kind: "reference", preview: "p.mp4" }); assert.ok("take" in min); assert.equal(min.take.summary, ""); assert.deepEqual(min.take.changes, []); assert.equal(min.take.seconds, null); assert.equal(min.take.builtAt, null); }); test("parseTake refuses a take it cannot show truthfully", () => { const cases = [ ["slow-burn", [], "not an object"], ["slow-burn", good({ id: "other" }), "not the directory"], ["Bad_Dir", good({ id: "Bad_Dir" }), "directory name"], ["slow-burn", good({ group: " " }), "group"], ["slow-burn", good({ order: "3" }), "order"], ["slow-burn", good({ order: Number.NaN }), "order"], ["slow-burn", good({ label: undefined }), "label"], ["slow-burn", good({ kind: "other" }), "kind"], ["slow-burn", good({ preview: "" }), "preview"], ["slow-burn", good({ preview: "/etc/passwd" }), "relative"], ["slow-burn", good({ preview: "../other/preview.mp4" }), "relative"], ["slow-burn", good({ preview: "a//b.mp4" }), "relative"], ["slow-burn", good({ changes: "beat 1.35" }), "changes"], ["slow-burn", good({ changes: [1] }), "changes"], ["slow-burn", good({ summary: 4 }), "summary"], ["slow-burn", good({ seconds: "38" }), "seconds"], ["slow-burn", good({ builtAt: 1 }), "builtAt"], ]; for (const [dir, doc, want] of cases) { const r = parseTake(dir, doc); assert.ok("error" in r, `${JSON.stringify(doc)} should fail`); assert.match(r.error, new RegExp(want), r.error); } }); test("listTakes: no takes/ is a state of its own, not an error", async () => { const dir = await project(); try { assert.deepEqual(await listTakes(dir), { exists: false, takes: [], groups: [], skipped: [] }); } finally { await rm(dir, { recursive: true, force: true }); } }); test("listTakes groups, sorts, skips with a reason, and ignores what is not a take", async () => { const dir = await project(); try { await writeTake(dir, "slow-burn", good()); await writeTake(dir, "ref", good({ id: "ref", label: "Reference", kind: "reference", order: 1 })); await writeTake(dir, "hard-cut", good({ id: "hard-cut", label: "Hard cut", kind: "different", order: 2 }), { preview: false }); await writeTake(dir, "intro-a", good({ id: "intro-a", group: "opening", order: 5 })); await writeTake(dir, "broken", "{ not json"); await writeTake(dir, "wrong-kind", good({ id: "wrong-kind", kind: "maybe" })); // A work directory and loose files: not takes, not listed, not "skipped". await mkdir(path.join(dir, "takes", "current", "out"), { recursive: true }); await writeFile(path.join(dir, "takes", "make-takes.py"), ""); await writeFile(path.join(dir, "takes", "verdicts.json"), "{}"); const r = await listTakes(dir); assert.equal(r.exists, true); assert.deepEqual(r.groups.map((g) => [g.group, g.takes.map((t) => t.id)]), [ ["finale", ["ref", "hard-cut", "slow-burn"]], ["opening", ["intro-a"]], ]); assert.deepEqual(r.skipped.map((s) => s.dir), ["broken", "wrong-kind"]); assert.match(r.skipped[0].why, /does not parse/); assert.match(r.skipped[1].why, /kind/); const hard = r.takes.find((t) => t.id === "hard-cut"); assert.equal(hard.previewSize, null, "a preview not rendered yet is listed, without a size"); const ref = r.takes.find((t) => t.id === "ref"); assert.equal(ref.previewSize, "not really an mp4".length); assert.equal(typeof ref.previewMtimeMs, "number"); } finally { await rm(dir, { recursive: true, force: true }); } }); test("previewFile never leaves the take's own directory", async () => { const dir = await project(); const outside = await mkdtemp(path.join(tmpdir(), "takes-outside-")); try { const takeDir = await writeTake(dir, "ok", good({ id: "ok" })); await writeFile(path.join(outside, "secret.mp4"), "secret"); assert.ok(await previewFile(dir, { id: "ok", preview: "preview.mp4" })); // Lexical escapes, even if parseTake were bypassed. assert.equal(await previewFile(dir, { id: "ok", preview: "../../video.manifest.json" }), null); assert.equal(await previewFile(dir, { id: "../ok", preview: "preview.mp4" }), null); assert.equal(await previewFile(dir, { id: "ok", preview: "." }), null); // A symlinked preview pointing out of the take. await symlink(path.join(outside, "secret.mp4"), path.join(takeDir, "leak.mp4")); assert.equal(await previewFile(dir, { id: "ok", preview: "leak.mp4" }), null); // A take directory that is itself a link out of takes/. await mkdir(path.join(outside, "linked")); await writeFile(path.join(outside, "linked", "preview.mp4"), "x"); await symlink(path.join(outside, "linked"), path.join(dir, "takes", "linked")); assert.equal(await previewFile(dir, { id: "linked", preview: "preview.mp4" }), null); // A directory is not a file; a missing file is null. await mkdir(path.join(takeDir, "sub")); assert.equal(await previewFile(dir, { id: "ok", preview: "sub" }), null); assert.equal(await previewFile(dir, { id: "ok", preview: "nope.mp4" }), null); } finally { await rm(dir, { recursive: true, force: true }); await rm(outside, { recursive: true, force: true }); } }); test("setTakeVerdict writes the contract's shape, merges, and removes an empty row", async () => { const dir = await project(); try { await mkdir(path.join(dir, "takes"), { recursive: true }); assert.deepEqual(await readVerdicts(dir), {}); const a = await setTakeVerdict(dir, "slow-burn", { verdict: "like" }); assert.equal(a.entry.verdict, "like"); assert.equal(a.entry.note, ""); assert.ok(!Number.isNaN(Date.parse(a.entry.at))); // A note alone keeps the verdict; trailing whitespace is not kept. await setTakeVerdict(dir, "slow-burn", { note: "the wait is right \n" }); await setTakeVerdict(dir, "ref", { verdict: "no", note: "too fast" }); const onDisk = JSON.parse(await readFile(verdictsFileOf(dir), "utf8")); assert.deepEqual(Object.keys(onDisk).sort(), ["ref", "slow-burn"]); assert.deepEqual( { verdict: onDisk["slow-burn"].verdict, note: onDisk["slow-burn"].note }, { verdict: "like", note: "the wait is right" }, ); assert.deepEqual(Object.keys(onDisk["ref"]).sort(), ["at", "note", "verdict"]); // Clearing the verdict keeps a row with a note; clearing both removes it. await setTakeVerdict(dir, "ref", { verdict: null }); assert.deepEqual((await readVerdicts(dir)).ref.verdict, null); const gone = await setTakeVerdict(dir, "ref", { note: "" }); assert.equal(gone.entry, null); assert.deepEqual(Object.keys(await readVerdicts(dir)), ["slow-burn"]); } finally { await rm(dir, { recursive: true, force: true }); } }); test("setTakeVerdict refuses bad input and never overwrites a file it cannot read", async () => { const dir = await project(); try { await mkdir(path.join(dir, "takes"), { recursive: true }); await assert.rejects(setTakeVerdict(dir, "../x", { verdict: "like" }), /take id/); await assert.rejects(setTakeVerdict(dir, "a", { verdict: "keep" }), /verdict must be/); await assert.rejects(setTakeVerdict(dir, "a", { note: 4 }), /note must be/); await writeFile(verdictsFileOf(dir), "{ half a file"); await assert.rejects(setTakeVerdict(dir, "a", { verdict: "like" }), /does not parse/); assert.equal(await readFile(verdictsFileOf(dir), "utf8"), "{ half a file"); } finally { await rm(dir, { recursive: true, force: true }); } }); test("concurrent verdicts on different takes are all kept", async () => { const dir = await project(); try { await mkdir(path.join(dir, "takes"), { recursive: true }); const ids = Array.from({ length: 12 }, (_, i) => `t${i}`); await Promise.all(ids.map((id, i) => setTakeVerdict(dir, id, { verdict: ["like", "maybe", "no"][i % 3] }))); assert.deepEqual(Object.keys(await readVerdicts(dir)).sort(), [...ids].sort()); } finally { await rm(dir, { recursive: true, force: true }); } }); test("takesWithVerdicts joins take.json and verdicts.json, keeping a verdict on a vanished take", async () => { const { takesWithVerdicts } = await import("./takes.mjs"); const dir = await mkdtemp(path.join(tmpdir(), "umtool-takes-digest-")); try { await mkdir(path.join(dir, "takes", "a"), { recursive: true }); await writeFile( path.join(dir, "takes", "a", "take.json"), JSON.stringify({ id: "a", group: "g", order: 1, label: "A", kind: "reference", preview: "p.mp4", summary: "s" }), ); await writeFile( path.join(dir, "takes", "verdicts.json"), JSON.stringify({ a: { verdict: "like", note: "yes", at: "x" }, gone: { verdict: "no", note: "", at: "y" } }), ); const rows = await takesWithVerdicts(dir); assert.deepEqual(rows.map((r) => [r.id, r.label, r.verdict, r.note, r.missing ?? false]), [ ["a", "A", "like", "yes", false], ["gone", null, "no", "", true], ]); } finally { await rm(dir, { recursive: true }); } });