// `umtool notes`: the agent's loop, end to end, against a temp SITES_DIR and // REPORTS_DIR -- read the digest, reply and resolve, reopen, list. // // Run with: pnpm test:scripts import assert from "node:assert/strict"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { notesCommand } from "./cli.mjs"; import { articleTarget, projectTarget, writeNote } from "./targets.mjs"; import { clearSourcesCache } from "../articles/sources.mjs"; const REPORT = { format: "archilyzer-report", version: 1, id: "polemic-x", kind: "sweep", title: "On X", summary: "She said **X** twice. Then she [denied it](cite:c1).", citations: { c1: { kind: "video", channel: "ch", id: "v1", start: 10, end: 20, quote: "I never said X", speaker: "Her" } }, sections: [{ id: "s1", title: "The first time", body: "In 2019 she said X on a podcast. Nobody noticed." }], }; async function fixture() { const root = await mkdtemp(path.join(tmpdir(), "umtool-notes-cli-")); const sites = path.join(root, "sites"); const reports = path.join(root, "reports"); await mkdir(path.join(sites, "priv", "reports", "polemic-x"), { recursive: true }); await writeFile(path.join(sites, "priv", "site.json"), "{}"); await writeFile(path.join(sites, "priv", "reports", "polemic-x", "report.json"), JSON.stringify(REPORT)); await mkdir(path.join(reports, "ws", "polemics", "drafts"), { recursive: true }); await writeFile(path.join(reports, "ws", "polemics", "drafts", "x.json"), JSON.stringify({ id: "polemic-x" })); await writeFile(path.join(reports, "ws", "polemics", "make-site.py"), 'SITE = "priv" # drafts\n'); const proj = path.join(reports, "ws", "polemic-x"); await mkdir(proj, { recursive: true }); await writeFile( path.join(proj, "video.manifest.json"), JSON.stringify({ schemaVersion: 1, slug: "polemic-x", generatedBy: "polemics/make-site.py", timeline: [{ type: "clip", id: "e1", channel: "ch", video: "v1", start: 10, end: 20, quote: "I never said X", onscreen: { title: "Denial" } }] }), ); clearSourcesCache(); return { root, sites, reports, opts: { sitesDir: sites, reportsRoot: reports } }; } async function run(args, opts) { const out = []; const code = await notesCommand(args, { ...opts, log: (s) => out.push(String(s)) }); return { code, text: out.join("\n") }; } test("the agent loop: digest, reply --resolve, reopen; every write is the agent's", async () => { const { root, sites, opts } = await fixture(); const t = await articleTarget("priv/polemic-x", opts); const a = await writeNote(t, { op: "add", text: "Too strong; say 'claimed'.", anchor: { kind: "text", section: "s1", quote: "she said X", prefix: "In 2019 ", suffix: " on a podcast" } }, { by: "operator" }); await writeNote(t, { op: "add", text: "Is this the right clip?", anchor: { kind: "cite", cite: "c1" } }, { by: "operator" }); const d = await run(["priv/polemic-x"], opts); assert.equal(d.code, 0); assert.match(d.text, /# Notes on priv\/polemic-x — On X/); assert.match(d.text, /edit `.*ws\/polemics\/drafts\/x\.json`; `report\.json` is regenerated by `.*make-site\.py`/); assert.match(d.text, /“she said X” in “The first time”/); assert.match(d.text, /> In 2019 she said X on a podcast\./); assert.match(d.text, /citation `c1` — ch\/v1@10-20 \(Her\)/); assert.match(d.text, /2 open, 0 closed/); const r = await run(["reply", a.note.id, "Changed to 'claimed' in drafts/x.json", "--resolve"], opts); assert.equal(r.code, 0, r.text); const doc = JSON.parse(await readFile(path.join(sites, "priv", "reports", "polemic-x", "notes.json"), "utf8")); const n = doc.notes.find((x) => x.id === a.note.id); assert.equal(n.status, "resolved"); assert.equal(n.resolvedBy, "agent"); assert.deepEqual(n.replies.map((x) => x.author), ["agent"]); assert.match((await run(["priv/polemic-x"], opts)).text, /1 open, 1 closed/); assert.match((await run(["priv/polemic-x", "--resolved"], opts)).text, /Changed to 'claimed'/); assert.equal((await run(["reopen", a.note.id], opts)).code, 0); const list = await run(["--all"], opts); assert.match(list.text, /priv\/polemic-x\s+article\s+2 open \/ 2/); // refusals exit 2 and write nothing assert.equal((await run(["reply", "n_nosuchnote"], opts)).code, 2); assert.equal((await run(["reply", a.note.id], opts)).code, 2); assert.equal((await run(["priv/../etc"], opts)).code, 2); await rm(root, { recursive: true }); }); test("a video project's digest names the generator and lists the takes' verdicts", async () => { const { root, reports, opts } = await fixture(); const proj = path.join(reports, "ws", "polemic-x"); await mkdir(path.join(proj, "takes", "deck"), { recursive: true }); await writeFile(path.join(proj, "takes", "deck", "take.json"), JSON.stringify({ id: "deck", group: "open", order: 1, label: "Deck first", kind: "similar", preview: "preview.mp4" })); await writeFile(path.join(proj, "takes", "verdicts.json"), JSON.stringify({ deck: { verdict: "like", note: "keep the beat", at: "x" } })); const t = await projectTarget("ws/polemic-x", opts); await writeNote(t, { op: "add", text: "Cut this.", anchor: { kind: "entry", entry: "e1" } }, { by: "operator" }); await writeNote(t, { op: "add", text: "quote changed", anchor: { kind: "edit", entry: "e1", field: "quote", from: "a", to: "b" } }, { by: "operator" }); const d = await run(["ws/polemic-x"], opts); assert.equal(d.code, 0, d.text); assert.match(d.text, /video\.manifest\.json` is regenerated by `.*ws\/polemics\/make-site\.py`/); assert.match(d.text, /timeline entry `e1` \(clip\) “Denial”/); assert.match(d.text, /`e1\.quote`: "a" → "b"/); assert.match(d.text, /- `deck` “Deck first” \(open, similar\): like — keep the beat/); await rm(root, { recursive: true }); });