import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import type { Report } from "yt-dlp-transcript-common/lib/report/schema"; import { REPORT_INDEX_FORMAT, REPORT_VIEWS_VERSION, buildReportPageView, reportIndexEntry, } from "yt-dlp-transcript-common/lib/report/views"; import { CONTRACT } from "yt-dlp-transcript-common/lib/archive/contract"; import { LocalSource, type ShardSource } from "./source"; import { createServer } from "./server"; import { renderReportIndex, renderReportPage, reportsLine } from "./reports"; // list_reports / get_report and the "cited-only site" line, over a composed // public dir on disk read by the real LocalSource. const ORIGIN = "https://reports.example"; const REPORT: Report = { format: "archilyzer-report", version: 1, id: "demo-report", kind: "factcheck", title: "A demo fact-check", subtitle: "Of an article", summary: "It starts [here](cite:v1).", published: "2026-10-01", subject: { source: "s0" }, sources: { s0: { kind: "article", title: "An article", url: "https://example.org/a", publisher: "Example" }, }, citations: { v1: { kind: "video", channel: "demo-channel", id: "abc123", start: 61, end: 75.5, quote: "the cited words" }, p1: { kind: "post", channel: "demo-social", id: "123", quote: "a posted line" }, s1: { kind: "source", source: "s0", quote: "the article's sentence" }, }, sections: [ { id: "one", title: "Section one", claims: [ { id: "c1", text: "The article claims a thing.", verdict: "CONTRADICTED", sourceQuote: { citation: "s1" }, findings: "The recording says otherwise [here](cite:v1).", citations: ["p1"], }, ], }, { id: "two", title: "Section two", body: "Nothing to check.", claims: [] }, ], }; const VIEW = buildReportPageView(REPORT, { record: (c) => ({ channel: c.channel, channelTitle: "Demo Channel", id: c.id, title: `Recording ${c.id}`, date: "2026-01-02", originalUrl: c.kind === "post" ? `https://social.example/${c.id}` : `https://video.example/watch?v=${c.id}&t=61`, }), post: () => ({ author: "@demo", text: "a posted line, and more" }), }); const INDEX = { format: REPORT_INDEX_FORMAT, version: REPORT_VIEWS_VERSION, reports: [reportIndexEntry(VIEW)], }; async function writeSite(opts: { cited: boolean; reports: boolean }): Promise { const dir = await mkdtemp(path.join(os.tmpdir(), "mcp-reports-")); const files: Record = { "corpus.json": { spec: CONTRACT.corpusSpec, kind: "site", site: { id: "demo-site", title: "Demo", url: ORIGIN, ...(opts.cited ? { scope: "cited", audience: "public" } : {}), }, channels: opts.cited ? [] : [{ slug: "demo-channel", name: "Demo Channel", videoCount: 1 }], ...(opts.reports ? { reports: { index: `${ORIGIN}/reports/index.json`, count: 1 } } : {}), }, }; if (opts.reports) { files["reports/index.json"] = INDEX; files["reports/demo-report/page.json"] = VIEW; } for (const [rel, body] of Object.entries(files)) { const file = path.join(dir, rel); await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, JSON.stringify(body)); } return dir; } async function connect(source: ShardSource): Promise { const server = createServer(source); const [ct, st] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "test", version: "0" }, { capabilities: {} }); await Promise.all([server.connect(st), client.connect(ct)]); return client; } async function call(client: Client, name: string, args: Record = {}) { const res = (await client.callTool({ name, arguments: args })) as { content: { text: string }[]; isError?: boolean; }; return { text: res.content.map((c) => c.text).join("\n"), isError: res.isError === true }; } test("a cited site: discovery tools say cited-only with its report count, not an empty corpus", async () => { const dir = await writeSite({ cited: true, reports: true }); const client = await connect(new LocalSource(dir)); try { const channels = await call(client, "list_channels"); assert.equal(channels.isError, false); assert.match(channels.text, /cited-only site: 1 report\(s\)/); assert.doesNotMatch(channels.text, /No channels found/); assert.match((await call(client, "list_sources")).text, /reports: cited-only site: 1 report\(s\)/); assert.match((await call(client, "resolve_source", { source: "default" })).text, /reports: cited-only site/); } finally { await client.close(); await rm(dir, { recursive: true, force: true }); } }); test("list_reports names each report with its counts, tally and page", async () => { const dir = await writeSite({ cited: true, reports: true }); const client = await connect(new LocalSource(dir)); try { const out = (await call(client, "list_reports")).text; assert.match(out, /1 report\(s\) in local:.*cited-only site/); assert.match(out, /- demo-report · A demo fact-check — Of an article/); assert.match(out, /factcheck · 1 claim\(s\) · 3 citation\(s\) · 2026-10-01/); assert.match(out, /verdicts: Contradicted 1/); assert.match(out, new RegExp(`${ORIGIN}/reports/demo-report/`)); } finally { await client.close(); await rm(dir, { recursive: true, force: true }); } }); test("get_report: claims with their citations' quote, original and moment URLs", async () => { const dir = await writeSite({ cited: true, reports: true }); const client = await connect(new LocalSource(dir)); try { const { text, isError } = await call(client, "get_report", { report: "demo-report" }); assert.equal(isError, false); assert.match(text, /# A demo fact-check/); assert.match(text, /verdicts: Contradicted 1/); assert.match(text, /under review: An article \(Example\) https:\/\/example\.org\/a/); assert.match(text, /\[Contradicted\] The article claims a thing\. \(#c1\)/); assert.match(text, /findings: The recording says otherwise/); // The source sentence, the span the findings cite, then the claim's post. const at = (s: string) => text.indexOf(s); assert.ok(at('"the article\'s sentence"') < at('"the cited words"')); assert.ok(at('"the cited words"') < at('"a posted line"')); assert.match(text, /video Demo Channel · Recording abc123 · 2026-01-02 @ 61–75 s/); assert.match(text, /original: https:\/\/video\.example\/watch\?v=abc123&t=61/); assert.match(text, new RegExp(`moment: ${ORIGIN}/m/demo-channel/abc123/61\\.00-75\\.50/`)); assert.match(text, /original: https:\/\/social\.example\/123/); assert.match(text, new RegExp(`moment: ${ORIGIN}/m/demo-social/123/`)); assert.match(text, new RegExp(`page: ${ORIGIN}/reports/demo-report/`)); assert.match(text, /## Section two \(#two\)/); const one = (await call(client, "get_report", { report: "demo-report", section: "two" })).text; assert.match(one, /## Section two/); assert.doesNotMatch(one, /Section one/); const badSection = await call(client, "get_report", { report: "demo-report", section: "nope" }); assert.equal(badSection.isError, true); assert.match(badSection.text, /no section nope — its sections: one, two/); const missing = await call(client, "get_report", { report: "nope" }); assert.equal(missing.isError, true); assert.match(missing.text, /report not found: nope — this source publishes: demo-report/); } finally { await client.close(); await rm(dir, { recursive: true, force: true }); } }); test("a full site: channels as before, and its reports are counted where it has any", async () => { const withReports = await writeSite({ cited: false, reports: true }); const without = await writeSite({ cited: false, reports: false }); const a = await connect(new LocalSource(withReports)); const b = await connect(new LocalSource(without)); try { const channels = (await call(a, "list_channels")).text; assert.match(channels, /1 channel\(s\)/); assert.match((await call(a, "list_sources")).text, /reports: 1 report\(s\) published/); assert.doesNotMatch((await call(b, "list_sources")).text, /reports:/); assert.match((await call(b, "list_reports")).text, /publishes no reports/); const missing = await call(b, "get_report", { report: "demo-report" }); assert.equal(missing.isError, true); assert.match(missing.text, /this source publishes no reports/); } finally { await a.close(); await b.close(); await rm(withReports, { recursive: true, force: true }); await rm(without, { recursive: true, force: true }); } }); test("a source with no reports of its own (a hub) points at its members", () => { const out = renderReportIndex("hub:https://hub.example", { supported: false, scope: "full", reports: [] }, null); assert.match(out, /reports are per site/); assert.equal(reportsLine({ supported: false, scope: "full", reports: [] }), null); }); test("get_report's text: the timeline newest first, dated, before the sections; `section` may name an entry", () => { const view = buildReportPageView( { ...REPORT, entries: [ { id: "e-old", date: "2026-10-02", title: "The first update", body: "It began [here](cite:v1)." }, { id: "e-new", date: "2026-10-05T09:30:00Z", updated: "2026-10-06", title: "The second update", body: "Then more." }, ], }, { record: (c) => ({ channel: c.channel, id: c.id, title: `Record ${c.id}` }) }, ); const text = renderReportPage(view, ORIGIN); const at = (s: string) => text.indexOf(s); assert.ok(at("## Timeline (newest first)") > 0); assert.ok(at("### 2026-10-05T09:30:00Z (updated 2026-10-06) — The second update (#e-new)") > at("## Timeline")); assert.ok(at("### 2026-10-02 — The first update (#e-old)") > at("(#e-new)")); assert.ok(at("## Section one (#one)") > at("(#e-old)")); // An entry's citations, as a section body's. assert.match(text, /\(#e-old\)\nIt began \[here\]\(cite:v1\)\.\n\[1\] video/); // The header's update is the newest entry's. assert.match(text, /updated 2026-10-06/); assert.match(text, /\(sections: one, two; timeline entries: e-new, e-old — pass section:"" for one\)$/); const one = renderReportPage(view, ORIGIN, "e-old"); assert.match(one, /### 2026-10-02 — The first update \(#e-old\)/); assert.doesNotMatch(one, /e-new|Section one/); const section = renderReportPage(view, ORIGIN, "one"); assert.doesNotMatch(section, /Timeline/); });