import { test } from "node:test"; import assert from "node:assert/strict"; import type { ReportMediaIndex } from "yt-dlp-transcript-common/publish/reportMedia"; import { publishRefusal, reportMediaSummary, reportRow, reportRows, withReportMoved, withReportPublished, withReportUnpublished, type ReportFileRead, } from "./reportList"; // Run with: pnpm -C editor exec tsx --test "app/**/*.test.ts" function factcheck(id = "demo-factcheck"): Record { return { format: "archilyzer-report", version: 1, id, kind: "factcheck", title: "A demo fact-check", subtitle: "One line under it", published: "2026-10-01", updated: "2026-10-04", verdicts: { PARTLY: { label: "Half true" } }, sources: { s0: { kind: "article", title: "A demo article", url: "https://example.test/a" } }, citations: { c01: { kind: "video", channel: "demo-channel", id: "abc123", start: 10, end: 20, quote: "q1" }, c02: { kind: "video", channel: "demo-channel", id: "abc123", start: 30, end: 40, quote: "q2" }, p01: { kind: "post", channel: "demo-channel", id: "1234567890", quote: "q3" }, a01: { kind: "source", source: "s0", quote: "the claim" }, }, sections: [ { id: "ch1", title: "Chapter one", claims: [ { id: "k1", text: "One.", verdict: "CONTRADICTED", citations: ["c01", "p01"] }, { id: "k2", text: "Two.", verdict: "PARTLY", sourceQuote: { citation: "a01" }, citations: ["c02"] }, { id: "k3", text: "Three.", verdict: "CONTRADICTED", citations: ["c01"] }, ], }, { id: "ch2", title: "Chapter two" }, ], }; } function sweep(id: string): Record { return { format: "archilyzer-report", version: 1, id, kind: "sweep", title: "A demo sweep", citations: { c01: { kind: "video", channel: "demo-channel", id: "abc123", start: 10, end: 20, quote: "q1" }, }, sections: [{ id: "s1", title: "Found", body: "Said [here](cite:c01)." }], }; } const ok = (value: unknown): ReportFileRead => ({ ok: true, value }); test("a sound fact-check: counts, citations by kind in the model's order, the tally under its overrides", () => { const row = reportRow("demo-factcheck", ok(factcheck()), 0); assert.equal(row.status, "ok"); assert.deepEqual(row.problems, []); assert.equal(row.title, "A demo fact-check"); assert.equal(row.subtitle, "One line under it"); assert.equal(row.kind, "factcheck"); assert.equal(row.publishedDate, "2026-10-01"); assert.equal(row.updatedDate, "2026-10-04"); assert.equal(row.sections, 2); assert.equal(row.claims, 3); assert.equal(row.citations, 4); assert.deepEqual(row.citationsByKind, [ { kind: "video", count: 2 }, { kind: "post", count: 1 }, { kind: "source", count: 1 }, ]); // The vocabulary's order, not the claims'; only verdicts a claim carries. assert.deepEqual(row.tally, [ { verdict: "PARTLY", count: 1 }, { verdict: "CONTRADICTED", count: 2 }, ]); assert.equal(row.verdicts?.PARTLY.label, "Half true"); assert.equal(publishRefusal(row), null); }); test("a sweep carries no tally", () => { const row = reportRow("demo-sweep", ok(sweep("demo-sweep")), null); assert.equal(row.status, "ok"); assert.equal(row.kind, "sweep"); assert.equal(row.claims, 0); assert.equal(row.tally, undefined); assert.equal(row.verdicts, undefined); }); test("value problems are listed with their paths, and refuse publishing", () => { const doc = factcheck("demo-factcheck"); // The id does not match the directory, and a claim names no citation. const row = reportRow("other-dir", ok({ ...doc, sections: [{ id: "ch1", title: "One", claims: [{ id: "k1", text: "x", citations: ["nope"] }] }] }), null); assert.equal(row.status, "problems"); assert.deepEqual( row.problems.map((p) => p.path), ["id", "sections[0].claims[0].citations[0]"], ); // Still summarised: it parsed. assert.equal(row.title, "A demo fact-check"); assert.match(publishRefusal(row) ?? "", /2 problem/); }); test("not a report, not JSON, unreadable and missing are rows with a problem, never thrown", () => { const shape = reportRow("bad", ok({ format: "something-else" }), null); assert.equal(shape.status, "invalid"); assert.ok(shape.problems.length > 0); assert.equal(shape.title, undefined); assert.equal(shape.citations, 0); assert.match(publishRefusal(shape) ?? "", /not a valid report/); const notJson = reportRow("bad", { ok: false, reason: "unparseable" }, null); assert.equal(notJson.status, "invalid"); assert.deepEqual(notJson.problems, [{ path: "", message: "report.json is not JSON" }]); assert.equal(reportRow("bad", { ok: false, reason: "unreadable" }, null).problems[0].message, "report.json cannot be read"); const missing = reportRow("gone", { ok: false, reason: "absent" }, 2); assert.equal(missing.status, "missing"); assert.equal(missing.position, 2); assert.match(publishRefusal(missing) ?? "", /no report\.json/); }); test("rows: the published list in the site's order, then drafts by id; a listed id with no directory is missing", () => { const reads = new Map([ ["b-report", ok(sweep("b-report"))], ["a-report", ok(sweep("a-report"))], ["z-draft", ok(sweep("z-draft"))], ["c-draft", ok(sweep("c-draft"))], ]); const rows = reportRows(["b-report", "a-report", "listed-only"], ["z-draft", "a-report", "c-draft", "b-report"], reads); assert.deepEqual( rows.map((r) => [r.id, r.position, r.status]), [ ["b-report", 0, "ok"], ["a-report", 1, "ok"], ["listed-only", 2, "missing"], ["c-draft", null, "ok"], ["z-draft", null, "ok"], ], ); assert.deepEqual(reportRows([], [], new Map()), []); }); test("the three list edits: publish appends once, unpublish removes, move swaps and stops at the ends", () => { assert.deepEqual(withReportPublished(["a", "b"], "c"), ["a", "b", "c"]); assert.deepEqual(withReportPublished(["a", "b"], "a"), ["a", "b"]); assert.deepEqual(withReportUnpublished(["a", "b", "c"], "b"), ["a", "c"]); assert.deepEqual(withReportUnpublished(["a"], "x"), ["a"]); assert.deepEqual(withReportMoved(["a", "b", "c"], "b", -1), ["b", "a", "c"]); assert.deepEqual(withReportMoved(["a", "b", "c"], "b", 1), ["a", "c", "b"]); assert.deepEqual(withReportMoved(["a", "b", "c"], "a", -1), ["a", "b", "c"]); assert.deepEqual(withReportMoved(["a", "b", "c"], "c", 1), ["a", "b", "c"]); assert.deepEqual(withReportMoved(["a", "b"], "x", 1), ["a", "b"]); // Never the caller's array. const list = ["a", "b"]; withReportMoved(list, "a", 1); withReportPublished(list, "c"); assert.deepEqual(list, ["a", "b"]); }); test("the media summary: moments by kind, every byte the build copies, problems by kind", () => { const index: ReportMediaIndex = { format: "archilyzer-report-media", version: 1, siteId: "demo-site", preparedAt: "2026-10-05T12:00:00.000Z", moments: { "demo-channel/abc123/10.00-20.00": { kind: "video", file: "a.mp4", bytes: 1000, sha256: "x", width: 1280, height: 720, durationSec: 20, }, "demo-channel/abc123/30.00-40.00": { kind: "video", file: "b.mp4", bytes: 2000, sha256: "x", width: 1280, height: 720, durationSec: 20, }, "demo-channel/def456/5.00-9.00": { kind: "audio", file: "c.m4a", bytes: 300, sha256: "x", width: null, height: null, durationSec: 14, }, "demo-channel/1234567890": { kind: "post", file: "posts/demo-channel/1234567890/shot.png", bytes: 40, sha256: "x", width: 600, height: 400, durationSec: null, media: [{ file: "posts/demo-channel/1234567890/m1.jpg", bytes: 5, sha256: "y" }], }, }, problems: [ { kind: "missing-media", message: "no media", moment: "demo-channel/ghi789/1.00-2.00", citations: ["r#c03"] }, { kind: "invalid-report", message: "bad", report: "r", path: "id" }, { kind: "missing-media", message: "no media", moment: "demo-channel/ghi789/3.00-4.00" }, ], }; const s = reportMediaSummary(index); assert.equal(s.preparedAt, "2026-10-05T12:00:00.000Z"); assert.equal(s.moments, 4); assert.deepEqual(s.momentsByKind, [ { kind: "video", count: 2 }, { kind: "audio", count: 1 }, { kind: "post", count: 1 }, ]); assert.equal(s.totalBytes, 1000 + 2000 + 300 + 40 + 5); assert.deepEqual(s.problemsByKind, [ { kind: "missing-media", count: 2 }, { kind: "invalid-report", count: 1 }, ]); assert.equal(s.problems.length, 3); const empty = reportMediaSummary({ ...index, moments: {}, problems: [] }); assert.deepEqual([empty.moments, empty.totalBytes, empty.momentsByKind, empty.problemsByKind], [0, 0, [], []]); });