import { describe, expect, onTestFinished, test } from "bun:test" import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" import { readLedger } from "./fact-ledger.mjs" import { run } from "./report-tools.mjs" const BRIEF = readFileSync(join(import.meta.dir, "tests/fixtures/fidelity-brief-locked.md"), "utf8") type Row = { fact_id: string; value: string; unit: string; quote: string; raw: string; sentence_id: string; locator: { artifact: string; unit: string; char_start: number; char_end: number } } type Defect = { code: string; message: string } /** {{F1}} -> {{F1:}}: fill requires the label. */ const labeled = (dir: string) => { const labels = new Map(readLedger(dir).rows.map((row: { fact_id: string; label: string }) => [row.fact_id, row.label])) return (placeholder: string) => placeholder.replace(/\{\{(\w+)\}\}/, (_, id) => `{{${id}:${labels.get(id)}}}`) } function session() { const dir = mkdtempSync(join(tmpdir(), "page-source-")) onTestFinished(() => rmSync(dir, { recursive: true, force: true })) writeFileSync(join(dir, "brief.md"), BRIEF) return dir } async function snapshotPage(dir: string, bytes: Buffer | string, id = "S1") { const file = join(dir, `${id}.capture.html`) writeFileSync(file, bytes) const result = await run(["source", "snapshot", "--id", id, "--html", file, "--session-dir", dir]) expect(result.exitCode).toBe(0) return Buffer.from(bytes) } const find = (dir: string, anchor: string, id = "S1") => run(["fact", "add", "--session-dir", dir, "--source", id, "--find", anchor, "--json"]) const quote = (dir: string, match: string) => run(["fact", "quote", "--session-dir", dir, "--source", "S1", "--match", match, "--json"]) /** Fill a report citing every given placeholder, then verify it; returns the verify result and the filled html. */ async function fillAndVerify(dir: string, placeholders: string[]) { const body = [ 'Bridge

Bridge

', "

Findings

", ...placeholders.map(labeled(dir)).map((placeholder) => `

The source states ${placeholder} [S1].

`), "

How this report was made

Collected from one cited public page [S1].

", '

Sources

  1. [S1] Bridge page, retrieved 2026-10-01.
', ].join("\n") writeFileSync(join(dir, "report.src.html"), body) const filled = await run(["fact", "fill", join(dir, "report.src.html"), "--out", join(dir, "report.html"), "--session-dir", dir]) expect(filled.exitCode).toBe(0) const verified = await run(["fact", "verify", join(dir, "report.html"), "--session-dir", dir]) return { verified, html: readFileSync(join(dir, "report.html"), "utf8") } } const codes = (json: { defects: Defect[] }) => json.defects.map((defect) => defect.code) const PAGE = [ "Deck 9 m", '

The main deck spans 320 m in total.

', '

The upper deck spans 330 m.

', "", ].join("") describe("page sources locate on visible text nodes only", () => { test("#given numbers in title, data and style attributes #when fact add --find runs #then the text-node value is recorded and verifies", async () => { const dir = session() const raw = await snapshotPage(dir, PAGE) const main = await find(dir, "main deck spans") const upper = await find(dir, "upper deck spans") expect(main.exitCode).toBe(0) const row: Row = main.json.data.row expect([row.value, row.unit, row.quote]).toEqual(["320", "m", "The main deck spans 320 m in total."]) expect(row.locator).toMatchObject({ artifact: "raw.bin", unit: "bytes" }) const span = raw.subarray(row.locator.char_start, row.locator.char_end).toString("utf8") expect(span).toBe('The main deck spans 320 m in total.') expect([upper.json.data.row.value, upper.json.data.row.unit]).toEqual(["330", "m"]) const { verified, html } = await fillAndVerify(dir, ["{{F1}}", "{{F2}}"]) expect(verified.exitCode).toBe(0) expect(html).toContain("320 m") expect(html).not.toContain(">9 m<") }) test("#given the same anchor in a hidden element and a visible paragraph #when --find runs #then only the visible number is located", async () => { const dir = session() const hidden = [ '

Visible length is 999 m.

', '

Shadow length is 998 m.

', '', "", "", "", ].join("") await snapshotPage(dir, `${hidden}

Visible length is 410 m.

`) const visible = await find(dir, "Visible length is") expect(visible.exitCode).toBe(0) expect(visible.json.data.row.value).toBe("410") for (const anchor of ["Shadow length is", "Ghost length is", "Secret length is", "Script length is", "Template length is", "Inline length is"]) { const result = await find(dir, anchor) expect([anchor, result.json.error]).toEqual([anchor, "fact_not_located"]) } expect(readFileSync(join(dir, "sources/S1/text.txt"), "utf8")).toBe("Visible length is 410 m.") }) test("#given elements hidden by " const body = '

Class length is 999 m.

Id length is 998 m.

Kept length is 410 m.

' await snapshotPage(dir, `${style}${body}`) for (const anchor of ["Class length is", "Id length is", "Tag length is"]) expect([anchor, (await find(dir, anchor)).json.error]).toEqual([anchor, "fact_not_located"]) expect((await find(dir, "Kept length is")).json.data.row.value).toBe("410") }) test("#given rules inside @media (a responsive or a print copy) #when --find runs #then the text is located, and an unconditional rule still hides", async () => { const dir = session() const style = "" const body = '

Desktop length is 410 m.

Screen length is 411 m.

Mobile length is 412 m.

' await snapshotPage(dir, `${style}${body}`) const desktop = await find(dir, "Desktop length is") expect(desktop.exitCode).toBe(0) expect(desktop.json.data.row.value).toBe("410") expect((await find(dir, "Screen length is")).json.data.row.value).toBe("411") expect((await find(dir, "Mobile length is")).json.error).toBe("fact_not_located") }) test("#given a quote across inline markup #when fact quote --match runs #then the quotation is that sentence, indexed like sentences.jsonl", async () => { const dir = session() await snapshotPage(dir, "

Construction opens no earlier than 2027. Funding is pending.

") const result = await quote(dir, "opens no earlier than 2027") expect(result.exitCode).toBe(0) const row: Row = result.json.data.row expect(row.quote).toBe("Construction opens no earlier than 2027.") const sentences = readFileSync(join(dir, "sources/S1/sentences.jsonl"), "utf8").trim().split("\n").map((line) => JSON.parse(line)) expect(sentences.find((entry: { sid: string }) => entry.sid === row.sentence_id)?.text).toBe(row.quote) const { verified, html } = await fillAndVerify(dir, ["{{Q1}}"]) expect(verified.exitCode).toBe(0) expect(html).toContain("Construction opens no earlier than 2027.") expect(html).not.toContain("<") }) test("#given a table cell without #when --table runs #then the quote is the cell text, not the rest of the document", async () => { const dir = session() const raw = await snapshotPage(dir, '
NameLength
Deck320 m

Appendix 9 holds notes.

') const result = await run(["fact", "add", "--session-dir", dir, "--source", "S1", "--table", "sizes", "--row", "Deck", "--col", "Length", "--json"]) expect(result.exitCode).toBe(0) const row: Row = result.json.data.row expect([row.value, row.unit, row.quote]).toEqual(["320", "m", "320 m"]) expect(raw.subarray(row.locator.char_start, row.locator.char_end).toString()).toBe('320 m') expect((await fillAndVerify(dir, ["{{F1}}"])).verified.exitCode).toBe(0) }) }) describe("byte offsets count the source's own encoding", () => { const latin1 = (text: string) => Buffer.from(text, "latin1") test("#given a declared ISO-8859-1 page with Caf\\xE9 before the value #when added and verified unchanged #then offsets hit the real bytes and verify is clean", async () => { const dir = session() const raw = await snapshotPage(dir, latin1('

Caf\u00e9. The span is 320 m.

')) const result = await find(dir, "The span is") const row: Row = result.json.data.row expect(raw.subarray(row.locator.char_start, row.locator.char_end).toString("latin1")).toBe("The span is 320 m.") const { verified } = await fillAndVerify(dir, ["{{F1}}"]) expect(verified.exitCode).toBe(0) expect(codes(verified.json)).not.toContain("relocated") }) test("#given an undeclared Latin-1 byte with more than 32 characters before the value #when verified unchanged #then no false relocation", async () => { const dir = session() await snapshotPage(dir, latin1("

Caf\u00e9 au lait is served here all day long. The span is 320 m.

")) await find(dir, "The span is") const { verified } = await fillAndVerify(dir, ["{{F1}}"]) expect(verified.exitCode).toBe(0) expect(codes(verified.json)).toEqual([]) }) test("#given an EUC-KR page #when added and verified unchanged #then the locator spans the EUC-KR bytes and verify is clean", async () => { const dir = session() const bytes = Buffer.from("3c68746d6c3e3c686561643e3c6d65746120636861727365743d226575632d6b72223e3c2f686561643e3c626f64793e3c703ec1d620b1b3b7aec0c720b1e6c0ccb4c220333230206d20c0ccb4d92e3c2f703e3c2f626f64793e3c2f68746d6c3e", "hex") const raw = await snapshotPage(dir, bytes) const result = await find(dir, "\uae38\uc774\ub294") expect(result.exitCode).toBe(0) const row: Row = result.json.data.row expect(row.value).toBe("320") expect(raw.subarray(row.locator.char_start, row.locator.char_end).toString("hex")).toBe("c1d620b1b3b7aec0c720b1e6c0ccb4c220333230206d20c0ccb4d92e") const { verified } = await fillAndVerify(dir, ["{{F1}}"]) expect(verified.exitCode).toBe(0) expect(codes(verified.json)).toEqual([]) }) })