import { describe, expect, test } from "bun:test" import { markupParts, skeletonChange } from "./html-markup.mjs" import { applyEdits, POLISH_MARKUP_REASON, POLISH_REFERENCE_REASON, textOf } from "./polish-apply.mjs" import { diffPolish } from "./polish-diff.mjs" const LEDGER = { rows: [{ fact_id: "F1", quote: "Revenue may reach 320 m." }] } const FACT = '320 m' const BASE = `
Revenue may reach ${FACT} this year.
` // The public polish boundary must freeze bytes, not inferred HTML/CSS semantics. const MARKUP_CASES = [ ["hidden added", BASE, BASE.replace('#gone{display:none}${BASE}`, `${BASE.replace('id="a"', 'id="gone"')}`], ["class changed", BASE, BASE.replace('class="plain"', 'class="gone"')], ["style changed", BASE, BASE.replace("color:red", "display:none")], ["stylesheet inserted", BASE, `${BASE}`], ["self-closing slash added", BASE, BASE.replace('color:red">', 'color:red"/>')], ["wrapper added", BASE, BASE.replace(FACT, `${FACT}`)], ["wrapper removed", BASE.replace(FACT, `${FACT}`), BASE], ["comment added", BASE, `${BASE}`], ["comment changed", `${BASE}`, `${BASE}`], ["attribute reorder", BASE, BASE.replace('id="a" class="plain"', 'class="plain" id="a"')], ["attribute quote changed", BASE, BASE.replace('id="a"', "id='a'")], ["attribute name case changed", BASE, BASE.replace('id="a"', 'ID="a"')], ["duplicate attribute changed", BASE.replace('
Reference', 'Reference'], ["closing tag changed", BASE, BASE.replace("
", "")], ["tag renamed", "Results may improve.
", "${BASE}`, `${BASE}`], ["script content changed", `${BASE}`, `${BASE}`], ["style content changed", `${BASE}`, `${BASE}`], ["template text changed", `${BASE}note`, `${BASE}notes`], ["nested template text changed", `${BASE}xnote`, `${BASE}xnotes`], ["unclosed script content changed", `${BASE}foo();" describe("polish edits never carry markup characters", () => { test.each([ ["bogus comment before a fact span", DATA, "Revenue reached ", "Revenue reached "], ["bogus comment hiding a sentence", "
Fine. The board met in spring.
", "The board met in spring.", " The board met in spring.>"], ["empty end tag", "Results may improve steadily.
", "steadily", "steadily >"], ["introduced entity", "Results may improve steadily.
", "steadily", "steadily <b>"], ["introduced greater-than", "Results may improve steadily.
", "steadily", "steadily >"], ["removed entity", "R&D may grow steadily.
", "R&D", "RD"], ])("#given %s #then apply rejects it and diff blocks", (_label: string, html: string, from: string, to: string) => { const result = applyEdits(html, [{ before: from, after: to }], LEDGER) expect(result.applied).toEqual([]) expect(result.out).toBe(html) expect(result.rejected[0]?.reason).toContain("edit contains markup characters (<, >, &): route this section through the full fidelity gates") if (from === "R&D" || to.includes("<") || to === "steadily >") return expect(diffPolish(html, html.replace(from, to), LEDGER).find((item: { code: string }) => item.code === "polish_diff_fail")?.severity).toBe("blocker") }) test.each([ ["words around R&D", "Our R&D budget grew in 2025.
", "budget grew", "budget rose", true], ["an edit spanning R&D", "Our R&D budget grew in 2025.
", "Our R&D budget grew", "The R&D budget rose", false], ["words around p < 0.05", "The effect held at p < 0.05 in the trial.
", "in the trial", "across the trial", true], ["an edit spanning p < 0.05", "The effect held at p < 0.05 in the trial.
", "held at p < 0.05", "stayed at p < 0.05", false], ])("#given legitimate prose with %s #then only an edit leaving the entity bytes untouched applies", (_label: string, html: string, from: string, to: string, passes: boolean) => { const result = applyEdits(html, [{ before: from, after: to }], LEDGER) if (passes) { expect(result.out).toBe(html.replace(from, to)) expect(diffPolish(html, result.out, LEDGER)).toEqual([]) return } expect(result.out).toBe(html) expect(result.rejected[0]?.reason).toContain(POLISH_MARKUP_REASON) }) test("#given double-escaped script code #then the ambiguity refusal blocks a text-only edit of it", () => { const result = applyEdits(SCRIPT, [{ before: "foo();", after: "bar();" }], LEDGER) expect(result.applied).toEqual([]) expect(result.rejected[0]?.reason).toContain("ambiguous markup:c
"], ["comment opener inside style", "c
"], ["CDATA section", "a c
"], ["processing instruction", "a c
"], ["less-than inside an attribute value", 'c
'], ["comment closed by --!>", "a c
"], ])("#given a document with %s #then diff refuses it as ambiguous markup", (_label: string, html: string) => { const defect = diffPolish(html, html, LEDGER).find((item: { code: string }) => item.code === "polish_diff_fail") expect(defect?.message).toContain("ambiguous markup") }) test("#given an end tag open before a non-letter #then the tokenizer skips a bogus comment to the next >", () => { expect(markupParts("a b> c > d").markup).toEqual([" b>", ">"]) }) test("#given a double-escaped script #then the script runs to the close outside the escape", () => { expect(markupParts(`${SCRIPT}x
`).markup).toEqual(["", "
", "foo();", "", "
"]) expect(markupParts("x").markup).toEqual(["", "", ""]) }) }) // Review #11152 round 5: the primary rule is a whole-document skeleton comparison. Every markup // token and every character reference in text must be byte-identical; an edit's located region may // not touch a character reference either, so an edit can neither change nor create one. const AMPD = "We fund R&D labs here.
" const TOM = "Tom & Jerry ran.
" describe("polish keeps character references intact", () => { test.each(["style", "p"])("#given a quoted greater-than in a %s end tag #when its quote changes #then diff blocks", (tag: string) => { const before = `<${tag}>p{}${tag} a='>'> trailing prose` const after = before.replace(">'", ">x") expect(diffPolish(before, after, LEDGER).map((item: { code: string }) => item.code)).toContain("polish_diff_fail") }) test.each(["&.", `&${"a".repeat(24)}.`])("#given %s immediately before an edit #when extended #then the reference reason is actionable", (reference: string) => { const html = `${reference} next
` const result = applyEdits(html, [{ before: " next", after: "later" }], LEDGER) expect(result.rejected[0]?.reason).toContain(POLISH_REFERENCE_REASON) expect(result.out).toBe(html) expect(diffPolish(html, html.replace(" next", "later"), LEDGER).map((item: { code: string }) => item.code)).toContain("polish_diff_fail") }) test.each([ ["an edit starting inside an entity", AMPD, "amp;D labs", "lt;D labs"], ["an edit starting right after a bare ampersand", TOM, " Jerry ran", "lt; ran"], ])("#given %s #then apply refuses it and diff blocks the hand edit", (_label: string, html: string, from: string, to: string) => { const result = applyEdits(html, [{ before: from, after: to }], LEDGER) expect(result.applied).toEqual([]) expect(result.out).toBe(html) expect(result.rejected[0]?.reason).toContain("character reference") const defects = diffPolish(html, html.replace(from, to), LEDGER) expect(defects.find((item: { code: string }) => item.code === "polish_diff_fail")?.severity).toBe("blocker") }) test("#given a hand edit from R&D to R<D #then diff blocks it", () => { const defect = diffPolish(AMPD, AMPD.replace("&", "<"), LEDGER).find((item: { code: string }) => item.code === "polish_diff_fail") expect(defect?.message).toContain("skeleton changed") }) test.each([ ["an entity changed", AMPD, AMPD.replace("&", "<")], ["an entity created", TOM, "Tom < ran.
"], ["an entity removed", AMPD, AMPD.replace("R&D", "RD")], ["a tag changed", AMPD, AMPD.replace("", "
Results may improve.
').trim()).toBe("Results may improve.") }) })