1import { describe, expect, it } from "vite-plus/test";
2import { getRenderablePatch, resolveFileDiffPath } from "./diffRendering";
3import { toolCallDiff } from "./toolCallDiff";
4
5describe("toolCallDiff", () => {
6 it("renders recorded hunks without reading a repository, including renamed paths", () => {
7 for (const changes of [
8 undefined,
9 [{ operation: "update", oldPath: "old.ts", path: "new.ts" }],
10 ]) {
11 const { patch } = toolCallDiff({
12 fileName: "old.ts",
13 diffStr: "@@ -1 +1 @@\n-before\n+after\n",
14 ...(changes ? { changes } : {}),
15 });
16 const rendered = getRenderablePatch(patch);
17 expect(rendered?.kind).toBe("files");
18 if (rendered?.kind !== "files") throw new Error("Expected a file diff");
19 expect(rendered.files).toHaveLength(1);
20 expect(resolveFileDiffPath(rendered.files[0]!)).toBe(changes ? "new.ts" : "old.ts");
21 expect(rendered.files[0]!.additionLines.join("")).toContain("after");
22 expect(rendered.files[0]!.deletionLines.join("")).toContain("before");
23 }
24 });
25
26 it("renders added and deleted file contents as a file diff", () => {
27 for (const operation of ["add", "delete"]) {
28 for (const [content, ending] of [
29 ["first\nsecond", ""],
30 ["@@ -0,0 +1 @@\n+content", "\n"],
31 ["@@ -1 +0,0 @@\n-content", "\n"],
32 ["first\nsecond", "\n"],
33 ["--- notes\nsecond", "\n"],
34 ["diff --git notes\nsecond", "\n"],
35 ["--- a/embedded.ts\n+++ b/embedded.ts\n@@ -1 +1 @@\n-before\n+after", "\n"],
36 [
37 "diff --git a/embedded.ts b/embedded.ts\n--- a/embedded.ts\n+++ b/embedded.ts\n@@ -1 +1 @@\n-before\n+after",
38 "\n",
39 ],
40 ] as const) {
41 const recordedContent = `${content}${ending}`;
42 const rendered = getRenderablePatch(
43 toolCallDiff({
44 fileName: "file with spaces.ts",
45 diffStr: recordedContent,
46 changes: [{ operation, path: "file with spaces.ts" }],
47 }).patch,
48 );
49 expect(rendered?.kind).toBe("files");
50 if (rendered?.kind !== "files") throw new Error("Expected a file diff");
51 expect(resolveFileDiffPath(rendered.files[0]!)).toBe("file with spaces.ts");
52 const file = rendered.files[0]!;
53 const lines = content.split("\n").length;
54 expect(file.additionLines).toHaveLength(operation === "add" ? lines : 0);
55 expect(file.deletionLines).toHaveLength(operation === "delete" ? lines : 0);
56 expect((operation === "add" ? file.additionLines : file.deletionLines).join("")).toBe(
57 recordedContent,
58 );
59 }
60 }
61 });
62
63 it("keeps content-only writes out of the patch and preserves empty recorded content", () => {
64 for (const operation of ["add", "delete"]) {
65 expect(
66 toolCallDiff({
67 fileName: "empty.ts",
68 diffStr: "",
69 changes: [{ operation, path: "empty.ts" }],
70 }).content,
71 ).toEqual([
72 {
73 label: operation === "add" ? "Recorded content after" : "Recorded content before",
74 text: "",
75 },
76 ]);
77 }
78 expect(toolCallDiff({ fileName: "a.ts", oldStr: "before", newStr: "" })).toEqual({
79 patch: undefined,
80 content: [
81 { label: "Recorded content before", text: "before" },
82 { label: "Recorded content after", text: "" },
83 ],
84 truncated: false,
85 });
86 expect(toolCallDiff({ fileName: "a.ts", newStr: "after" }).content).toEqual([
87 { label: "Recorded content after", text: "after" },
88 ]);
89 });
90
91 it("removes transport truncation markers before converting file contents", () => {
92 const suffix = "\n… output truncated for transport";
93 const diff = toolCallDiff({
94 fileName: "a.ts",
95 diffStr: `first\nsecond${suffix}`,
96 changes: [{ operation: "add", path: "a.ts" }],
97 });
98 expect(diff.truncated).toBe(true);
99 expect(diff.patch).not.toContain("output truncated");
100 expect(getRenderablePatch(diff.patch)?.kind).toBe("files");
101 expect(toolCallDiff({ fileName: "a.ts", newStr: `content${suffix}` })).toMatchObject({
102 content: [{ label: "Recorded content after", text: "content" }],
103 truncated: true,
104 });
105 });
106
107 it("preserves complete patches and unsupported provider output", () => {
108 for (const operation of ["add", "delete"]) {
109 const diffStr = [
110 "diff --git a/a.ts b/a.ts",
111 `--- ${operation === "add" ? "/dev/null" : "a/a.ts"}`,
112 `+++ ${operation === "add" ? "b/a.ts" : "/dev/null"}`,
113 operation === "add" ? "@@ -0,0 +1 @@" : "@@ -1 +0,0 @@",
114 operation === "add" ? "+content" : "-content",
115 "",
116 ].join("\n");
117 expect(
118 toolCallDiff({ fileName: "a.ts", diffStr, changes: [{ operation, path: "a.ts" }] }).patch,
119 ).toBe(diffStr);
120 }
121 for (const diffStr of [
122 undefined,
123 "",
124 "provider output",
125 "--- a/a.ts\n+++ b/a.ts\n@@ -1 +1 @@\n-a\n+b\n",
126 ]) {
127 expect(
128 toolCallDiff({ fileName: "a.ts", ...(diffStr === undefined ? {} : { diffStr }) }).patch,
129 ).toBe(diffStr);
130 }
131 });
132});