1
10import { describe, expect, it } from "vitest";
11
12import { reconstructNew, reconstructOld, wordDiff, type DiffSegment } from "./diff.js";
13
14const both = (oldText: string, newText: string) => {
15 const segs = wordDiff(oldText, newText);
16 expect(reconstructOld(segs)).toBe(oldText);
17 expect(reconstructNew(segs)).toBe(newText);
18 return segs;
19};
20
21describe("wordDiff (TASK-0123)", () => {
22 it("identical texts → single same segment (or [] when empty)", () => {
23 expect(wordDiff("same text", "same text")).toEqual([{ type: "same", text: "same text" }]);
24 expect(wordDiff("", "")).toEqual([]);
25 });
26
27 it("empty old → all add; empty new → all del", () => {
28 both("", "brand new text").forEach((s) => expect(s.type).toBe("add"));
29 expect(wordDiff("gone now", "")).toEqual([{ type: "del", text: "gone now" }]);
30 });
31
32 it("single word substitution: same del add in minimal form", () => {
33 const segs = both("Hello old world", "Hello new world");
34
35 expect(segs).toEqual([
36 { type: "same", text: "Hello " },
37 { type: "del", text: "old " },
38 { type: "add", text: "new " },
39 { type: "same", text: "world" },
40 ]);
41 });
42
43 it("prefix + suffix preserved; middle replaced", () => {
44 const segs = both("the quick brown fox", "the fast silver fox");
45 const oldRecon = reconstructOld(segs);
46 const newRecon = reconstructNew(segs);
47 expect(oldRecon).toBe("the quick brown fox");
48 expect(newRecon).toBe("the fast silver fox");
49
50
51 expect(segs[0]).toEqual({ type: "same", text: "the " });
52 expect(segs.at(-1)).toEqual({ type: "same", text: "fox" });
53 expect(segs.map((s) => s.type)).toEqual(["same", "del", "add", "same"]);
54 });
55
56 it("pure insertion", () => {
57 const segs = both("a c", "a b c");
58 expect(segs).toEqual([
59 { type: "same", text: "a " },
60 { type: "add", text: "b " },
61 { type: "same", text: "c" },
62 ]);
63 });
64
65 it("pure deletion", () => {
66 const segs = both("a b c", "a c");
67 expect(segs).toEqual([
68 { type: "same", text: "a " },
69 { type: "del", text: "b " },
70 { type: "same", text: "c" },
71 ]);
72 });
73
74 it("whitespace differences are themselves diffed (invariants hold)", () => {
75 both("a b", "a b").forEach(() => {});
76 const segs = wordDiff("a b", "a b");
77 expect(reconstructOld(segs)).toBe("a b");
78 expect(reconstructNew(segs)).toBe("a b");
79
80 expect(segs.some((s) => s.type === "del")).toBe(true);
81 });
82
83 it("multi-line text diffs line-wise-ish (word-level, invariants hold)", () => {
84 const oldText = "line one stays\nline two changes here\nline three stays";
85 const newText = "line one stays\nline two changes there\nline three stays";
86 both(oldText, newText);
87 });
88
89 it("is deterministic (same input → byte-identical output)", () => {
90 const a = wordDiff("the cat sat on the mat", "the dog sat on the rug");
91 const b = wordDiff("the cat sat on the mat", "the dog sat on the rug");
92 expect(a).toEqual(b);
93 expect(JSON.stringify(a)).toBe(JSON.stringify(b));
94 });
95
96 it("long texts (> 2000 words) degrade to full-replace, invariants hold", () => {
97 const oldText = Array.from({ length: 2500 }, (_, i) => `word${i}`).join(" ");
98 const newText = Array.from({ length: 2500 }, (_, i) => `word${i}-x`).join(" ");
99 const segs = wordDiff(oldText, newText);
100 expect(segs).toEqual([
101 { type: "del", text: oldText },
102 { type: "add", text: newText },
103 ]);
104 });
105
106 it("realistic AI rewrite (the E2E 'Rewritten:' prefix) produces a meaningful diff", () => {
107 const oldText = "Ship the release on Friday after the freeze lifts.";
108 const newText = "Rewritten: Ship the release on Friday after the freeze lifts.";
109 const segs = wordDiff(oldText, newText);
110
111 expect(segs[0]).toEqual({ type: "add", text: "Rewritten: " });
112 expect(segs[1]).toEqual({ type: "same", text: oldText });
113 });
114
115 it("consecutive same-type steps merge into one segment", () => {
116 const segs = both("alpha beta gamma", "alpha X Y gamma");
117
118 expect(segs.map((s: DiffSegment) => s.type)).toEqual(["same", "del", "add", "same"]);
119 });
120});
121