/** * TASK-0123 — word-level diff (deterministic, invariant-checked). * * Invariants pinned here (the UI + E2E depend on them): * reconstructOld(segments) === oldText * reconstructNew(segments) === newText * plus determinism (same input → same output) and minimality on the * canonical word-substitution case. */ import { describe, expect, it } from "vitest"; import { reconstructNew, reconstructOld, wordDiff, type DiffSegment } from "./diff.js"; const both = (oldText: string, newText: string) => { const segs = wordDiff(oldText, newText); expect(reconstructOld(segs)).toBe(oldText); expect(reconstructNew(segs)).toBe(newText); return segs; }; describe("wordDiff (TASK-0123)", () => { it("identical texts → single same segment (or [] when empty)", () => { expect(wordDiff("same text", "same text")).toEqual([{ type: "same", text: "same text" }]); expect(wordDiff("", "")).toEqual([]); }); it("empty old → all add; empty new → all del", () => { both("", "brand new text").forEach((s) => expect(s.type).toBe("add")); expect(wordDiff("gone now", "")).toEqual([{ type: "del", text: "gone now" }]); }); it("single word substitution: same del add in minimal form", () => { const segs = both("Hello old world", "Hello new world"); // whitespace rides with its word: del "old " / add "new " expect(segs).toEqual([ { type: "same", text: "Hello " }, { type: "del", text: "old " }, { type: "add", text: "new " }, { type: "same", text: "world" }, ]); }); it("prefix + suffix preserved; middle replaced", () => { const segs = both("the quick brown fox", "the fast silver fox"); const oldRecon = reconstructOld(segs); const newRecon = reconstructNew(segs); expect(oldRecon).toBe("the quick brown fox"); expect(newRecon).toBe("the fast silver fox"); // minimal: "the " is unchanged; the trailing "fox" keeps no whitespace // (the space before it belongs to the added "silver ") expect(segs[0]).toEqual({ type: "same", text: "the " }); expect(segs.at(-1)).toEqual({ type: "same", text: "fox" }); expect(segs.map((s) => s.type)).toEqual(["same", "del", "add", "same"]); }); it("pure insertion", () => { const segs = both("a c", "a b c"); expect(segs).toEqual([ { type: "same", text: "a " }, { type: "add", text: "b " }, { type: "same", text: "c" }, ]); }); it("pure deletion", () => { const segs = both("a b c", "a c"); expect(segs).toEqual([ { type: "same", text: "a " }, { type: "del", text: "b " }, { type: "same", text: "c" }, ]); }); it("whitespace differences are themselves diffed (invariants hold)", () => { both("a b", "a b").forEach(() => {}); const segs = wordDiff("a b", "a b"); expect(reconstructOld(segs)).toBe("a b"); expect(reconstructNew(segs)).toBe("a b"); // the extra space must appear in the script (as del), not vanish expect(segs.some((s) => s.type === "del")).toBe(true); }); it("multi-line text diffs line-wise-ish (word-level, invariants hold)", () => { const oldText = "line one stays\nline two changes here\nline three stays"; const newText = "line one stays\nline two changes there\nline three stays"; both(oldText, newText); }); it("is deterministic (same input → byte-identical output)", () => { const a = wordDiff("the cat sat on the mat", "the dog sat on the rug"); const b = wordDiff("the cat sat on the mat", "the dog sat on the rug"); expect(a).toEqual(b); expect(JSON.stringify(a)).toBe(JSON.stringify(b)); }); it("long texts (> 2000 words) degrade to full-replace, invariants hold", () => { const oldText = Array.from({ length: 2500 }, (_, i) => `word${i}`).join(" "); const newText = Array.from({ length: 2500 }, (_, i) => `word${i}-x`).join(" "); const segs = wordDiff(oldText, newText); expect(segs).toEqual([ { type: "del", text: oldText }, { type: "add", text: newText }, ]); }); it("realistic AI rewrite (the E2E 'Rewritten:' prefix) produces a meaningful diff", () => { const oldText = "Ship the release on Friday after the freeze lifts."; const newText = "Rewritten: Ship the release on Friday after the freeze lifts."; const segs = wordDiff(oldText, newText); // the only change is the prefix expect(segs[0]).toEqual({ type: "add", text: "Rewritten: " }); expect(segs[1]).toEqual({ type: "same", text: oldText }); }); it("consecutive same-type steps merge into one segment", () => { const segs = both("alpha beta gamma", "alpha X Y gamma"); // beta→X, gamma kept: del "beta " add "X Y " expect(segs.map((s: DiffSegment) => s.type)).toEqual(["same", "del", "add", "same"]); }); });