// The converters over files: the disk callbacks (cues behind the text guard, // a post's channel and record off the posts archive) and the two CLI mains, // on a temp channels tree. import { test } from "node:test"; import assert from "node:assert/strict"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { convertMain, toManifestMain } from "./reports-convert"; import { diskCuesOf, diskPostChannelOf, diskPostOf } from "../lib/report/convert-server"; import { resolveCommand } from "./_cli"; import { COMMANDS } from "./archilyzer"; const ORIGIN = "https://archive.example"; function tree() { const root = mkdtempSync(path.join(tmpdir(), "reports-convert-")); const channels = path.join(root, "channels"); const rec = path.join(channels, "demo-channel", "data", "abc123"); mkdirSync(rec, { recursive: true }); writeFileSync( path.join(rec, "transcript.cues.json"), JSON.stringify({ cues: [ { start: 60.1, end: 62.0, text: "and then the host says" }, { start: 58.2, end: 60.1, text: "This is where it starts." }, { start: 62.4, end: 64.9, text: "we never said that." }, ], }), ); const social = path.join(channels, "demo-social"); mkdirSync(path.join(social, "posts"), { recursive: true }); writeFileSync(path.join(social, "posts-archive"), "twitter 987654321\n"); writeFileSync( path.join(social, "posts", "2024-05.jsonl"), JSON.stringify({ id: "987654321", channelSlug: "demo-social", author: "demo", authorName: "Demo Account", createdAt: "2024-05-01T12:00:00.000Z", text: "a platform post", url: "https://x.com/demo/status/987654321", platform: "twitter", }) + "\n", ); // A second channel holding the same post id: the handle picks. const mirror = path.join(channels, "other-mirror"); mkdirSync(mirror, { recursive: true }); writeFileSync(path.join(mirror, "posts-archive"), "twitter 987654321\n"); return { root, channels, done: () => rmSync(root, { recursive: true, force: true }) }; } const quiet = () => { const lines = { log: [] as string[], error: [] as string[] }; return { lines, out: { log: (s: string) => lines.log.push(s), error: (s: string) => lines.error.push(s) } }; }; test("diskCuesOf reads a record's cues sorted; a missing record or an unsafe id is null", async () => { const t = tree(); try { const cuesOf = diskCuesOf(t.channels); const cues = await cuesOf("demo-channel", "abc123"); assert.deepEqual(cues?.map((c) => c.start), [58.2, 60.1, 62.4]); assert.equal(await cuesOf("demo-channel", "nope"), null); assert.equal(await cuesOf("demo-channel", ".."), null); } finally { t.done(); } }); test("diskCuesOf: a channel whose text is not readable is a warning, not a read", async () => { const t = tree(); try { const legacy = path.join(t.channels, "legacy-channel"); mkdirSync(legacy, { recursive: true }); writeFileSync(path.join(legacy, "config.json"), JSON.stringify({ dataDir: "/elsewhere/legacy-channel/data" })); const warnings: string[] = []; assert.equal(await diskCuesOf(t.channels, warnings)("legacy-channel", "abc123"), null); assert.equal(warnings.length, 1); assert.match(warnings[0], /^legacy-channel: /); } finally { t.done(); } }); test("diskPostChannelOf finds the channel that keeps a post, the handle breaking a tie; diskPostOf reads its record", async () => { const t = tree(); try { const channelOf = diskPostChannelOf(t.channels); assert.equal(await channelOf({ platform: "x", id: "987654321", handle: "demo" }), "demo-social"); assert.equal(await channelOf({ platform: "x", id: "987654321", handle: "other" }), "other-mirror"); assert.equal(await channelOf({ platform: "x", id: "1" }), null); const postOf = diskPostOf(t.channels); assert.deepEqual(await postOf("demo-social", "987654321"), { platform: "x", url: "https://x.com/demo/status/987654321", createdAt: "2024-05-01T12:00:00.000Z", author: "demo", authorName: "Demo Account", }); assert.equal(await postOf("demo-social", "2"), null); } finally { t.done(); } }); test("reports convert sweep: widened from the cues, the platform post found, written", async () => { const t = tree(); try { const input = path.join(t.root, "sweep.md"); writeFileSync( input, [ "# A demo sweep", "", "## Findings", "", `- "we never said that" — [Demo @ 1:02](${ORIGIN}/?v=demo-channel%2Fabc123&t=62)`, `- "a platform post" [post by demo](https://x.com/demo/status/987654321)`, ].join("\n"), ); const out = path.join(t.root, "reports", "demo-sweep", "report.json"); const { lines, out: o } = quiet(); assert.equal(await convertMain({ from: "sweep", input, out, channelsDir: t.channels }, o), 0); const report = JSON.parse(readFileSync(out, "utf8")); assert.equal(report.id, "a-demo-sweep"); assert.deepEqual([report.citations.c01.start, report.citations.c01.end], [60.1, 64.9]); assert.equal(report.citations.p01.channel, "demo-social"); assert.deepEqual(lines.error, []); assert.match(lines.log[0], /report a-demo-sweep \(sweep\) — 1 section\(s\), 2 claim\(s\), 2 citation\(s\)/); } finally { t.done(); } }); test("reports convert refuses to write a report with problems; an unreadable input is exit 2", async () => { const t = tree(); try { const input = path.join(t.root, "answer.json"); writeFileSync(input, JSON.stringify({ answer: "Said [1 @ 0:05].", sources: [{ key: "demo-channel/abc123", snippets: [] }] })); const out = path.join(t.root, "out.json"); const { lines, out: o } = quiet(); assert.equal(await convertMain({ from: "ask", input, out, id: "Not A Slug" }, o), 1); assert.equal(existsSync(out), false); assert.ok(lines.error.some((l) => l.startsWith(" id: must be a lowercase slug"))); assert.equal(await convertMain({ from: "ask", input: path.join(t.root, "missing.json"), out }, quiet().out), 2); writeFileSync(input, JSON.stringify({ unrelated: true })); assert.equal(await convertMain({ from: "ask", input, out }, quiet().out), 2); } finally { t.done(); } }); test("reports to-manifest: a starter manifest, its posts read off the channels tree", async () => { const t = tree(); try { const input = path.join(t.root, "report.json"); writeFileSync( input, JSON.stringify({ format: "archilyzer-report", version: 1, id: "demo-report", kind: "sweep", title: "A demo report", citations: { c01: { kind: "video", channel: "demo-channel", id: "abc123", start: 60.1, end: 64.9, quote: "we never said that." }, p01: { kind: "post", channel: "demo-social", id: "987654321", quote: "a platform post" }, }, sections: [{ id: "s1", title: "Findings", claims: [{ id: "k1", text: "It was said.", citations: ["c01", "p01"] }] }], }), ); const out = path.join(t.root, "video.manifest.json"); const { lines, out: o } = quiet(); assert.equal(await toManifestMain({ input, out, channelsDir: t.channels, siteOrigin: ORIGIN }, o), 0); const m = JSON.parse(readFileSync(out, "utf8")); assert.deepEqual( m.timeline.map((e: { type: string; id: string }) => `${e.type}:${e.id}`), ["card:title", "card:s1", "clip:c01"], ); assert.deepEqual(m.posts, [ { id: "p01", platform: "x", author: "Demo Account", handle: "demo", date: "2024-05-01T12:00:00.000Z", text: "a platform post", url: "https://x.com/demo/status/987654321", attachTo: "c01", siteChannel: "demo-social", postId: "987654321", }, ]); assert.equal(m.provenance.siteOrigin, ORIGIN); assert.deepEqual(lines.error, []); // An invalid report is refused, and nothing is written. writeFileSync(input, JSON.stringify({ format: "archilyzer-report" })); rmSync(out); assert.equal(await toManifestMain({ input, out }, quiet().out), 1); assert.equal(existsSync(out), false); } finally { t.done(); } }); test("the archilyzer table routes reports convert and reports to-manifest", () => { assert.deepEqual(resolveCommand(COMMANDS, ["reports", "convert", "sweep", "in.md"])?.command.path, ["reports", "convert"]); assert.deepEqual(resolveCommand(COMMANDS, ["reports", "to-manifest", "r.json"])?.command.path, ["reports", "to-manifest"]); });