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('

Reference', 'Reference'], ["closing tag changed", BASE, BASE.replace("

", "")], ["tag renamed", "

Results may improve.

", "
Results may improve.
"], ["tag whitespace changed", BASE, BASE.replace('

${BASE}`, `${BASE}`], ["script content changed", `${BASE}`, `${BASE}`], ["style content changed", `${BASE}`, `${BASE}`], ["template text changed", `${BASE}`, `${BASE}`], ["nested template text changed", `${BASE}`, `${BASE}`], ["unclosed script content changed", `${BASE}" describe("polish edits never carry markup characters", () => { test.each([ ["bogus comment before a fact span", DATA, "Revenue reached ", "Revenue reached Fine. 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 c d").markup).toEqual(["", ""]) }) test("#given a double-escaped script #then the script runs to the close outside the escape", () => { expect(markupParts(`${SCRIPT}

x

`).markup).toEqual(["

", "

", "", "

", "

"]) 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{}'> 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("

", "

")], ])("#given %s #then the skeleton check alone names the first differing token", (_label: string, before: string, after: string) => { expect(skeletonChange(before, after)).toContain("skeleton changed at token") }) test.each([ ["words beside an entity", AMPD, AMPD.replace("labs here", "labs nearby")], ["words beside a bare ampersand", TOM, TOM.replace("ran", "walked")], ["an unchanged document", AMPD, AMPD], ])("#given %s #then the skeleton is unchanged", (_label: string, before: string, after: string) => { expect(skeletonChange(before, after)).toBeNull() }) test("#given a quoted greater-than in an attribute #then the text projection follows the scanner", () => { expect(textOf('

Results may improve.

').trim()).toBe("Results may improve.") }) })