@abacus-ai/cli 1.106.25007 → 2.0.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/.oxlintrc.json +8 -0
  2. package/dist/index.mjs +12603 -0
  3. package/package.json +7 -39
  4. package/resources/abacus.ico +0 -0
  5. package/resources/entitlements.plist +9 -0
  6. package/src/__e2e__/README.md +196 -0
  7. package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
  8. package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
  9. package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
  10. package/src/__e2e__/conversation.e2e.test.tsx +56 -0
  11. package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
  12. package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
  13. package/src/__e2e__/helpers/test-helpers.ts +450 -0
  14. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
  15. package/src/__e2e__/llm-models.e2e.test.ts +402 -0
  16. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
  17. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
  18. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
  19. package/src/__e2e__/repl.e2e.test.tsx +78 -0
  20. package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
  21. package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
  22. package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
  23. package/src/args.ts +22 -0
  24. package/src/components/__tests__/react-compiler.test.tsx +78 -0
  25. package/src/components/__tests__/status-indicator.test.tsx +403 -0
  26. package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
  27. package/src/components/composer/agent-mode-indicator.tsx +63 -0
  28. package/src/components/composer/bash-runner.tsx +54 -0
  29. package/src/components/composer/commands/default-commands.tsx +615 -0
  30. package/src/components/composer/commands/handler.tsx +59 -0
  31. package/src/components/composer/commands/picker.tsx +273 -0
  32. package/src/components/composer/commands/registry.ts +233 -0
  33. package/src/components/composer/commands/types.ts +33 -0
  34. package/src/components/composer/context.tsx +88 -0
  35. package/src/components/composer/file-mention-picker.tsx +83 -0
  36. package/src/components/composer/help.tsx +44 -0
  37. package/src/components/composer/index.tsx +1006 -0
  38. package/src/components/composer/mentions.ts +57 -0
  39. package/src/components/composer/message-queue.tsx +70 -0
  40. package/src/components/composer/mode-panel.tsx +35 -0
  41. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
  42. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
  43. package/src/components/composer/modes/bash-handler.tsx +132 -0
  44. package/src/components/composer/modes/bash-renderer.tsx +175 -0
  45. package/src/components/composer/modes/default-handlers.tsx +33 -0
  46. package/src/components/composer/modes/index.ts +41 -0
  47. package/src/components/composer/modes/types.ts +21 -0
  48. package/src/components/composer/persistent-shell.ts +283 -0
  49. package/src/components/composer/process.ts +65 -0
  50. package/src/components/composer/types.ts +9 -0
  51. package/src/components/composer/use-mention-search.ts +68 -0
  52. package/src/components/error-boundry.tsx +60 -0
  53. package/src/components/exit-message.tsx +29 -0
  54. package/src/components/expanded-view.tsx +74 -0
  55. package/src/components/file-completion.tsx +127 -0
  56. package/src/components/header.tsx +47 -0
  57. package/src/components/logo.tsx +37 -0
  58. package/src/components/segments.tsx +356 -0
  59. package/src/components/status-indicator.tsx +306 -0
  60. package/src/components/tool-group-summary.tsx +263 -0
  61. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
  62. package/src/components/tool-permissions/diff-preview.tsx +355 -0
  63. package/src/components/tool-permissions/index.ts +5 -0
  64. package/src/components/tool-permissions/permission-options.tsx +375 -0
  65. package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
  66. package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
  67. package/src/components/tools/agent/ask-user-question.tsx +101 -0
  68. package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
  69. package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
  70. package/src/components/tools/agent/handoff-to-main.tsx +27 -0
  71. package/src/components/tools/agent/subagent.tsx +37 -0
  72. package/src/components/tools/agent/todo-write.tsx +104 -0
  73. package/src/components/tools/browser/close-tab.tsx +58 -0
  74. package/src/components/tools/browser/computer.tsx +70 -0
  75. package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
  76. package/src/components/tools/browser/get-tab-content.tsx +51 -0
  77. package/src/components/tools/browser/navigate-to.tsx +59 -0
  78. package/src/components/tools/browser/new-tab.tsx +60 -0
  79. package/src/components/tools/browser/perform-action.tsx +63 -0
  80. package/src/components/tools/browser/refresh-tab.tsx +43 -0
  81. package/src/components/tools/browser/switch-tab.tsx +58 -0
  82. package/src/components/tools/filesystem/delete-file.tsx +104 -0
  83. package/src/components/tools/filesystem/edit.tsx +220 -0
  84. package/src/components/tools/filesystem/list-dir.tsx +78 -0
  85. package/src/components/tools/filesystem/read-file.tsx +180 -0
  86. package/src/components/tools/filesystem/upload-image.tsx +76 -0
  87. package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
  88. package/src/components/tools/index.ts +91 -0
  89. package/src/components/tools/mcp/mcp-tool.tsx +158 -0
  90. package/src/components/tools/search/fetch-url.tsx +73 -0
  91. package/src/components/tools/search/file-search.tsx +78 -0
  92. package/src/components/tools/search/grep.tsx +90 -0
  93. package/src/components/tools/search/semantic-search.tsx +66 -0
  94. package/src/components/tools/search/web-search.tsx +71 -0
  95. package/src/components/tools/shared/index.tsx +48 -0
  96. package/src/components/tools/shared/zod-coercion.ts +35 -0
  97. package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
  98. package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
  99. package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
  100. package/src/components/tools/types.ts +16 -0
  101. package/src/components/tools.tsx +66 -0
  102. package/src/components/ui/__tests__/divider.test.tsx +61 -0
  103. package/src/components/ui/__tests__/gradient.test.tsx +125 -0
  104. package/src/components/ui/__tests__/input.test.tsx +166 -0
  105. package/src/components/ui/__tests__/select.test.tsx +273 -0
  106. package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
  107. package/src/components/ui/blinking-indicator.tsx +25 -0
  108. package/src/components/ui/divider.tsx +162 -0
  109. package/src/components/ui/gradient.tsx +56 -0
  110. package/src/components/ui/input.tsx +228 -0
  111. package/src/components/ui/select.tsx +151 -0
  112. package/src/components/ui/shimmer.tsx +84 -0
  113. package/src/context/agent-mode.tsx +95 -0
  114. package/src/context/extension-file.tsx +136 -0
  115. package/src/context/network-activity.tsx +45 -0
  116. package/src/context/notification.tsx +62 -0
  117. package/src/context/shell-size.tsx +49 -0
  118. package/src/context/shell-title.tsx +38 -0
  119. package/src/entrypoints/print-mode.ts +312 -0
  120. package/src/entrypoints/repl.tsx +401 -0
  121. package/src/hooks/use-agent.ts +15 -0
  122. package/src/hooks/use-api-client.ts +1 -0
  123. package/src/hooks/use-available-height.ts +8 -0
  124. package/src/hooks/use-cleanup.ts +29 -0
  125. package/src/hooks/use-interrupt-manager.ts +242 -0
  126. package/src/hooks/use-models.ts +22 -0
  127. package/src/index.ts +217 -0
  128. package/src/lib/__tests__/ansi.test.ts +255 -0
  129. package/src/lib/__tests__/cli.test.ts +122 -0
  130. package/src/lib/__tests__/commands.test.ts +325 -0
  131. package/src/lib/__tests__/constants.test.ts +15 -0
  132. package/src/lib/__tests__/focusables.test.ts +25 -0
  133. package/src/lib/__tests__/fs.test.ts +231 -0
  134. package/src/lib/__tests__/markdown.test.tsx +348 -0
  135. package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
  136. package/src/lib/__tests__/mcpManagement.test.ts +38 -0
  137. package/src/lib/__tests__/path-paste.test.ts +144 -0
  138. package/src/lib/__tests__/path.test.ts +300 -0
  139. package/src/lib/__tests__/queries.test.ts +39 -0
  140. package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
  141. package/src/lib/__tests__/text-buffer.test.ts +328 -0
  142. package/src/lib/__tests__/text-utils.test.ts +32 -0
  143. package/src/lib/__tests__/timing.test.ts +78 -0
  144. package/src/lib/__tests__/utils.test.ts +238 -0
  145. package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
  146. package/src/lib/ansi.ts +150 -0
  147. package/src/lib/cli-push-server.ts +112 -0
  148. package/src/lib/cli.ts +44 -0
  149. package/src/lib/clipboard.ts +226 -0
  150. package/src/lib/command-utils.ts +93 -0
  151. package/src/lib/commands.ts +270 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/extension-connection.ts +181 -0
  154. package/src/lib/focusables.ts +7 -0
  155. package/src/lib/fs.ts +533 -0
  156. package/src/lib/markdown/code-block.tsx +63 -0
  157. package/src/lib/markdown/index.ts +4 -0
  158. package/src/lib/markdown/link.tsx +19 -0
  159. package/src/lib/markdown/markdown.tsx +372 -0
  160. package/src/lib/markdown/types.ts +15 -0
  161. package/src/lib/mcpCommandHandler.ts +121 -0
  162. package/src/lib/mcpManagement.ts +44 -0
  163. package/src/lib/path-paste.ts +185 -0
  164. package/src/lib/path.ts +179 -0
  165. package/src/lib/queries.ts +15 -0
  166. package/src/lib/standaloneMcpService.ts +688 -0
  167. package/src/lib/status-utils.ts +237 -0
  168. package/src/lib/test-utils.tsx +72 -0
  169. package/src/lib/text-buffer.ts +2415 -0
  170. package/src/lib/text-utils.ts +272 -0
  171. package/src/lib/timing.ts +63 -0
  172. package/src/lib/types.ts +295 -0
  173. package/src/lib/utils.ts +182 -0
  174. package/src/lib/vim-buffer-actions.ts +732 -0
  175. package/src/providers/agent.tsx +1075 -0
  176. package/src/providers/api-client.tsx +43 -0
  177. package/src/services/logger.ts +85 -0
  178. package/src/terminal/detection.ts +187 -0
  179. package/src/terminal/exit.ts +279 -0
  180. package/src/terminal/notification.ts +83 -0
  181. package/src/terminal/progress.ts +201 -0
  182. package/src/terminal/setup.ts +797 -0
  183. package/src/terminal/suspend.ts +58 -0
  184. package/src/terminal/types.ts +51 -0
  185. package/src/theme/context.tsx +57 -0
  186. package/src/theme/index.ts +4 -0
  187. package/src/theme/themed.tsx +35 -0
  188. package/src/theme/themes.json +546 -0
  189. package/src/theme/types.ts +110 -0
  190. package/src/tools/types.ts +59 -0
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
  192. package/src/tools/utils/tool-ui-components.tsx +631 -0
  193. package/src/tools/utils/zod-coercion.ts +35 -0
  194. package/tsconfig.json +11 -0
  195. package/tsconfig.node.json +29 -0
  196. package/tsconfig.test.json +27 -0
  197. package/tsdown.config.ts +17 -0
  198. package/vitest.config.ts +76 -0
  199. package/README.md +0 -28
  200. package/dist/index.js +0 -26
@@ -0,0 +1,3399 @@
1
+ import stripAnsi from "strip-ansi";
2
+ import { describe, it, expect, afterEach, beforeEach } from "vitest";
3
+
4
+ import { DiffPreview, DiffPreviewTitle } from "../components/tool-permissions/diff-preview.js";
5
+ // TODO: Replace with jar-testing-library when available
6
+ import { render, logInk, cleanup } from "../lib/test-utils.js";
7
+ import { ThemeProvider } from "../theme/context.js";
8
+ import { computeExpectedFinalContentForDiff } from "../tools/utils/tool-ui-components.js";
9
+ void logInk; // Keep for debugging
10
+ import { View, Text } from "@codellm/jar";
11
+ import { structuredPatch } from "diff";
12
+ import * as fs from "fs/promises";
13
+ import * as os from "os";
14
+ import * as path from "path";
15
+ import React from "react";
16
+
17
+ import { PermissionPreviewHeader } from "../components/tool-permissions/permission-preview-header.js";
18
+
19
+ /**
20
+ * =============================================================================
21
+ * PRODUCTION CODE - DIRECT IMPORT
22
+ * =============================================================================
23
+ * We import the ACTUAL production function to test it directly.
24
+ * tuiPreviewLogic = computeExpectedFinalContentForDiff from production
25
+ * =============================================================================
26
+ */
27
+
28
+ // Use the ACTUAL production function for TUI preview logic
29
+ const tuiPreviewLogic = computeExpectedFinalContentForDiff;
30
+
31
+ /**
32
+ * COPIED FROM: src/tools/implementations/filesystem/EditTool.tsx (execute fallback, lines 489-529)
33
+ * This is what PRODUCTION uses when API codeChanges is not available
34
+ */
35
+ function productionFallbackLogic(
36
+ currentContent: string,
37
+ codeEdit: string,
38
+ overwriteFile: boolean,
39
+ startLine?: number,
40
+ endLine?: number,
41
+ _instructions?: string,
42
+ ): string {
43
+ // New file or overwrite - same as preview
44
+ if (overwriteFile) {
45
+ return codeEdit;
46
+ }
47
+
48
+ // Line range replacement - SAME as preview
49
+ if (startLine !== undefined && endLine !== undefined) {
50
+ const lines = currentContent.split("\n");
51
+ const totalLines = lines.length;
52
+ const validStartLine = Math.max(1, Math.min(startLine, totalLines));
53
+ const validEndLine = Math.max(validStartLine, Math.min(endLine, totalLines));
54
+
55
+ const beforeLines = lines.slice(0, validStartLine - 1);
56
+ const afterLines = lines.slice(validEndLine);
57
+ const newLines = [...beforeLines, ...codeEdit.split("\n"), ...afterLines];
58
+ return newLines.join("\n");
59
+ }
60
+
61
+ // Check if already present
62
+ if (currentContent.includes(codeEdit)) {
63
+ return currentContent; // No change
64
+ }
65
+
66
+ // Default: append
67
+ // NOTE: Production fallback does NOT have the 80% rule or instructions check!
68
+ return currentContent + "\n" + codeEdit;
69
+ }
70
+
71
+ /**
72
+ * Mock EditToolPreview component that renders the same UI as production
73
+ */
74
+ interface MockEditToolPreviewProps {
75
+ targetFile: string;
76
+ codeEdit: string;
77
+ currentFileContent?: string;
78
+ fileExists?: boolean;
79
+ overwriteFile?: boolean;
80
+ startLine?: number;
81
+ endLine?: number;
82
+ instructions?: string;
83
+ }
84
+
85
+ function MockEditToolPreview({
86
+ targetFile,
87
+ codeEdit,
88
+ currentFileContent = "",
89
+ fileExists = true,
90
+ overwriteFile = false,
91
+ startLine,
92
+ endLine,
93
+ instructions,
94
+ }: MockEditToolPreviewProps) {
95
+ const isNewFile = !fileExists;
96
+ const operationType = isNewFile ? "Create" : overwriteFile ? "Overwrite" : "Edit";
97
+
98
+ const expectedFinalContent = React.useMemo(() => {
99
+ if (isNewFile || overwriteFile) {
100
+ return codeEdit;
101
+ }
102
+ return computeExpectedFinalContentForDiff(
103
+ currentFileContent,
104
+ codeEdit,
105
+ overwriteFile,
106
+ startLine,
107
+ endLine,
108
+ instructions,
109
+ );
110
+ }, [currentFileContent, codeEdit, overwriteFile, isNewFile, startLine, endLine, instructions]);
111
+
112
+ return (
113
+ <View flexDirection="column">
114
+ <PermissionPreviewHeader title={`${operationType} file ${targetFile}`} />
115
+ <DiffPreview
116
+ filePath={targetFile}
117
+ originalContent={isNewFile ? "" : currentFileContent}
118
+ newContent={expectedFinalContent}
119
+ />
120
+ <View paddingLeft={1} paddingY={1}>
121
+ <Text> Do you want to make this edit to {targetFile}?</Text>
122
+ </View>
123
+ </View>
124
+ );
125
+ }
126
+
127
+ /**
128
+ * Type for tool permission data used in tests
129
+ */
130
+ interface ToolPermissionData {
131
+ targetFile: string;
132
+ codeEdit: string;
133
+ currentFileContent?: string;
134
+ fileExists?: boolean;
135
+ overwriteFile?: boolean;
136
+ startLine?: number;
137
+ endLine?: number;
138
+ instructions?: string;
139
+ }
140
+
141
+ /**
142
+ * Helper function to create mock tool permission data
143
+ */
144
+ function createMockToolPermission(props: ToolPermissionData): ToolPermissionData {
145
+ return {
146
+ targetFile: props.targetFile,
147
+ codeEdit: props.codeEdit,
148
+ currentFileContent: props.currentFileContent ?? "",
149
+ fileExists: props.fileExists ?? true,
150
+ overwriteFile: props.overwriteFile ?? false,
151
+ startLine: props.startLine,
152
+ endLine: props.endLine,
153
+ instructions: props.instructions,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * EditToolPreview wrapper - accepts queuedTool prop like old production component
159
+ */
160
+ function EditToolPreview({ queuedTool }: { queuedTool: ToolPermissionData }) {
161
+ return (
162
+ <MockEditToolPreview
163
+ targetFile={queuedTool.targetFile}
164
+ codeEdit={queuedTool.codeEdit}
165
+ currentFileContent={queuedTool.currentFileContent}
166
+ fileExists={queuedTool.fileExists}
167
+ overwriteFile={queuedTool.overwriteFile}
168
+ startLine={queuedTool.startLine}
169
+ endLine={queuedTool.endLine}
170
+ instructions={queuedTool.instructions}
171
+ />
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Helper to wrap components in ThemeProvider for testing
177
+ */
178
+ function renderWithTheme(element: React.ReactElement) {
179
+ return render(<ThemeProvider>{element}</ThemeProvider>);
180
+ }
181
+
182
+ /**
183
+ * Always display the TUI output in tests
184
+ * This makes it easy to see what the TUI renders
185
+ */
186
+ function displayTUI(testName: string, instance: ReturnType<typeof render>) {
187
+ const output = instance.lastFrame() ?? "";
188
+ const plainText = stripAnsi(output);
189
+
190
+ console.log("\n" + "=".repeat(80));
191
+ console.log(`TUI OUTPUT: ${testName}`);
192
+ console.log("=".repeat(80));
193
+ console.log(plainText);
194
+ console.log("=".repeat(80) + "\n");
195
+
196
+ return { output, plainText };
197
+ }
198
+
199
+ describe.concurrent("DiffPreview - Basic Rendering", () => {
200
+ afterEach(() => {
201
+ cleanup();
202
+ });
203
+
204
+ it('should render "No changes detected" when content is identical', () => {
205
+ const content = "const x = 1;\nconst y = 2;";
206
+
207
+ const instance = renderWithTheme(
208
+ <DiffPreview filePath="test.js" originalContent={content} newContent={content} />,
209
+ );
210
+
211
+ const { plainText } = displayTUI("No changes detected", instance);
212
+
213
+ expect(plainText).toContain("No changes detected");
214
+ });
215
+
216
+ it("should show added line with + indicator when adding a line", () => {
217
+ const original = "const x = 1;";
218
+ const modified = "const x = 1;\nconsole.log(x);";
219
+
220
+ const instance = renderWithTheme(
221
+ <DiffPreview filePath="test.js" originalContent={original} newContent={modified} />,
222
+ );
223
+
224
+ const { plainText } = displayTUI("Added line with +", instance);
225
+
226
+ // Should show the added line with + indicator
227
+ expect(plainText).toContain("+");
228
+ expect(plainText).toContain("console.log(x)");
229
+ });
230
+
231
+ it("should show removed line with - indicator when removing a line", () => {
232
+ const original = "const x = 1;\nconsole.log(x);\nconst y = 2;";
233
+ const modified = "const x = 1;\nconst y = 2;";
234
+
235
+ const instance = renderWithTheme(
236
+ <DiffPreview filePath="test.js" originalContent={original} newContent={modified} />,
237
+ );
238
+
239
+ const { plainText } = displayTUI("Removed line with -", instance);
240
+
241
+ // Should show the removed line with - indicator
242
+ expect(plainText).toContain("-");
243
+ expect(plainText).toContain("console.log(x)");
244
+ });
245
+ });
246
+
247
+ describe.concurrent("DiffPreview - Adding console.log statements", () => {
248
+ afterEach(() => {
249
+ cleanup();
250
+ });
251
+
252
+ it("should correctly show diff when adding console.log at the beginning", () => {
253
+ const original = `function add(a, b) {
254
+ return a + b;
255
+ }`;
256
+ const modified = `function add(a, b) {
257
+ console.log('add called with:', a, b);
258
+ return a + b;
259
+ }`;
260
+
261
+ const instance = renderWithTheme(
262
+ <DiffPreview filePath="math.js" originalContent={original} newContent={modified} />,
263
+ );
264
+
265
+ const { plainText } = displayTUI("console.log at beginning", instance);
266
+
267
+ // The added console.log line should appear
268
+ expect(plainText).toContain("console.log");
269
+ expect(plainText).toContain("add called with");
270
+ // Should have + indicator for the added line
271
+ expect(plainText).toContain("+");
272
+ });
273
+
274
+ it("should correctly show diff when adding console.log at the end", () => {
275
+ const original = `function multiply(a, b) {
276
+ const result = a * b;
277
+ return result;
278
+ }`;
279
+ const modified = `function multiply(a, b) {
280
+ const result = a * b;
281
+ console.log('result:', result);
282
+ return result;
283
+ }`;
284
+
285
+ const instance = renderWithTheme(
286
+ <DiffPreview filePath="math.js" originalContent={original} newContent={modified} />,
287
+ );
288
+
289
+ const { plainText } = displayTUI("console.log at end", instance);
290
+
291
+ // The added console.log line should appear
292
+ expect(plainText).toContain("console.log");
293
+ expect(plainText).toContain("result");
294
+ expect(plainText).toContain("+");
295
+ });
296
+
297
+ it("should correctly show diff when adding multiple console.log statements", () => {
298
+ const original = `function process(data) {
299
+ const parsed = JSON.parse(data);
300
+ const filtered = parsed.filter(x => x.active);
301
+ return filtered;
302
+ }`;
303
+ const modified = `function process(data) {
304
+ console.log('Input data:', data);
305
+ const parsed = JSON.parse(data);
306
+ console.log('Parsed:', parsed);
307
+ const filtered = parsed.filter(x => x.active);
308
+ console.log('Filtered count:', filtered.length);
309
+ return filtered;
310
+ }`;
311
+
312
+ const instance = renderWithTheme(
313
+ <DiffPreview filePath="processor.js" originalContent={original} newContent={modified} />,
314
+ );
315
+
316
+ const { plainText } = displayTUI("Multiple console.log statements", instance);
317
+
318
+ // All three console.log statements should appear
319
+ expect(plainText).toContain("Input data");
320
+ expect(plainText).toContain("Parsed");
321
+ expect(plainText).toContain("Filtered count");
322
+
323
+ // Count the number of + indicators (should be at least 3 for the 3 added lines)
324
+ const plusCount = (plainText.match(/\+/g) || []).length;
325
+ expect(plusCount).toBeGreaterThanOrEqual(3);
326
+ });
327
+ });
328
+
329
+ describe.concurrent("DiffPreview - Line modifications", () => {
330
+ afterEach(() => {
331
+ cleanup();
332
+ });
333
+
334
+ it("should show both removed and added when modifying a line", () => {
335
+ const original = 'const message = "hello";';
336
+ const modified = 'const message = "goodbye";';
337
+
338
+ const instance = renderWithTheme(
339
+ <DiffPreview filePath="test.js" originalContent={original} newContent={modified} />,
340
+ );
341
+
342
+ const { plainText } = displayTUI(" Line modification", instance);
343
+
344
+ // Should show both the old and new versions
345
+ expect(plainText).toContain("hello");
346
+ expect(plainText).toContain("goodbye");
347
+ // Should have both - and + indicators
348
+ expect(plainText).toContain("-");
349
+ expect(plainText).toContain("+");
350
+ });
351
+
352
+ it("should correctly show variable rename", () => {
353
+ const original = `const oldName = 42;
354
+ console.log(oldName);`;
355
+ const modified = `const newName = 42;
356
+ console.log(newName);`;
357
+
358
+ const instance = renderWithTheme(
359
+ <DiffPreview filePath="test.js" originalContent={original} newContent={modified} />,
360
+ );
361
+
362
+ const { plainText } = displayTUI(" Variable rename", instance);
363
+
364
+ // Should show the change from oldName to newName
365
+ expect(plainText).toContain("oldName");
366
+ expect(plainText).toContain("newName");
367
+ });
368
+ });
369
+
370
+ describe.concurrent("DiffPreview - Line numbers", () => {
371
+ afterEach(() => {
372
+ cleanup();
373
+ });
374
+
375
+ it("should display correct line numbers for single-digit lines", () => {
376
+ const original = "line1\nline2\nline3";
377
+ const modified = "line1\nmodified\nline3";
378
+
379
+ const instance = renderWithTheme(
380
+ <DiffPreview filePath="test.txt" originalContent={original} newContent={modified} />,
381
+ );
382
+
383
+ const { plainText } = displayTUI(" Line numbers", instance);
384
+
385
+ // Line 2 is modified, so we should see line number 2
386
+ expect(plainText).toContain("2");
387
+ });
388
+
389
+ it("should display correct line numbers for multi-digit lines", () => {
390
+ // Create a file with 15 lines
391
+ const lines = Array.from({ length: 15 }, (_, i) => `line ${i + 1}`);
392
+ const original = lines.join("\n");
393
+
394
+ // Modify line 12
395
+ const modifiedLines = [...lines];
396
+ modifiedLines[11] = "modified line 12";
397
+ const modified = modifiedLines.join("\n");
398
+
399
+ const instance = renderWithTheme(
400
+ <DiffPreview filePath="test.txt" originalContent={original} newContent={modified} />,
401
+ );
402
+
403
+ const { plainText } = displayTUI(" Multi-digit line numbers", instance);
404
+
405
+ // Should show line 12
406
+ expect(plainText).toContain("12");
407
+ expect(plainText).toContain("modified line 12");
408
+ });
409
+ });
410
+
411
+ describe.concurrent("DiffPreview - Context and separators", () => {
412
+ afterEach(() => {
413
+ cleanup();
414
+ });
415
+
416
+ it("should show separator for skipped unchanged lines", () => {
417
+ // Create a file with many lines, change only at the beginning and end
418
+ const lines = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`);
419
+ const original = lines.join("\n");
420
+
421
+ // Modify first and last line
422
+ const modifiedLines = [...lines];
423
+ modifiedLines[0] = "modified first line";
424
+ modifiedLines[19] = "modified last line";
425
+ const modified = modifiedLines.join("\n");
426
+
427
+ const instance = renderWithTheme(
428
+ <DiffPreview filePath="test.txt" originalContent={original} newContent={modified} />,
429
+ );
430
+
431
+ const { plainText } = displayTUI(" Separator for unchanged lines", instance);
432
+
433
+ // Should show separator indicator for skipped lines
434
+ expect(plainText).toContain("unchanged lines");
435
+ });
436
+ });
437
+
438
+ describe.concurrent("DiffPreview - Real-world scenarios", () => {
439
+ afterEach(() => {
440
+ cleanup();
441
+ });
442
+
443
+ it("should correctly display a typical code change (adding error handling)", () => {
444
+ const original = `async function fetchData(url) {
445
+ const response = await fetch(url);
446
+ const data = await response.json();
447
+ return data;
448
+ }`;
449
+ const modified = `async function fetchData(url) {
450
+ try {
451
+ const response = await fetch(url);
452
+ const data = await response.json();
453
+ return data;
454
+ } catch (error) {
455
+ console.error('Fetch failed:', error);
456
+ throw error;
457
+ }
458
+ }`;
459
+
460
+ const instance = renderWithTheme(
461
+ <DiffPreview filePath="api.js" originalContent={original} newContent={modified} />,
462
+ );
463
+
464
+ const { plainText } = displayTUI(" Error handling addition", instance);
465
+
466
+ // Should show the try/catch structure
467
+ expect(plainText).toContain("try");
468
+ expect(plainText).toContain("catch");
469
+ expect(plainText).toContain("console.error");
470
+ });
471
+
472
+ it("should correctly display a React component modification", () => {
473
+ const original = `function Button({ label }) {
474
+ return (
475
+ <button>{label}</button>
476
+ );
477
+ }`;
478
+ const modified = `function Button({ label, onClick, disabled }) {
479
+ return (
480
+ <button onClick={onClick} disabled={disabled}>
481
+ {label}
482
+ </button>
483
+ );
484
+ }`;
485
+
486
+ const instance = renderWithTheme(
487
+ <DiffPreview filePath="Button.tsx" originalContent={original} newContent={modified} />,
488
+ );
489
+
490
+ const { plainText } = displayTUI(" React component change", instance);
491
+
492
+ // Should show the new props
493
+ expect(plainText).toContain("onClick");
494
+ expect(plainText).toContain("disabled");
495
+ });
496
+ });
497
+
498
+ describe.concurrent("New File Preview (via DiffPreview with empty original)", () => {
499
+ afterEach(() => {
500
+ cleanup();
501
+ });
502
+
503
+ it("should display all lines as added for new file", () => {
504
+ const content = `const x = 1;
505
+ const y = 2;
506
+ console.log(x + y);`;
507
+
508
+ // New files are displayed using DiffPreview with empty original content
509
+ const instance = renderWithTheme(
510
+ <DiffPreview filePath="new-file.js" originalContent="" newContent={content} />,
511
+ );
512
+
513
+ const { plainText } = displayTUI(" New file preview", instance);
514
+
515
+ // All lines should have + indicator
516
+ expect(plainText).toContain("const x = 1");
517
+ expect(plainText).toContain("const y = 2");
518
+ expect(plainText).toContain("console.log");
519
+
520
+ // Count + indicators (should be at least 3)
521
+ const plusCount = (plainText.match(/\+/g) || []).length;
522
+ expect(plusCount).toBeGreaterThanOrEqual(3);
523
+ });
524
+
525
+ it("should display correct line numbers for new file", () => {
526
+ const content = `line 1
527
+ line 2
528
+ line 3`;
529
+
530
+ // New files are displayed using DiffPreview with empty original content
531
+ const instance = renderWithTheme(
532
+ <DiffPreview filePath="new-file.txt" originalContent="" newContent={content} />,
533
+ );
534
+
535
+ const { plainText } = displayTUI(" New file line numbers", instance);
536
+
537
+ // Should show line numbers 1, 2, 3
538
+ expect(plainText).toContain("1");
539
+ expect(plainText).toContain("2");
540
+ expect(plainText).toContain("3");
541
+ });
542
+ });
543
+
544
+ describe.concurrent("DiffPreviewTitle", () => {
545
+ afterEach(() => {
546
+ cleanup();
547
+ });
548
+
549
+ it("should display file path and addition count", () => {
550
+ // Use newline-terminated content to avoid "No newline at end of file" markers
551
+ const hunks = structuredPatch(
552
+ "test.js",
553
+ "test.js",
554
+ "const x = 1;\n",
555
+ "const x = 1;\nconst y = 2;\n",
556
+ ).hunks;
557
+
558
+ const instance = renderWithTheme(<DiffPreviewTitle filePath="test.js" hunks={hunks} />);
559
+
560
+ const { plainText } = displayTUI(" Title with additions", instance);
561
+
562
+ // Should show file path
563
+ expect(plainText).toContain("test.js");
564
+ // Should show +1 for one added line
565
+ expect(plainText).toContain("+1");
566
+ });
567
+
568
+ it("should display removal count", () => {
569
+ // Use newline-terminated content to avoid "No newline at end of file" markers
570
+ const hunks = structuredPatch(
571
+ "test.js",
572
+ "test.js",
573
+ "const x = 1;\nconst y = 2;\n",
574
+ "const x = 1;\n",
575
+ ).hunks;
576
+
577
+ const instance = renderWithTheme(<DiffPreviewTitle filePath="test.js" hunks={hunks} />);
578
+
579
+ const { plainText } = displayTUI(" Title with removals", instance);
580
+
581
+ // Should show -1 for one removed line
582
+ expect(plainText).toContain("-1");
583
+ });
584
+
585
+ it("should display both addition and removal counts", () => {
586
+ const hunks = structuredPatch(
587
+ "test.js",
588
+ "test.js",
589
+ "const old = 1;",
590
+ "const new1 = 1;\nconst new2 = 2;",
591
+ ).hunks;
592
+
593
+ const instance = renderWithTheme(<DiffPreviewTitle filePath="test.js" hunks={hunks} />);
594
+
595
+ const { plainText } = displayTUI(" Title with both", instance);
596
+
597
+ // Should show both + and - counts
598
+ expect(plainText).toContain("+");
599
+ expect(plainText).toContain("-");
600
+ });
601
+ });
602
+
603
+ describe.concurrent("DiffPreview - Diff accuracy verification", () => {
604
+ afterEach(() => {
605
+ cleanup();
606
+ });
607
+
608
+ /**
609
+ * This test verifies that the diff shown in TUI matches
610
+ * what the actual file change would be. This is critical
611
+ * for catching regression bugs where the diff display
612
+ * doesn't match reality.
613
+ */
614
+ it("should accurately represent the exact changes being made", () => {
615
+ const original = `// Calculator module
616
+ function add(a, b) {
617
+ return a + b;
618
+ }
619
+
620
+ function subtract(a, b) {
621
+ return a - b;
622
+ }
623
+
624
+ module.exports = { add, subtract };`;
625
+
626
+ const modified = `// Calculator module
627
+ function add(a, b) {
628
+ console.log('Adding:', a, '+', b);
629
+ return a + b;
630
+ }
631
+
632
+ function subtract(a, b) {
633
+ console.log('Subtracting:', a, '-', b);
634
+ return a - b;
635
+ }
636
+
637
+ function multiply(a, b) {
638
+ return a * b;
639
+ }
640
+
641
+ module.exports = { add, subtract, multiply };`;
642
+
643
+ const instance = renderWithTheme(
644
+ <DiffPreview filePath="calculator.js" originalContent={original} newContent={modified} />,
645
+ );
646
+
647
+ const { plainText } = displayTUI("Diff Accuracy - Full diff output", instance);
648
+
649
+ // Verify added console.log statements appear
650
+ expect(plainText).toContain("console.log('Adding:'");
651
+ expect(plainText).toContain("console.log('Subtracting:'");
652
+
653
+ // Verify new multiply function appears
654
+ expect(plainText).toContain("function multiply");
655
+ expect(plainText).toContain("a * b");
656
+
657
+ // Verify module.exports change appears
658
+ expect(plainText).toContain("multiply");
659
+
660
+ // Verify we have + indicators for added lines
661
+ const plusLines = plainText.split("\n").filter((line) => line.includes("+"));
662
+ console.log("Lines with + indicator:", plusLines.length);
663
+
664
+ // Should have multiple added lines
665
+ expect(plusLines.length).toBeGreaterThanOrEqual(5);
666
+ });
667
+
668
+ it("should show exact content for replaced lines", () => {
669
+ const original = 'const API_URL = "http://localhost:3000";';
670
+ const modified = 'const API_URL = "https://api.production.com";';
671
+
672
+ const instance = renderWithTheme(
673
+ <DiffPreview filePath="config.js" originalContent={original} newContent={modified} />,
674
+ );
675
+
676
+ const { plainText } = displayTUI("Diff Accuracy - URL replacement", instance);
677
+
678
+ // Both old and new URL should be visible
679
+ expect(plainText).toContain("localhost:3000");
680
+ expect(plainText).toContain("api.production.com");
681
+
682
+ // Old line should have - indicator, new line should have + indicator
683
+ const minusLine = plainText
684
+ .split("\n")
685
+ .find((line) => line.includes("-") && line.includes("localhost"));
686
+ const plusLine = plainText
687
+ .split("\n")
688
+ .find((line) => line.includes("+") && line.includes("production"));
689
+
690
+ expect(minusLine).toBeDefined();
691
+ expect(plusLine).toBeDefined();
692
+ });
693
+ });
694
+
695
+ /**
696
+ * =============================================================================
697
+ * PRODUCTION FLOW TESTS
698
+ * =============================================================================
699
+ * These tests verify the actual content calculation logic used in production.
700
+ * If these tests pass, production will work correctly.
701
+ * If production fails, these tests should also fail.
702
+ * =============================================================================
703
+ */
704
+
705
+ /**
706
+ * Replicates the EXACT production computeExpectedFinalContent logic.
707
+ * This is copied from edit-tool-permission.tsx to test it directly.
708
+ * If the production code changes, this must be updated to match.
709
+ */
710
+ function computeExpectedFinalContent(
711
+ currentContent: string,
712
+ codeEdit: string,
713
+ overwriteFile: boolean,
714
+ startLine?: number,
715
+ endLine?: number,
716
+ instructions?: string,
717
+ ): string {
718
+ if (overwriteFile) {
719
+ return codeEdit;
720
+ }
721
+
722
+ if (startLine !== undefined && endLine !== undefined) {
723
+ const lines = currentContent.split("\n");
724
+ const totalLines = lines.length;
725
+ const validStartLine = Math.max(1, Math.min(startLine, totalLines));
726
+ const validEndLine = Math.max(validStartLine, Math.min(endLine, totalLines));
727
+
728
+ const beforeLines = lines.slice(0, validStartLine - 1);
729
+ const afterLines = lines.slice(validEndLine);
730
+ const newLines = codeEdit.split("\n");
731
+
732
+ return [...beforeLines, ...newLines, ...afterLines].join("\n");
733
+ }
734
+
735
+ if (instructions) {
736
+ return currentContent + "\n" + codeEdit;
737
+ }
738
+
739
+ const codeEditLines = codeEdit.split("\n");
740
+ const currentLines = currentContent.split("\n");
741
+
742
+ if (codeEditLines.length >= currentLines.length * 0.8) {
743
+ return codeEdit;
744
+ }
745
+
746
+ if (currentContent.includes(codeEdit)) {
747
+ return currentContent;
748
+ }
749
+
750
+ return currentContent + "\n" + codeEdit;
751
+ }
752
+
753
+ describe.concurrent("computeExpectedFinalContent - Production Logic Tests", () => {
754
+ /**
755
+ * These tests verify the content calculation that happens BEFORE
756
+ * the diff is rendered. If this logic is wrong, the diff shown
757
+ * won't match what actually happens to the file.
758
+ */
759
+
760
+ it("should overwrite entire file when overwriteFile is true", () => {
761
+ const currentContent = `function old() {
762
+ return 'old';
763
+ }`;
764
+ const codeEdit = `function new() {
765
+ return 'new';
766
+ }`;
767
+
768
+ const result = computeExpectedFinalContent(
769
+ currentContent,
770
+ codeEdit,
771
+ true, // overwriteFile
772
+ );
773
+
774
+ expect(result).toBe(codeEdit);
775
+ expect(result).not.toContain("old");
776
+ console.log("[Prod Test] Overwrite file:", result);
777
+ });
778
+
779
+ it("should replace specific lines when startLine and endLine are provided", () => {
780
+ const currentContent = `line 1
781
+ line 2
782
+ line 3
783
+ line 4
784
+ line 5`;
785
+ const codeEdit = `replaced line 2
786
+ replaced line 3`;
787
+
788
+ const result = computeExpectedFinalContent(
789
+ currentContent,
790
+ codeEdit,
791
+ false,
792
+ 2, // startLine
793
+ 3, // endLine
794
+ );
795
+
796
+ expect(result).toBe(`line 1
797
+ replaced line 2
798
+ replaced line 3
799
+ line 4
800
+ line 5`);
801
+ console.log("[Prod Test] Replace lines 2-3:", result);
802
+ });
803
+
804
+ it("should correctly add console.log at specific line", () => {
805
+ const currentContent = `function greet(name) {
806
+ return 'Hello ' + name;
807
+ }`;
808
+ const codeEdit = ` console.log('greet called with:', name);
809
+ return 'Hello ' + name;`;
810
+
811
+ // Simulating replacing lines 2-2 (the return statement)
812
+ const result = computeExpectedFinalContent(
813
+ currentContent,
814
+ codeEdit,
815
+ false,
816
+ 2, // startLine
817
+ 2, // endLine
818
+ );
819
+
820
+ const expected = `function greet(name) {
821
+ console.log('greet called with:', name);
822
+ return 'Hello ' + name;
823
+ }`;
824
+
825
+ expect(result).toBe(expected);
826
+ expect(result).toContain("console.log");
827
+ console.log("[Prod Test] Add console.log:", result);
828
+ });
829
+
830
+ it("should append content when instructions are provided", () => {
831
+ const currentContent = `const x = 1;`;
832
+ const codeEdit = `const y = 2;`;
833
+
834
+ const result = computeExpectedFinalContent(
835
+ currentContent,
836
+ codeEdit,
837
+ false,
838
+ undefined,
839
+ undefined,
840
+ "Add a new variable", // instructions
841
+ );
842
+
843
+ expect(result).toBe(`const x = 1;
844
+ const y = 2;`);
845
+ console.log("[Prod Test] Append with instructions:", result);
846
+ });
847
+
848
+ it("should replace all when codeEdit is >= 80% of current content", () => {
849
+ const currentContent = `a
850
+ b
851
+ c
852
+ d
853
+ e`;
854
+ // codeEdit has 4 lines, which is 80% of 5 lines
855
+ const codeEdit = `1
856
+ 2
857
+ 3
858
+ 4`;
859
+
860
+ const result = computeExpectedFinalContent(currentContent, codeEdit, false);
861
+
862
+ expect(result).toBe(codeEdit);
863
+ console.log("[Prod Test] Replace all (80% rule):", result);
864
+ });
865
+
866
+ it("should not duplicate content if codeEdit already exists in file", () => {
867
+ const currentContent = `const x = 1;
868
+ console.log(x);
869
+ const y = 2;`;
870
+ const codeEdit = `console.log(x);`;
871
+
872
+ const result = computeExpectedFinalContent(currentContent, codeEdit, false);
873
+
874
+ // Should not append since codeEdit already exists
875
+ expect(result).toBe(currentContent);
876
+ // Should only have one console.log
877
+ expect((result.match(/console\.log/g) || []).length).toBe(1);
878
+ console.log("[Prod Test] No duplicate:", result);
879
+ });
880
+
881
+ it("should append small additions to end of file", () => {
882
+ const currentContent = `function main() {
883
+ doSomething();
884
+ doSomethingElse();
885
+ doAnotherThing();
886
+ doMoreStuff();
887
+ }`;
888
+ const codeEdit = `console.log('done');`;
889
+
890
+ const result = computeExpectedFinalContent(currentContent, codeEdit, false);
891
+
892
+ expect(result).toContain("console.log");
893
+ expect(result.endsWith("console.log('done');")).toBe(true);
894
+ console.log("[Prod Test] Append small addition:", result);
895
+ });
896
+ });
897
+
898
+ describe.concurrent("Production Diff Flow - End to End", () => {
899
+ afterEach(() => {
900
+ cleanup();
901
+ });
902
+
903
+ /**
904
+ * These tests simulate what happens in production:
905
+ * 1. We have a file with content (currentContent)
906
+ * 2. An edit request comes in with codeEdit and parameters
907
+ * 3. computeExpectedFinalContent calculates the new content
908
+ * 4. DiffPreview shows the diff between original and expected
909
+ *
910
+ * We verify that the diff shown MATCHES what computeExpectedFinalContent produces.
911
+ */
912
+
913
+ it("PRODUCTION FLOW: adding console.log should show correct diff", () => {
914
+ // Step 1: Current file content (what's on disk)
915
+ const currentContent = `function processUser(user) {
916
+ validateUser(user);
917
+ saveUser(user);
918
+ return user.id;
919
+ }`;
920
+
921
+ // Step 2: Edit request comes in
922
+ const codeEdit = ` console.log('Processing user:', user.id);
923
+ validateUser(user);`;
924
+ const startLine = 2;
925
+ const endLine = 2;
926
+
927
+ // Step 3: Calculate expected final content (this is what production does)
928
+ const expectedFinalContent = computeExpectedFinalContent(
929
+ currentContent,
930
+ codeEdit,
931
+ false,
932
+ startLine,
933
+ endLine,
934
+ );
935
+
936
+ console.log("[Prod Flow] Expected final content:");
937
+ console.log(expectedFinalContent);
938
+
939
+ // Step 4: Verify the calculated content is correct
940
+ expect(expectedFinalContent).toContain("console.log");
941
+ expect(expectedFinalContent).toContain("validateUser");
942
+ expect(expectedFinalContent).toContain("saveUser");
943
+
944
+ // Step 5: Render the diff (what user sees in TUI)
945
+ const instance = renderWithTheme(
946
+ <DiffPreview
947
+ filePath="user.js"
948
+ originalContent={currentContent}
949
+ newContent={expectedFinalContent}
950
+ />,
951
+ );
952
+
953
+ const { plainText } = displayTUI("PROD FLOW: Adding console.log", instance);
954
+
955
+ // Step 6: Verify diff shows the added console.log
956
+ expect(plainText).toContain("console.log");
957
+ expect(plainText).toContain("+");
958
+
959
+ // Step 7: Verify the diff accurately reflects the change
960
+ // The added line should be visible with + indicator
961
+ const addedLines = plainText.split("\n").filter((line) => line.includes("+"));
962
+ expect(addedLines.some((line) => line.includes("console.log"))).toBe(true);
963
+ });
964
+
965
+ it("PRODUCTION FLOW: replacing a function should show both old and new", () => {
966
+ const currentContent = `function calculate(a, b) {
967
+ return a + b;
968
+ }
969
+
970
+ function helper() {
971
+ return 42;
972
+ }`;
973
+
974
+ // Overwrite the entire file with new implementation
975
+ const codeEdit = `function calculate(a, b) {
976
+ console.log('Calculating:', a, b);
977
+ const result = a + b;
978
+ console.log('Result:', result);
979
+ return result;
980
+ }
981
+
982
+ function helper() {
983
+ return 42;
984
+ }`;
985
+
986
+ // Using overwrite mode
987
+ const expectedFinalContent = computeExpectedFinalContent(
988
+ currentContent,
989
+ codeEdit,
990
+ true, // overwriteFile
991
+ );
992
+
993
+ console.log("[Prod Flow] After overwrite:");
994
+ console.log(expectedFinalContent);
995
+
996
+ // Render diff
997
+ const instance = renderWithTheme(
998
+ <DiffPreview
999
+ filePath="calc.js"
1000
+ originalContent={currentContent}
1001
+ newContent={expectedFinalContent}
1002
+ />,
1003
+ );
1004
+
1005
+ const { plainText } = displayTUI("PROD FLOW: Function replacement", instance);
1006
+
1007
+ // Should show the old simple return being removed
1008
+ expect(plainText).toContain("return a + b");
1009
+ // Should show the new console.log being added
1010
+ expect(plainText).toContain("console.log");
1011
+ expect(plainText).toContain("Calculating");
1012
+ });
1013
+
1014
+ it("PRODUCTION FLOW: startLine/endLine edit should only affect specified lines", () => {
1015
+ const currentContent = `// Config file
1016
+ const DEBUG = false;
1017
+ const API_URL = 'http://localhost:3000';
1018
+ const TIMEOUT = 5000;
1019
+ // End config`;
1020
+
1021
+ // Only change line 3 (API_URL)
1022
+ const codeEdit = `const API_URL = 'https://api.prod.com';`;
1023
+ const startLine = 3;
1024
+ const endLine = 3;
1025
+
1026
+ const expectedFinalContent = computeExpectedFinalContent(
1027
+ currentContent,
1028
+ codeEdit,
1029
+ false,
1030
+ startLine,
1031
+ endLine,
1032
+ );
1033
+
1034
+ console.log("[Prod Flow] After line 3 replacement:");
1035
+ console.log(expectedFinalContent);
1036
+
1037
+ // Verify only API_URL changed
1038
+ expect(expectedFinalContent).toContain("DEBUG = false"); // unchanged
1039
+ expect(expectedFinalContent).toContain("TIMEOUT = 5000"); // unchanged
1040
+ expect(expectedFinalContent).toContain("api.prod.com"); // changed
1041
+ expect(expectedFinalContent).not.toContain("localhost"); // old value gone
1042
+
1043
+ // Render diff
1044
+ const instance = renderWithTheme(
1045
+ <DiffPreview
1046
+ filePath="config.js"
1047
+ originalContent={currentContent}
1048
+ newContent={expectedFinalContent}
1049
+ />,
1050
+ );
1051
+
1052
+ const { plainText } = displayTUI("PROD FLOW: Single line change", instance);
1053
+
1054
+ // Should show localhost being removed
1055
+ expect(plainText).toContain("localhost");
1056
+ expect(plainText).toContain("-");
1057
+ // Should show prod.com being added
1058
+ expect(plainText).toContain("prod.com");
1059
+ expect(plainText).toContain("+");
1060
+ });
1061
+
1062
+ it("PRODUCTION FLOW: multi-line insertion should show all added lines", () => {
1063
+ const currentContent = `class User {
1064
+ constructor(name) {
1065
+ this.name = name;
1066
+ }
1067
+ }`;
1068
+
1069
+ // Insert logging methods
1070
+ const codeEdit = ` constructor(name) {
1071
+ console.log('Creating user:', name);
1072
+ this.name = name;
1073
+ this.createdAt = new Date();
1074
+ }
1075
+
1076
+ log() {
1077
+ console.log('User:', this.name, 'created at:', this.createdAt);
1078
+ }`;
1079
+
1080
+ const startLine = 2;
1081
+ const endLine = 4;
1082
+
1083
+ const expectedFinalContent = computeExpectedFinalContent(
1084
+ currentContent,
1085
+ codeEdit,
1086
+ false,
1087
+ startLine,
1088
+ endLine,
1089
+ );
1090
+
1091
+ console.log("[Prod Flow] After multi-line insertion:");
1092
+ console.log(expectedFinalContent);
1093
+
1094
+ // Render diff
1095
+ const instance = renderWithTheme(
1096
+ <DiffPreview
1097
+ filePath="User.js"
1098
+ originalContent={currentContent}
1099
+ newContent={expectedFinalContent}
1100
+ />,
1101
+ );
1102
+
1103
+ const { plainText } = displayTUI("PROD FLOW: Multi-line insertion", instance);
1104
+
1105
+ // Should show new log method
1106
+ expect(plainText).toContain("log()");
1107
+ expect(plainText).toContain("createdAt");
1108
+ // Should have multiple + lines
1109
+ const plusLines = plainText.split("\n").filter((line) => line.includes("+"));
1110
+ expect(plusLines.length).toBeGreaterThan(3);
1111
+ });
1112
+
1113
+ it("PRODUCTION REGRESSION: diff must match actual file change", () => {
1114
+ /**
1115
+ * This is the CRITICAL test. It verifies that:
1116
+ * 1. The content calculation produces the expected result
1117
+ * 2. The diff shown matches that result
1118
+ * 3. If you apply the shown diff to the original, you get the expected result
1119
+ *
1120
+ * If this test passes but production fails, there's a bug in the test.
1121
+ * If this test fails, we've caught a production bug.
1122
+ */
1123
+ const currentContent = `export function greet(name: string): string {
1124
+ return \`Hello, \${name}!\`;
1125
+ }
1126
+
1127
+ export function farewell(name: string): string {
1128
+ return \`Goodbye, \${name}!\`;
1129
+ }`;
1130
+
1131
+ // Simulate adding console.log to both functions
1132
+ const codeEdit = `export function greet(name: string): string {
1133
+ console.log('[DEBUG] greet called with:', name);
1134
+ return \`Hello, \${name}!\`;
1135
+ }
1136
+
1137
+ export function farewell(name: string): string {
1138
+ console.log('[DEBUG] farewell called with:', name);
1139
+ return \`Goodbye, \${name}!\`;
1140
+ }`;
1141
+
1142
+ // Full file replacement
1143
+ const expectedFinalContent = computeExpectedFinalContent(currentContent, codeEdit, true);
1144
+
1145
+ // VERIFY: The calculated content is exactly what we expect
1146
+ expect(expectedFinalContent).toBe(codeEdit);
1147
+ expect(expectedFinalContent).toContain("[DEBUG] greet called");
1148
+ expect(expectedFinalContent).toContain("[DEBUG] farewell called");
1149
+
1150
+ // Render diff
1151
+ const instance = renderWithTheme(
1152
+ <DiffPreview
1153
+ filePath="greetings.ts"
1154
+ originalContent={currentContent}
1155
+ newContent={expectedFinalContent}
1156
+ />,
1157
+ );
1158
+
1159
+ const { plainText } = displayTUI("REGRESSION TEST: Diff must match file change", instance);
1160
+
1161
+ // VERIFY: Both DEBUG logs appear in diff
1162
+ expect(plainText).toContain("[DEBUG] greet called");
1163
+ expect(plainText).toContain("[DEBUG] farewell called");
1164
+
1165
+ // VERIFY: + indicators show these are additions
1166
+ const debugLines = plainText.split("\n").filter((line) => line.includes("[DEBUG]"));
1167
+ expect(debugLines.length).toBe(2);
1168
+ debugLines.forEach((line) => {
1169
+ expect(line).toContain("+");
1170
+ });
1171
+
1172
+ // VERIFY: Original content (without DEBUG) shown as removed
1173
+ // The original return lines should still be present but unchanged
1174
+ expect(plainText).toContain("Hello");
1175
+ expect(plainText).toContain("Goodbye");
1176
+
1177
+ console.log("[REGRESSION TEST] ✓ Diff matches expected changes");
1178
+ });
1179
+ });
1180
+
1181
+ /**
1182
+ * =============================================================================
1183
+ * EditToolPreview COMPONENT TESTS - ACTUAL PRODUCTION COMPONENT
1184
+ * =============================================================================
1185
+ * These tests render the ACTUAL EditToolPreview component used in production.
1186
+ * This catches bugs where:
1187
+ * - TUI claims to make changes but diff shows nothing
1188
+ * - TUI shows wrong diff for the tool request
1189
+ * - codeEdit doesn't appear in the diff
1190
+ * =============================================================================
1191
+ */
1192
+
1193
+ /**
1194
+ * Helper to render MockEditToolPreview - simulates what user sees
1195
+ */
1196
+ function renderEditToolPreview(params: {
1197
+ targetFile: string;
1198
+ codeEdit: string;
1199
+ currentFileContent?: string;
1200
+ fileExists?: boolean;
1201
+ overwriteFile?: boolean;
1202
+ startLine?: number;
1203
+ endLine?: number;
1204
+ instructions?: string;
1205
+ }) {
1206
+ return renderWithTheme(
1207
+ <MockEditToolPreview
1208
+ targetFile={params.targetFile}
1209
+ codeEdit={params.codeEdit}
1210
+ currentFileContent={params.currentFileContent}
1211
+ fileExists={params.fileExists}
1212
+ overwriteFile={params.overwriteFile}
1213
+ startLine={params.startLine}
1214
+ endLine={params.endLine}
1215
+ instructions={params.instructions}
1216
+ />,
1217
+ );
1218
+ }
1219
+ void renderEditToolPreview; // Keep helper for future use
1220
+
1221
+ describe.concurrent("EditToolPreview - TUI Display Matches Actual Changes", () => {
1222
+ afterEach(() => {
1223
+ cleanup();
1224
+ });
1225
+
1226
+ /**
1227
+ * CRITICAL TEST: This is what the user sees in TUI.
1228
+ * If codeEdit contains "console.log", the diff MUST show "console.log"
1229
+ */
1230
+ it("TUI MUST show codeEdit content in diff - adding console.log", () => {
1231
+ const currentFile = `function hello() {
1232
+ return 'world';
1233
+ }`;
1234
+ const codeEdit = `function hello() {
1235
+ console.log('hello called');
1236
+ return 'world';
1237
+ }`;
1238
+
1239
+ const toolData = createMockToolPermission({
1240
+ targetFile: "/path/to/file.js",
1241
+ codeEdit: codeEdit,
1242
+ currentFileContent: currentFile,
1243
+ fileExists: true,
1244
+ overwriteFile: true,
1245
+ });
1246
+
1247
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1248
+
1249
+ const { plainText } = displayTUI("TUI TEST: EditToolPreview output", instance);
1250
+
1251
+ // CRITICAL: The codeEdit content MUST appear in the TUI
1252
+ expect(plainText).toContain("console.log");
1253
+ expect(plainText).toContain("hello called");
1254
+
1255
+ // Should show it's being added (+ indicator)
1256
+ expect(plainText).toContain("+");
1257
+
1258
+ console.log("[TUI TEST] ✓ codeEdit content visible in TUI diff");
1259
+ });
1260
+
1261
+ it("TUI MUST show the file path being edited", () => {
1262
+ const toolData = createMockToolPermission({
1263
+ targetFile: "/Users/dev/project/src/utils/helper.js",
1264
+ codeEdit: "const x = 1;",
1265
+ currentFileContent: "",
1266
+ fileExists: false,
1267
+ });
1268
+
1269
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1270
+
1271
+ const { plainText } = displayTUI("TUI TEST: File path display", instance);
1272
+
1273
+ // Should show the file path (possibly truncated)
1274
+ expect(plainText).toContain("helper.js");
1275
+ });
1276
+
1277
+ it("TUI MUST show correct operation type - Create for new file", () => {
1278
+ const toolData = createMockToolPermission({
1279
+ targetFile: "/path/to/newfile.js",
1280
+ codeEdit: 'console.log("new file");',
1281
+ currentFileContent: undefined,
1282
+ fileExists: false,
1283
+ });
1284
+
1285
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1286
+
1287
+ const { plainText } = displayTUI("TUI TEST: New file operation", instance);
1288
+
1289
+ // Should indicate this is creating a new file
1290
+ expect(plainText).toContain("Create");
1291
+ // New file content should be visible
1292
+ expect(plainText).toContain("console.log");
1293
+ });
1294
+
1295
+ it("TUI MUST show correct operation type - Edit for existing file", () => {
1296
+ const toolData = createMockToolPermission({
1297
+ targetFile: "/path/to/existing.js",
1298
+ codeEdit: "const modified = true;",
1299
+ currentFileContent: "const original = false;",
1300
+ fileExists: true,
1301
+ startLine: 1,
1302
+ endLine: 1,
1303
+ });
1304
+
1305
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1306
+
1307
+ const { plainText } = displayTUI("TUI TEST: Edit operation", instance);
1308
+
1309
+ // Should indicate this is editing
1310
+ expect(plainText).toContain("Edit");
1311
+ });
1312
+
1313
+ it("TUI diff MUST match startLine/endLine replacement", () => {
1314
+ const currentFile = `line 1
1315
+ line 2 - will be replaced
1316
+ line 3 - will be replaced
1317
+ line 4`;
1318
+
1319
+ const codeEdit = `NEW LINE 2
1320
+ NEW LINE 3`;
1321
+
1322
+ const toolData = createMockToolPermission({
1323
+ targetFile: "/path/to/file.txt",
1324
+ codeEdit: codeEdit,
1325
+ currentFileContent: currentFile,
1326
+ fileExists: true,
1327
+ startLine: 2,
1328
+ endLine: 3,
1329
+ });
1330
+
1331
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1332
+
1333
+ const { plainText } = displayTUI("TUI TEST: startLine/endLine replacement", instance);
1334
+
1335
+ // Old content should be marked as removed
1336
+ expect(plainText).toContain("will be replaced");
1337
+ expect(plainText).toContain("-");
1338
+
1339
+ // New content should be marked as added
1340
+ expect(plainText).toContain("NEW LINE 2");
1341
+ expect(plainText).toContain("NEW LINE 3");
1342
+ expect(plainText).toContain("+");
1343
+
1344
+ // Unchanged lines should still be present
1345
+ expect(plainText).toContain("line 1");
1346
+ expect(plainText).toContain("line 4");
1347
+ });
1348
+
1349
+ it("BUG CHECK: codeEdit with console.log MUST appear in diff, not be hidden", () => {
1350
+ /**
1351
+ * This test catches a specific bug where:
1352
+ * - Agent says "I'll add console.log for debugging"
1353
+ * - codeEdit contains console.log
1354
+ * - But diff shows nothing or wrong content
1355
+ */
1356
+ const currentFile = `export async function fetchData(url) {
1357
+ const response = await fetch(url);
1358
+ return response.json();
1359
+ }`;
1360
+
1361
+ const codeEdit = `export async function fetchData(url) {
1362
+ console.log('[DEBUG] Fetching:', url);
1363
+ const response = await fetch(url);
1364
+ console.log('[DEBUG] Response status:', response.status);
1365
+ return response.json();
1366
+ }`;
1367
+
1368
+ const toolData = createMockToolPermission({
1369
+ targetFile: "/src/api.ts",
1370
+ codeEdit: codeEdit,
1371
+ currentFileContent: currentFile,
1372
+ fileExists: true,
1373
+ overwriteFile: true,
1374
+ });
1375
+
1376
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1377
+
1378
+ const { plainText } = displayTUI("BUG CHECK: Console.log visibility", instance);
1379
+
1380
+ // BOTH console.log statements MUST be visible
1381
+ expect(plainText).toContain("[DEBUG] Fetching:");
1382
+ expect(plainText).toContain("[DEBUG] Response status:");
1383
+
1384
+ // They should be shown as additions
1385
+ const lines = plainText.split("\n");
1386
+ const debugLines = lines.filter((line) => line.includes("[DEBUG]"));
1387
+
1388
+ console.log("[BUG CHECK] Found DEBUG lines:", debugLines.length);
1389
+ expect(debugLines.length).toBe(2);
1390
+
1391
+ // Each DEBUG line should have + indicator
1392
+ debugLines.forEach((line, idx) => {
1393
+ console.log(`[BUG CHECK] DEBUG line ${idx + 1}: "${line}"`);
1394
+ expect(line).toContain("+");
1395
+ });
1396
+
1397
+ console.log("[BUG CHECK] ✓ All console.log statements visible in diff");
1398
+ });
1399
+
1400
+ it('BUG CHECK: empty codeEdit should show "No changes detected"', () => {
1401
+ const currentFile = "const x = 1;";
1402
+
1403
+ const toolData = createMockToolPermission({
1404
+ targetFile: "/path/to/file.js",
1405
+ codeEdit: currentFile, // Same content = no changes
1406
+ currentFileContent: currentFile,
1407
+ fileExists: true,
1408
+ overwriteFile: true,
1409
+ });
1410
+
1411
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1412
+
1413
+ const { plainText } = displayTUI("BUG CHECK: No changes test", instance);
1414
+
1415
+ // Should indicate no changes
1416
+ expect(plainText).toContain("No changes detected");
1417
+ });
1418
+
1419
+ it("BUG CHECK: large codeEdit should not be truncated in diff", () => {
1420
+ const currentFile = "const x = 1;";
1421
+
1422
+ // Large codeEdit with many lines
1423
+ const codeEdit = Array.from(
1424
+ { length: 20 },
1425
+ (_, i) => `console.log('Line ${i + 1}: debugging step ${i + 1}');`,
1426
+ ).join("\n");
1427
+
1428
+ const toolData = createMockToolPermission({
1429
+ targetFile: "/path/to/debug.js",
1430
+ codeEdit: codeEdit,
1431
+ currentFileContent: currentFile,
1432
+ fileExists: true,
1433
+ overwriteFile: true,
1434
+ });
1435
+
1436
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1437
+
1438
+ const { plainText } = displayTUI("BUG CHECK: Large codeEdit (no truncation)", instance);
1439
+
1440
+ // First and last lines should be visible
1441
+ expect(plainText).toContain("Line 1");
1442
+ expect(plainText).toContain("Line 20");
1443
+
1444
+ // Count how many "debugging step" lines are shown
1445
+ const debugStepMatches = plainText.match(/debugging step/g) || [];
1446
+ console.log("[BUG CHECK] Lines visible:", debugStepMatches.length);
1447
+
1448
+ // Should show all 20 lines (or at least indicate with separators)
1449
+ // Even if collapsed, we should see multiple lines
1450
+ expect(debugStepMatches.length).toBeGreaterThan(0);
1451
+ });
1452
+
1453
+ it("REGRESSION: TUI diff must show EXACT codeEdit, not computed content", () => {
1454
+ /**
1455
+ * This catches a bug where the diff shows something different
1456
+ * from what codeEdit actually contains.
1457
+ *
1458
+ * The diff shown MUST contain the exact strings from codeEdit.
1459
+ */
1460
+ const currentFile = `function test() {
1461
+ // old implementation
1462
+ return null;
1463
+ }`;
1464
+
1465
+ // Very specific codeEdit with identifiable strings
1466
+ const codeEdit = `function test() {
1467
+ // MARKER_ABC_123
1468
+ console.log('UNIQUE_STRING_XYZ');
1469
+ return { status: 'SPECIAL_VALUE_789' };
1470
+ }`;
1471
+
1472
+ const toolData = createMockToolPermission({
1473
+ targetFile: "/test/regression.js",
1474
+ codeEdit: codeEdit,
1475
+ currentFileContent: currentFile,
1476
+ fileExists: true,
1477
+ overwriteFile: true,
1478
+ });
1479
+
1480
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1481
+
1482
+ const { plainText } = displayTUI("REGRESSION: Exact codeEdit must appear", instance);
1483
+
1484
+ // ALL unique markers from codeEdit MUST appear in diff
1485
+ expect(plainText).toContain("MARKER_ABC_123");
1486
+ expect(plainText).toContain("UNIQUE_STRING_XYZ");
1487
+ expect(plainText).toContain("SPECIAL_VALUE_789");
1488
+ });
1489
+ });
1490
+
1491
+ /**
1492
+ * =============================================================================
1493
+ * TRUE END-TO-END TESTS - REAL FILE OPERATIONS
1494
+ * =============================================================================
1495
+ * These tests create REAL files on disk, apply edits, and verify:
1496
+ * 1. What the TUI diff shows
1497
+ * 2. What actually gets written to disk
1498
+ * 3. That #1 and #2 MATCH
1499
+ *
1500
+ * This catches the critical bug where TUI shows one diff but the actual
1501
+ * file change is different.
1502
+ * =============================================================================
1503
+ */
1504
+
1505
+ describe.sequential("TRUE E2E: Diff Preview vs Actual File Write", () => {
1506
+ let testDir: string;
1507
+
1508
+ beforeEach(async () => {
1509
+ // Create a unique temp directory for each test
1510
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), "diff-preview-e2e-"));
1511
+ console.log(`[E2E] Test directory: ${testDir}`);
1512
+ });
1513
+
1514
+ afterEach(async () => {
1515
+ cleanup();
1516
+ // Clean up temp directory
1517
+ try {
1518
+ await fs.rm(testDir, { recursive: true, force: true });
1519
+ } catch {
1520
+ // Ignore cleanup errors
1521
+ }
1522
+ });
1523
+
1524
+ /**
1525
+ * Simulates what happens when user approves an edit:
1526
+ * 1. Apply the edit using the same logic as computeExpectedFinalContent
1527
+ * 2. Write to disk
1528
+ * 3. Return what was written
1529
+ */
1530
+ async function applyEditToFile(
1531
+ filePath: string,
1532
+ currentContent: string | undefined,
1533
+ codeEdit: string,
1534
+ overwriteFile: boolean,
1535
+ startLine?: number,
1536
+ endLine?: number,
1537
+ instructions?: string,
1538
+ ): Promise<string> {
1539
+ let finalContent: string;
1540
+
1541
+ if (currentContent === undefined) {
1542
+ // New file
1543
+ finalContent = codeEdit;
1544
+ } else if (overwriteFile) {
1545
+ finalContent = codeEdit;
1546
+ } else if (startLine !== undefined && endLine !== undefined) {
1547
+ // Line range replacement - SAME logic as computeExpectedFinalContent
1548
+ const lines = currentContent.split("\n");
1549
+ const totalLines = lines.length;
1550
+ const validStartLine = Math.max(1, Math.min(startLine, totalLines));
1551
+ const validEndLine = Math.max(validStartLine, Math.min(endLine, totalLines));
1552
+ const beforeLines = lines.slice(0, validStartLine - 1);
1553
+ const afterLines = lines.slice(validEndLine);
1554
+ const newLines = codeEdit.split("\n");
1555
+ finalContent = [...beforeLines, ...newLines, ...afterLines].join("\n");
1556
+ } else if (instructions) {
1557
+ finalContent = currentContent + "\n" + codeEdit;
1558
+ } else {
1559
+ // 80% rule and includes check
1560
+ const codeEditLines = codeEdit.split("\n");
1561
+ const currentLines = currentContent.split("\n");
1562
+ if (codeEditLines.length >= currentLines.length * 0.8) {
1563
+ finalContent = codeEdit;
1564
+ } else if (currentContent.includes(codeEdit)) {
1565
+ finalContent = currentContent;
1566
+ } else {
1567
+ finalContent = currentContent + "\n" + codeEdit;
1568
+ }
1569
+ }
1570
+
1571
+ // Write to disk
1572
+ await fs.writeFile(filePath, finalContent, "utf-8");
1573
+ return finalContent;
1574
+ }
1575
+
1576
+ /**
1577
+ * Verifies the diff preview matches what was actually written to disk
1578
+ */
1579
+ function verifyDiffMatchesFileContent(
1580
+ diffPlainText: string,
1581
+ originalContent: string | undefined,
1582
+ actualFileContent: string,
1583
+ testName: string,
1584
+ ) {
1585
+ console.log(`\n[E2E VERIFY] ${testName}`);
1586
+ console.log("[E2E] Original content length:", originalContent?.length ?? 0);
1587
+ console.log("[E2E] Actual file content length:", actualFileContent.length);
1588
+
1589
+ // Find all lines marked as added in the diff (+ lines)
1590
+ const diffAddedLines = diffPlainText
1591
+ .split("\n")
1592
+ .filter((line) => line.trimStart().startsWith("+"))
1593
+ .map((line) => line.replace(/^\s*\+\s*/, "").trim())
1594
+ .filter((line) => line.length > 0);
1595
+
1596
+ // Find all lines marked as removed in the diff (- lines)
1597
+ const diffRemovedLines = diffPlainText
1598
+ .split("\n")
1599
+ .filter((line) => line.trimStart().startsWith("-"))
1600
+ .map((line) => line.replace(/^\s*-\s*/, "").trim())
1601
+ .filter((line) => line.length > 0);
1602
+
1603
+ console.log("[E2E] Diff shows added lines:", diffAddedLines.length);
1604
+ console.log("[E2E] Diff shows removed lines:", diffRemovedLines.length);
1605
+
1606
+ // Verify: added lines should be in the actual file
1607
+ for (const addedLine of diffAddedLines) {
1608
+ if (addedLine.length > 2) {
1609
+ // Skip very short lines that might be ambiguous
1610
+ const isInFile = actualFileContent.includes(addedLine);
1611
+ console.log(
1612
+ `[E2E] Checking added line in file: "${addedLine.substring(0, 50)}..." - ${isInFile ? "✓" : "✗"}`,
1613
+ );
1614
+ expect(isInFile).toBe(true);
1615
+ }
1616
+ }
1617
+
1618
+ // Verify: removed lines should NOT be in the actual file (unless they appear elsewhere)
1619
+ if (originalContent) {
1620
+ for (const removedLine of diffRemovedLines) {
1621
+ if (removedLine.length > 5 && !diffAddedLines.some((a) => a.includes(removedLine))) {
1622
+ // Only check unique removals (not modifications)
1623
+ const wasInOriginal = originalContent.includes(removedLine);
1624
+ if (wasInOriginal) {
1625
+ const stillInFile = actualFileContent.includes(removedLine);
1626
+ console.log(
1627
+ `[E2E] Checking removed line NOT in file: "${removedLine.substring(0, 50)}..." - ${!stillInFile ? "✓" : "⚠ still present"}`,
1628
+ );
1629
+ }
1630
+ }
1631
+ }
1632
+ }
1633
+
1634
+ console.log(`[E2E VERIFY] ${testName} - PASSED ✓`);
1635
+ }
1636
+
1637
+ it("E2E: New file creation - diff matches disk", async () => {
1638
+ const testFile = path.join(testDir, "new-file.js");
1639
+ const codeEdit = `// New file created by test
1640
+ function hello() {
1641
+ console.log('Hello, world!');
1642
+ return 42;
1643
+ }
1644
+
1645
+ module.exports = { hello };`;
1646
+
1647
+ // Step 1: Render the diff preview (what user sees)
1648
+ const toolData = createMockToolPermission({
1649
+ targetFile: testFile,
1650
+ codeEdit: codeEdit,
1651
+ currentFileContent: undefined,
1652
+ fileExists: false,
1653
+ });
1654
+
1655
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1656
+
1657
+ const { plainText } = displayTUI("E2E: New file creation diff", instance);
1658
+
1659
+ // Step 2: Apply the edit (what happens when user approves)
1660
+ const writtenContent = await applyEditToFile(testFile, undefined, codeEdit, false);
1661
+
1662
+ // Step 3: Read back from disk
1663
+ const actualContent = await fs.readFile(testFile, "utf-8");
1664
+
1665
+ // Step 4: Verify diff matches actual file
1666
+ expect(actualContent).toBe(writtenContent);
1667
+ expect(actualContent).toBe(codeEdit);
1668
+
1669
+ // Step 5: Verify diff shows what was actually written
1670
+ expect(plainText).toContain("console.log");
1671
+ expect(plainText).toContain("Hello, world!");
1672
+ expect(plainText).toContain("hello");
1673
+
1674
+ verifyDiffMatchesFileContent(plainText, undefined, actualContent, "New file creation");
1675
+
1676
+ console.log("[E2E] ✓ New file creation: diff matches actual file content");
1677
+ });
1678
+
1679
+ it("E2E: Overwrite file - diff matches disk", async () => {
1680
+ const testFile = path.join(testDir, "overwrite.js");
1681
+
1682
+ // Create original file
1683
+ const originalContent = `function old() {
1684
+ return 'old implementation';
1685
+ }`;
1686
+ await fs.writeFile(testFile, originalContent, "utf-8");
1687
+
1688
+ // New content to overwrite
1689
+ const codeEdit = `function new() {
1690
+ console.log('NEW implementation');
1691
+ return 'new implementation';
1692
+ }`;
1693
+
1694
+ // Step 1: Render the diff preview
1695
+ const toolData = createMockToolPermission({
1696
+ targetFile: testFile,
1697
+ codeEdit: codeEdit,
1698
+ currentFileContent: originalContent,
1699
+ fileExists: true,
1700
+ overwriteFile: true,
1701
+ });
1702
+
1703
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1704
+
1705
+ const { plainText } = displayTUI("E2E: Overwrite file diff", instance);
1706
+
1707
+ // Step 2: Apply the edit
1708
+ const writtenContent = await applyEditToFile(testFile, originalContent, codeEdit, true);
1709
+
1710
+ // Step 3: Read back from disk
1711
+ const actualContent = await fs.readFile(testFile, "utf-8");
1712
+
1713
+ // Step 4: Verify
1714
+ expect(actualContent).toBe(writtenContent);
1715
+ expect(actualContent).toBe(codeEdit);
1716
+ expect(actualContent).not.toContain("old implementation");
1717
+ expect(actualContent).toContain("new implementation");
1718
+
1719
+ // Diff should show both old and new
1720
+ expect(plainText).toContain("old implementation");
1721
+ expect(plainText).toContain("new implementation");
1722
+ expect(plainText).toContain("-"); // removal
1723
+ expect(plainText).toContain("+"); // addition
1724
+
1725
+ verifyDiffMatchesFileContent(plainText, originalContent, actualContent, "Overwrite file");
1726
+
1727
+ console.log("[E2E] ✓ Overwrite: diff matches actual file content");
1728
+ });
1729
+
1730
+ it("E2E: Line range replacement - diff matches disk", async () => {
1731
+ const testFile = path.join(testDir, "line-range.js");
1732
+
1733
+ // Create original file with 5 lines
1734
+ const originalContent = `// Line 1: Header
1735
+ function process(data) { // Line 2
1736
+ return data; // Line 3
1737
+ } // Line 4
1738
+ // Line 5: Footer`;
1739
+ await fs.writeFile(testFile, originalContent, "utf-8");
1740
+
1741
+ // Replace lines 2-4 (the function)
1742
+ const codeEdit = `function process(data) {
1743
+ console.log('PROCESSING:', data);
1744
+ const result = transform(data);
1745
+ console.log('RESULT:', result);
1746
+ return result;
1747
+ }`;
1748
+
1749
+ // Step 1: Render the diff preview
1750
+ const toolData = createMockToolPermission({
1751
+ targetFile: testFile,
1752
+ codeEdit: codeEdit,
1753
+ currentFileContent: originalContent,
1754
+ fileExists: true,
1755
+ startLine: 2,
1756
+ endLine: 4,
1757
+ });
1758
+
1759
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1760
+
1761
+ const { plainText } = displayTUI("E2E: Line range replacement diff", instance);
1762
+
1763
+ // Step 2: Apply the edit
1764
+ const writtenContent = await applyEditToFile(testFile, originalContent, codeEdit, false, 2, 4);
1765
+
1766
+ // Step 3: Read back from disk
1767
+ const actualContent = await fs.readFile(testFile, "utf-8");
1768
+
1769
+ // Step 4: Verify structure
1770
+ expect(actualContent).toBe(writtenContent);
1771
+ expect(actualContent).toContain("// Line 1: Header"); // unchanged
1772
+ expect(actualContent).toContain("// Line 5: Footer"); // unchanged
1773
+ expect(actualContent).toContain("PROCESSING:"); // new
1774
+ expect(actualContent).toContain("RESULT:"); // new
1775
+
1776
+ // Diff should show the changes
1777
+ expect(plainText).toContain("PROCESSING");
1778
+ expect(plainText).toContain("+");
1779
+
1780
+ verifyDiffMatchesFileContent(
1781
+ plainText,
1782
+ originalContent,
1783
+ actualContent,
1784
+ "Line range replacement",
1785
+ );
1786
+
1787
+ console.log("[E2E] ✓ Line range replacement: diff matches actual file content");
1788
+ });
1789
+
1790
+ it("E2E: Add console.log statements - diff matches disk", async () => {
1791
+ const testFile = path.join(testDir, "add-console-log.js");
1792
+
1793
+ // Create original file
1794
+ const originalContent = `async function fetchUser(id) {
1795
+ const response = await fetch('/api/users/' + id);
1796
+ const user = await response.json();
1797
+ return user;
1798
+ }`;
1799
+ await fs.writeFile(testFile, originalContent, "utf-8");
1800
+
1801
+ // New content with console.log statements
1802
+ const codeEdit = `async function fetchUser(id) {
1803
+ console.log('[DEBUG] Fetching user:', id);
1804
+ const response = await fetch('/api/users/' + id);
1805
+ console.log('[DEBUG] Response status:', response.status);
1806
+ const user = await response.json();
1807
+ console.log('[DEBUG] User data:', user);
1808
+ return user;
1809
+ }`;
1810
+
1811
+ // Step 1: Render the diff preview
1812
+ const toolData = createMockToolPermission({
1813
+ targetFile: testFile,
1814
+ codeEdit: codeEdit,
1815
+ currentFileContent: originalContent,
1816
+ fileExists: true,
1817
+ overwriteFile: true,
1818
+ });
1819
+
1820
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1821
+
1822
+ const { plainText } = displayTUI("E2E: Add console.log diff", instance);
1823
+
1824
+ // Step 2: Apply the edit
1825
+ const writtenContent = await applyEditToFile(testFile, originalContent, codeEdit, true);
1826
+
1827
+ // Step 3: Read back from disk
1828
+ const actualContent = await fs.readFile(testFile, "utf-8");
1829
+
1830
+ // Step 4: Verify
1831
+ expect(actualContent).toBe(writtenContent);
1832
+
1833
+ // All 3 console.log statements should be in the file
1834
+ expect(actualContent).toContain("[DEBUG] Fetching user:");
1835
+ expect(actualContent).toContain("[DEBUG] Response status:");
1836
+ expect(actualContent).toContain("[DEBUG] User data:");
1837
+
1838
+ // All 3 should appear in the diff
1839
+ expect(plainText).toContain("[DEBUG] Fetching user:");
1840
+ expect(plainText).toContain("[DEBUG] Response status:");
1841
+ expect(plainText).toContain("[DEBUG] User data:");
1842
+
1843
+ // Count console.log in diff vs file
1844
+ const fileConsoleLogCount = (actualContent.match(/console\.log/g) || []).length;
1845
+ const diffConsoleLogCount = (plainText.match(/console\.log/g) || []).length;
1846
+
1847
+ console.log("[E2E] console.log in file:", fileConsoleLogCount);
1848
+ console.log("[E2E] console.log shown in diff:", diffConsoleLogCount);
1849
+
1850
+ // Diff should show at least as many as are in the file
1851
+ expect(diffConsoleLogCount).toBeGreaterThanOrEqual(fileConsoleLogCount);
1852
+
1853
+ verifyDiffMatchesFileContent(
1854
+ plainText,
1855
+ originalContent,
1856
+ actualContent,
1857
+ "Add console.log statements",
1858
+ );
1859
+
1860
+ console.log("[E2E] ✓ Console.log addition: diff matches actual file content");
1861
+ });
1862
+
1863
+ it("E2E: Multiple files in sequence - each diff matches its disk write", async () => {
1864
+ // Test that sequential edits each produce correct diffs
1865
+
1866
+ // File 1
1867
+ const file1 = path.join(testDir, "file1.js");
1868
+ const content1 = "const a = 1;";
1869
+ const edit1 = "const a = 1;\nconst b = 2;";
1870
+
1871
+ // File 2
1872
+ const file2 = path.join(testDir, "file2.js");
1873
+ const content2 = "function x() { return 1; }";
1874
+ const edit2 = 'function x() { console.log("x"); return 1; }';
1875
+
1876
+ // Create original files
1877
+ await fs.writeFile(file1, content1, "utf-8");
1878
+ await fs.writeFile(file2, content2, "utf-8");
1879
+
1880
+ // Edit file 1
1881
+ const toolData1 = createMockToolPermission({
1882
+ targetFile: file1,
1883
+ codeEdit: edit1,
1884
+ currentFileContent: content1,
1885
+ fileExists: true,
1886
+ overwriteFile: true,
1887
+ });
1888
+ const instance1 = renderWithTheme(<EditToolPreview queuedTool={toolData1} />);
1889
+ const { plainText: diff1 } = displayTUI("E2E: Sequential edit 1", instance1);
1890
+
1891
+ await applyEditToFile(file1, content1, edit1, true);
1892
+ const actualContent1 = await fs.readFile(file1, "utf-8");
1893
+
1894
+ cleanup();
1895
+
1896
+ // Edit file 2
1897
+ const toolData2 = createMockToolPermission({
1898
+ targetFile: file2,
1899
+ codeEdit: edit2,
1900
+ currentFileContent: content2,
1901
+ fileExists: true,
1902
+ overwriteFile: true,
1903
+ });
1904
+ const instance2 = renderWithTheme(<EditToolPreview queuedTool={toolData2} />);
1905
+ const { plainText: diff2 } = displayTUI("E2E: Sequential edit 2", instance2);
1906
+
1907
+ await applyEditToFile(file2, content2, edit2, true);
1908
+ const actualContent2 = await fs.readFile(file2, "utf-8");
1909
+
1910
+ // Verify both
1911
+ expect(actualContent1).toContain("const b = 2");
1912
+ expect(diff1).toContain("const b = 2");
1913
+
1914
+ expect(actualContent2).toContain('console.log("x")');
1915
+ expect(diff2).toContain("console.log");
1916
+
1917
+ console.log("[E2E] ✓ Sequential edits: each diff matches its file");
1918
+ });
1919
+
1920
+ it("E2E CRITICAL: Detect mismatch between diff and actual file write", async () => {
1921
+ /**
1922
+ * This test demonstrates how we catch the bug where diff shows
1923
+ * something different from what's written to disk.
1924
+ *
1925
+ * We intentionally create a mismatch scenario and verify our
1926
+ * detection catches it.
1927
+ */
1928
+ const testFile = path.join(testDir, "mismatch-test.js");
1929
+
1930
+ const originalContent = "const x = 1;";
1931
+ const codeEdit = "const x = 1;\nconst y = 2;"; // This is what diff should show
1932
+
1933
+ // Create file
1934
+ await fs.writeFile(testFile, originalContent, "utf-8");
1935
+
1936
+ // Render diff preview
1937
+ const toolData = createMockToolPermission({
1938
+ targetFile: testFile,
1939
+ codeEdit: codeEdit,
1940
+ currentFileContent: originalContent,
1941
+ fileExists: true,
1942
+ overwriteFile: true,
1943
+ });
1944
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1945
+ const { plainText } = displayTUI("E2E: Mismatch detection test", instance);
1946
+
1947
+ // Apply edit correctly
1948
+ await applyEditToFile(testFile, originalContent, codeEdit, true);
1949
+ const actualContent = await fs.readFile(testFile, "utf-8");
1950
+
1951
+ // VERIFY: Diff and file should match
1952
+ expect(actualContent).toBe(codeEdit);
1953
+ expect(plainText).toContain("const y = 2");
1954
+
1955
+ // This is the key assertion: what diff shows must be in the file
1956
+ const diffShowsY = plainText.includes("const y = 2");
1957
+ const fileHasY = actualContent.includes("const y = 2");
1958
+
1959
+ console.log('[E2E CRITICAL] Diff shows "const y = 2":', diffShowsY);
1960
+ console.log('[E2E CRITICAL] File has "const y = 2":', fileHasY);
1961
+
1962
+ // BOTH must be true for no mismatch
1963
+ expect(diffShowsY).toBe(true);
1964
+ expect(fileHasY).toBe(true);
1965
+ expect(diffShowsY).toBe(fileHasY);
1966
+
1967
+ console.log("[E2E] ✓ Mismatch detection: diff and file are synchronized");
1968
+ });
1969
+
1970
+ it("E2E: Verify the 80% rule produces correct diff and file content", async () => {
1971
+ const testFile = path.join(testDir, "80-percent-rule.js");
1972
+
1973
+ // Original: 5 lines
1974
+ const originalContent = `line 1
1975
+ line 2
1976
+ line 3
1977
+ line 4
1978
+ line 5`;
1979
+ await fs.writeFile(testFile, originalContent, "utf-8");
1980
+
1981
+ // codeEdit: 4 lines (80% of 5)
1982
+ const codeEdit = `NEW LINE 1
1983
+ NEW LINE 2
1984
+ NEW LINE 3
1985
+ NEW LINE 4`;
1986
+
1987
+ // This should trigger the 80% rule and replace entire content
1988
+ const toolData = createMockToolPermission({
1989
+ targetFile: testFile,
1990
+ codeEdit: codeEdit,
1991
+ currentFileContent: originalContent,
1992
+ fileExists: true,
1993
+ // No overwriteFile, no startLine/endLine - let 80% rule apply
1994
+ });
1995
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
1996
+ const { plainText } = displayTUI("E2E: 80% rule test", instance);
1997
+
1998
+ // Apply edit (80% rule should replace all)
1999
+ await applyEditToFile(testFile, originalContent, codeEdit, false);
2000
+ const actualContent = await fs.readFile(testFile, "utf-8");
2001
+
2002
+ // Verify 80% rule was applied (should be just the codeEdit)
2003
+ expect(actualContent).toBe(codeEdit);
2004
+ expect(actualContent).not.toContain("line 1"); // old content should be gone
2005
+
2006
+ // Diff should show this replacement
2007
+ expect(plainText).toContain("NEW LINE 1");
2008
+ expect(plainText).toContain("line 1"); // showing what was removed
2009
+
2010
+ verifyDiffMatchesFileContent(plainText, originalContent, actualContent, "80% rule");
2011
+
2012
+ console.log("[E2E] ✓ 80% rule: diff matches actual file replacement");
2013
+ });
2014
+
2015
+ it("E2E: Instructions parameter appends content correctly", async () => {
2016
+ const testFile = path.join(testDir, "instructions-append.js");
2017
+
2018
+ const originalContent = `// Existing file
2019
+ const config = { debug: false };`;
2020
+ await fs.writeFile(testFile, originalContent, "utf-8");
2021
+
2022
+ const codeEdit = `const logger = { log: console.log };`;
2023
+ const instructions = "Add a logger variable";
2024
+
2025
+ // With instructions, should append
2026
+ const toolData = createMockToolPermission({
2027
+ targetFile: testFile,
2028
+ codeEdit: codeEdit,
2029
+ currentFileContent: originalContent,
2030
+ fileExists: true,
2031
+ instructions: instructions,
2032
+ });
2033
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2034
+ const { plainText } = displayTUI("E2E: Instructions append test", instance);
2035
+
2036
+ // Apply edit (instructions mode)
2037
+ await applyEditToFile(
2038
+ testFile,
2039
+ originalContent,
2040
+ codeEdit,
2041
+ false,
2042
+ undefined,
2043
+ undefined,
2044
+ instructions,
2045
+ );
2046
+ const actualContent = await fs.readFile(testFile, "utf-8");
2047
+
2048
+ // Should contain both original and new content
2049
+ expect(actualContent).toContain("const config = { debug: false }");
2050
+ expect(actualContent).toContain("const logger = { log: console.log }");
2051
+
2052
+ // Diff should show the addition
2053
+ expect(plainText).toContain("logger");
2054
+ expect(plainText).toContain("+");
2055
+
2056
+ verifyDiffMatchesFileContent(plainText, originalContent, actualContent, "Instructions append");
2057
+
2058
+ console.log("[E2E] ✓ Instructions append: diff matches actual file");
2059
+ });
2060
+
2061
+ it("E2E FULL LIFECYCLE: Edit request -> TUI diff -> File write -> Verification", async () => {
2062
+ /**
2063
+ * This is the complete E2E test that simulates the full lifecycle:
2064
+ * 1. User's file exists with original content
2065
+ * 2. Agent sends edit tool request
2066
+ * 3. TUI shows diff preview to user
2067
+ * 4. User approves
2068
+ * 5. Edit is applied to file
2069
+ * 6. We verify diff matched the actual change
2070
+ */
2071
+ const testFile = path.join(testDir, "full-lifecycle.ts");
2072
+
2073
+ // STAGE 1: Original file on disk
2074
+ const originalContent = `import { useState } from 'react';
2075
+
2076
+ export function Counter() {
2077
+ const [count, setCount] = useState(0);
2078
+
2079
+ return (
2080
+ <div>
2081
+ <p>Count: {count}</p>
2082
+ <button onClick={() => setCount(count + 1)}>+</button>
2083
+ </div>
2084
+ );
2085
+ }`;
2086
+ await fs.writeFile(testFile, originalContent, "utf-8");
2087
+ console.log("[LIFECYCLE] Stage 1: Created original file");
2088
+
2089
+ // STAGE 2: Agent generates edit request
2090
+ const codeEdit = `import { useState, useEffect } from 'react';
2091
+
2092
+ export function Counter() {
2093
+ const [count, setCount] = useState(0);
2094
+
2095
+ useEffect(() => {
2096
+ console.log('[Counter] Count changed:', count);
2097
+ document.title = \`Count: \${count}\`;
2098
+ }, [count]);
2099
+
2100
+ return (
2101
+ <div>
2102
+ <p>Count: {count}</p>
2103
+ <button onClick={() => setCount(count + 1)}>+</button>
2104
+ <button onClick={() => setCount(0)}>Reset</button>
2105
+ </div>
2106
+ );
2107
+ }`;
2108
+ console.log("[LIFECYCLE] Stage 2: Agent prepared edit");
2109
+
2110
+ // STAGE 3: TUI renders diff preview
2111
+ const toolData = createMockToolPermission({
2112
+ targetFile: testFile,
2113
+ codeEdit: codeEdit,
2114
+ currentFileContent: originalContent,
2115
+ fileExists: true,
2116
+ overwriteFile: true,
2117
+ });
2118
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2119
+ const { plainText } = displayTUI("LIFECYCLE: Full E2E diff preview", instance);
2120
+ console.log("[LIFECYCLE] Stage 3: TUI rendered diff");
2121
+
2122
+ // STAGE 4: User approves (simulated)
2123
+ console.log("[LIFECYCLE] Stage 4: User approved edit");
2124
+
2125
+ // STAGE 5: Apply edit to file
2126
+ await applyEditToFile(testFile, originalContent, codeEdit, true);
2127
+ const actualContent = await fs.readFile(testFile, "utf-8");
2128
+ console.log("[LIFECYCLE] Stage 5: Edit applied to file");
2129
+
2130
+ // STAGE 6: Verification
2131
+ console.log("[LIFECYCLE] Stage 6: Verifying...");
2132
+
2133
+ // Check that all new features are in the file
2134
+ expect(actualContent).toContain("useEffect");
2135
+ expect(actualContent).toContain("[Counter] Count changed:");
2136
+ expect(actualContent).toContain("document.title");
2137
+ expect(actualContent).toContain("Reset");
2138
+
2139
+ // Check that all new features were shown in diff
2140
+ expect(plainText).toContain("useEffect");
2141
+ expect(plainText).toContain("[Counter] Count changed:");
2142
+ expect(plainText).toContain("Reset");
2143
+
2144
+ // Verify complete content match
2145
+ expect(actualContent).toBe(codeEdit);
2146
+
2147
+ verifyDiffMatchesFileContent(plainText, originalContent, actualContent, "Full lifecycle");
2148
+
2149
+ console.log("[LIFECYCLE] ✓ COMPLETE: All stages passed");
2150
+ console.log("[LIFECYCLE] ✓ Diff preview matched actual file write");
2151
+ console.log("[LIFECYCLE] ✓ User would have seen correct changes before approving");
2152
+ });
2153
+ });
2154
+
2155
+ /**
2156
+ * =============================================================================
2157
+ * STRICT CONTRACT TESTS - TUI OUTPUT IS A CONTRACT WITH THE USER
2158
+ * =============================================================================
2159
+ * These tests enforce that EVERYTHING the TUI claims MUST be true.
2160
+ * If TUI shows "+ console.log('hello')" then that MUST be in the file.
2161
+ * If TUI shows "- old code" then that MUST NOT be in the file.
2162
+ * If TUI claims 10 additions, there MUST be exactly 10 additions.
2163
+ *
2164
+ * These tests will FAIL if production has a bug where TUI lies to the user.
2165
+ * =============================================================================
2166
+ */
2167
+
2168
+ /**
2169
+ * Extracts all lines from TUI diff output that are marked as additions (+)
2170
+ * Returns the actual content (without the + prefix)
2171
+ */
2172
+ function extractAddedLinesFromTUI(tuiOutput: string): string[] {
2173
+ const lines = tuiOutput.split("\n");
2174
+ const addedLines: string[] = [];
2175
+
2176
+ for (const line of lines) {
2177
+ // Match lines that have + indicator (format: " N + content" or "N + content")
2178
+ // Skip lines that are just metadata like "No newline at end of file"
2179
+ const addMatch = line.match(/^\s*\d+\s*\+\s*(.+)$/);
2180
+ if (addMatch && addMatch[1]) {
2181
+ const content = addMatch[1].trim();
2182
+ // Skip metadata lines
2183
+ if (!content.includes("No newline at end of file") && !content.includes("unchanged lines")) {
2184
+ addedLines.push(content);
2185
+ }
2186
+ }
2187
+ }
2188
+
2189
+ return addedLines;
2190
+ }
2191
+
2192
+ /**
2193
+ * Extracts all lines from TUI diff output that are marked as removals (-)
2194
+ * Returns the actual content (without the - prefix)
2195
+ */
2196
+ function extractRemovedLinesFromTUI(tuiOutput: string): string[] {
2197
+ const lines = tuiOutput.split("\n");
2198
+ const removedLines: string[] = [];
2199
+
2200
+ for (const line of lines) {
2201
+ // Match lines that have - indicator (format: " N - content" or "N - content")
2202
+ const removeMatch = line.match(/^\s*\d+\s*-\s*(.+)$/);
2203
+ if (removeMatch && removeMatch[1]) {
2204
+ const content = removeMatch[1].trim();
2205
+ // Skip metadata lines
2206
+ if (!content.includes("No newline at end of file") && !content.includes("unchanged lines")) {
2207
+ removedLines.push(content);
2208
+ }
2209
+ }
2210
+ }
2211
+
2212
+ return removedLines;
2213
+ }
2214
+
2215
+ /**
2216
+ * STRICT verification that TUI output matches actual file content.
2217
+ * This is the CONTRACT enforcement - if TUI lies, this fails.
2218
+ */
2219
+ function strictVerifyTUIContract(
2220
+ tuiOutput: string,
2221
+ originalContent: string | undefined,
2222
+ actualFileContent: string,
2223
+ testName: string,
2224
+ ): { passed: boolean; errors: string[] } {
2225
+ const errors: string[] = [];
2226
+
2227
+ console.log(`\n${"=".repeat(80)}`);
2228
+ console.log(`STRICT CONTRACT VERIFICATION: ${testName}`);
2229
+ console.log("=".repeat(80));
2230
+
2231
+ // Extract what TUI claims
2232
+ const tuiAddedLines = extractAddedLinesFromTUI(tuiOutput);
2233
+ const tuiRemovedLines = extractRemovedLinesFromTUI(tuiOutput);
2234
+
2235
+ console.log(`[CONTRACT] TUI claims ${tuiAddedLines.length} additions`);
2236
+ console.log(`[CONTRACT] TUI claims ${tuiRemovedLines.length} removals`);
2237
+
2238
+ // VERIFY ALL ADDITIONS: Every line TUI claims to add MUST be in the file
2239
+ console.log("\n[CONTRACT] Verifying ALL claimed additions exist in file...");
2240
+ for (let i = 0; i < tuiAddedLines.length; i++) {
2241
+ const addedLine = tuiAddedLines[i];
2242
+ const isInFile = actualFileContent.includes(addedLine);
2243
+
2244
+ if (isInFile) {
2245
+ console.log(
2246
+ ` ✓ Addition ${i + 1}/${tuiAddedLines.length}: "${addedLine.substring(0, 50)}${addedLine.length > 50 ? "..." : ""}"`,
2247
+ );
2248
+ } else {
2249
+ const errorMsg = `VIOLATION: TUI claims to add "${addedLine}" but it's NOT in the file!`;
2250
+ console.log(` ✗ ${errorMsg}`);
2251
+ errors.push(errorMsg);
2252
+ }
2253
+ }
2254
+
2255
+ // VERIFY ALL REMOVALS: Every line TUI claims to remove should NOT be in the final file
2256
+ // (unless it was re-added as part of the same edit)
2257
+ console.log("\n[CONTRACT] Verifying ALL claimed removals are gone from file...");
2258
+ for (let i = 0; i < tuiRemovedLines.length; i++) {
2259
+ const removedLine = tuiRemovedLines[i];
2260
+ const stillInFile = actualFileContent.includes(removedLine);
2261
+ const wasReAdded = tuiAddedLines.some((added) => added.includes(removedLine));
2262
+
2263
+ if (!stillInFile) {
2264
+ console.log(
2265
+ ` ✓ Removal ${i + 1}/${tuiRemovedLines.length}: "${removedLine.substring(0, 50)}${removedLine.length > 50 ? "..." : ""}" - correctly removed`,
2266
+ );
2267
+ } else if (wasReAdded) {
2268
+ console.log(
2269
+ ` ~ Removal ${i + 1}/${tuiRemovedLines.length}: "${removedLine.substring(0, 50)}..." - still in file but was re-added (OK)`,
2270
+ );
2271
+ } else {
2272
+ const errorMsg = `VIOLATION: TUI claims to remove "${removedLine}" but it's STILL in the file!`;
2273
+ console.log(` ✗ ${errorMsg}`);
2274
+ errors.push(errorMsg);
2275
+ }
2276
+ }
2277
+
2278
+ // VERIFY ORIGINAL CONTENT: If we had original content, verify unchanged parts are preserved
2279
+ if (originalContent) {
2280
+ console.log("\n[CONTRACT] Verifying file structure integrity...");
2281
+ const originalLines = originalContent.split("\n");
2282
+ const finalLines = actualFileContent.split("\n");
2283
+ void finalLines; // Keep for potential future debugging
2284
+
2285
+ // Check that lines not mentioned in TUI removals are still present
2286
+ for (const origLine of originalLines) {
2287
+ const trimmedOrig = origLine.trim();
2288
+ if (trimmedOrig.length > 3) {
2289
+ // Skip very short lines
2290
+ const wasRemoved = tuiRemovedLines.some(
2291
+ (r) => r.includes(trimmedOrig) || trimmedOrig.includes(r),
2292
+ );
2293
+ const isInFinal = actualFileContent.includes(trimmedOrig);
2294
+
2295
+ if (!wasRemoved && !isInFinal) {
2296
+ // This line was silently removed - TUI didn't tell user about it
2297
+ const errorMsg = `SILENT REMOVAL: Line "${trimmedOrig}" disappeared without TUI showing removal!`;
2298
+ console.log(` ✗ ${errorMsg}`);
2299
+ errors.push(errorMsg);
2300
+ }
2301
+ }
2302
+ }
2303
+ }
2304
+
2305
+ console.log("\n" + "=".repeat(80));
2306
+ if (errors.length === 0) {
2307
+ console.log(`[CONTRACT] ✓ ALL VERIFICATIONS PASSED - TUI told the truth`);
2308
+ } else {
2309
+ console.log(`[CONTRACT] ✗ ${errors.length} VIOLATIONS FOUND - TUI LIED TO USER!`);
2310
+ errors.forEach((e) => console.log(` - ${e}`));
2311
+ }
2312
+ console.log("=".repeat(80) + "\n");
2313
+
2314
+ return { passed: errors.length === 0, errors };
2315
+ }
2316
+
2317
+ describe.concurrent("STRICT CONTRACT: TUI Must Not Lie To User", () => {
2318
+ let testDir: string;
2319
+
2320
+ beforeEach(async () => {
2321
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), "diff-contract-test-"));
2322
+ console.log(`[CONTRACT TEST] Directory: ${testDir}`);
2323
+ });
2324
+
2325
+ afterEach(async () => {
2326
+ cleanup();
2327
+ try {
2328
+ await fs.rm(testDir, { recursive: true, force: true });
2329
+ } catch {
2330
+ // Ignore cleanup errors
2331
+ }
2332
+ });
2333
+
2334
+ it("CONTRACT: If TUI shows +10 lines added, exactly those 10 lines MUST be in file", async () => {
2335
+ const testFile = path.join(testDir, "ten-additions.js");
2336
+
2337
+ // Original file
2338
+ const originalContent = `function process() {
2339
+ return null;
2340
+ }`;
2341
+ await fs.writeFile(testFile, originalContent, "utf-8");
2342
+
2343
+ // codeEdit with exactly 10 new lines (numbered for verification)
2344
+ const codeEdit = `function process() {
2345
+ console.log('LINE_1_ADDED');
2346
+ console.log('LINE_2_ADDED');
2347
+ console.log('LINE_3_ADDED');
2348
+ console.log('LINE_4_ADDED');
2349
+ console.log('LINE_5_ADDED');
2350
+ console.log('LINE_6_ADDED');
2351
+ console.log('LINE_7_ADDED');
2352
+ console.log('LINE_8_ADDED');
2353
+ console.log('LINE_9_ADDED');
2354
+ console.log('LINE_10_ADDED');
2355
+ return null;
2356
+ }`;
2357
+
2358
+ // Render TUI
2359
+ const toolData = createMockToolPermission({
2360
+ targetFile: testFile,
2361
+ codeEdit: codeEdit,
2362
+ currentFileContent: originalContent,
2363
+ fileExists: true,
2364
+ overwriteFile: true,
2365
+ });
2366
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2367
+ const { plainText } = displayTUI("CONTRACT: 10 additions test", instance);
2368
+
2369
+ // Apply edit
2370
+ await fs.writeFile(testFile, codeEdit, "utf-8");
2371
+ const actualContent = await fs.readFile(testFile, "utf-8");
2372
+
2373
+ // STRICT CONTRACT VERIFICATION
2374
+ const result = strictVerifyTUIContract(
2375
+ plainText,
2376
+ originalContent,
2377
+ actualContent,
2378
+ "10 Additions",
2379
+ );
2380
+
2381
+ // Count actual additions in TUI
2382
+ const tuiAdditions = extractAddedLinesFromTUI(plainText);
2383
+ console.log(`[TEST] TUI showed ${tuiAdditions.length} additions`);
2384
+
2385
+ // Verify we got exactly 10 LINE_X_ADDED entries
2386
+ const lineAdditions = tuiAdditions.filter(
2387
+ (line) => line.includes("LINE_") && line.includes("_ADDED"),
2388
+ );
2389
+ console.log(`[TEST] Found ${lineAdditions.length} LINE_X_ADDED entries in TUI`);
2390
+
2391
+ // Verify ALL 10 are in the file
2392
+ for (let i = 1; i <= 10; i++) {
2393
+ const marker = `LINE_${i}_ADDED`;
2394
+ expect(actualContent).toContain(marker);
2395
+ expect(plainText).toContain(marker);
2396
+ }
2397
+
2398
+ expect(result.passed).toBe(true);
2399
+ expect(result.errors).toHaveLength(0);
2400
+ });
2401
+
2402
+ it("CONTRACT: If TUI shows -5 lines removed, those 5 lines MUST NOT be in file", async () => {
2403
+ const testFile = path.join(testDir, "five-removals.js");
2404
+
2405
+ // Original file with 5 lines to remove
2406
+ const originalContent = `function old() {
2407
+ console.log('REMOVE_LINE_1');
2408
+ console.log('REMOVE_LINE_2');
2409
+ console.log('REMOVE_LINE_3');
2410
+ console.log('REMOVE_LINE_4');
2411
+ console.log('REMOVE_LINE_5');
2412
+ return true;
2413
+ }`;
2414
+ await fs.writeFile(testFile, originalContent, "utf-8");
2415
+
2416
+ // codeEdit without those 5 lines
2417
+ const codeEdit = `function old() {
2418
+ return true;
2419
+ }`;
2420
+
2421
+ // Render TUI
2422
+ const toolData = createMockToolPermission({
2423
+ targetFile: testFile,
2424
+ codeEdit: codeEdit,
2425
+ currentFileContent: originalContent,
2426
+ fileExists: true,
2427
+ overwriteFile: true,
2428
+ });
2429
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2430
+ const { plainText } = displayTUI("CONTRACT: 5 removals test", instance);
2431
+
2432
+ // Apply edit
2433
+ await fs.writeFile(testFile, codeEdit, "utf-8");
2434
+ const actualContent = await fs.readFile(testFile, "utf-8");
2435
+
2436
+ // STRICT CONTRACT VERIFICATION
2437
+ const result = strictVerifyTUIContract(plainText, originalContent, actualContent, "5 Removals");
2438
+
2439
+ // Verify ALL 5 REMOVE_LINE_X are gone from file
2440
+ for (let i = 1; i <= 5; i++) {
2441
+ const marker = `REMOVE_LINE_${i}`;
2442
+ expect(actualContent).not.toContain(marker);
2443
+ // TUI should have shown these as removed
2444
+ expect(plainText).toContain(marker);
2445
+ }
2446
+
2447
+ expect(result.passed).toBe(true);
2448
+ expect(result.errors).toHaveLength(0);
2449
+ });
2450
+
2451
+ it("CONTRACT: Mixed additions and removals - ALL must be accurate", async () => {
2452
+ const testFile = path.join(testDir, "mixed-changes.js");
2453
+
2454
+ // Original with some content
2455
+ const originalContent = `// Header comment
2456
+ function calculate(a, b) {
2457
+ const OLD_VAR_1 = a + b;
2458
+ const OLD_VAR_2 = a - b;
2459
+ const OLD_VAR_3 = a * b;
2460
+ return OLD_VAR_1;
2461
+ }
2462
+ // Footer comment`;
2463
+ await fs.writeFile(testFile, originalContent, "utf-8");
2464
+
2465
+ // Mixed changes: remove OLD_VAR_2 and OLD_VAR_3, add NEW_VAR_A and NEW_VAR_B
2466
+ const codeEdit = `// Header comment
2467
+ function calculate(a, b) {
2468
+ const OLD_VAR_1 = a + b;
2469
+ const NEW_VAR_A = a / b;
2470
+ const NEW_VAR_B = a % b;
2471
+ console.log('ADDED_DEBUG_LINE');
2472
+ return OLD_VAR_1;
2473
+ }
2474
+ // Footer comment`;
2475
+
2476
+ // Render TUI
2477
+ const toolData = createMockToolPermission({
2478
+ targetFile: testFile,
2479
+ codeEdit: codeEdit,
2480
+ currentFileContent: originalContent,
2481
+ fileExists: true,
2482
+ overwriteFile: true,
2483
+ });
2484
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2485
+ const { plainText } = displayTUI("CONTRACT: Mixed changes test", instance);
2486
+
2487
+ // Apply edit
2488
+ await fs.writeFile(testFile, codeEdit, "utf-8");
2489
+ const actualContent = await fs.readFile(testFile, "utf-8");
2490
+
2491
+ // STRICT CONTRACT VERIFICATION
2492
+ const result = strictVerifyTUIContract(
2493
+ plainText,
2494
+ originalContent,
2495
+ actualContent,
2496
+ "Mixed Changes",
2497
+ );
2498
+
2499
+ // Verify removed lines are gone
2500
+ expect(actualContent).not.toContain("OLD_VAR_2");
2501
+ expect(actualContent).not.toContain("OLD_VAR_3");
2502
+
2503
+ // Verify added lines are present
2504
+ expect(actualContent).toContain("NEW_VAR_A");
2505
+ expect(actualContent).toContain("NEW_VAR_B");
2506
+ expect(actualContent).toContain("ADDED_DEBUG_LINE");
2507
+
2508
+ // Verify unchanged lines are preserved
2509
+ expect(actualContent).toContain("OLD_VAR_1");
2510
+ expect(actualContent).toContain("// Header comment");
2511
+ expect(actualContent).toContain("// Footer comment");
2512
+
2513
+ expect(result.passed).toBe(true);
2514
+ expect(result.errors).toHaveLength(0);
2515
+ });
2516
+
2517
+ it("CONTRACT: Every console.log TUI shows as added MUST be in file", async () => {
2518
+ const testFile = path.join(testDir, "console-logs.ts");
2519
+
2520
+ const originalContent = `export async function fetchData(url: string) {
2521
+ const response = await fetch(url);
2522
+ const data = await response.json();
2523
+ return data;
2524
+ }
2525
+
2526
+ export function processData(data: any) {
2527
+ const result = data.map((x: any) => x.value);
2528
+ return result;
2529
+ }`;
2530
+ await fs.writeFile(testFile, originalContent, "utf-8");
2531
+
2532
+ // Add 6 specific console.log statements
2533
+ const codeEdit = `export async function fetchData(url: string) {
2534
+ console.log('[fetchData] MARKER_LOG_1 - Starting fetch');
2535
+ const response = await fetch(url);
2536
+ console.log('[fetchData] MARKER_LOG_2 - Got response:', response.status);
2537
+ const data = await response.json();
2538
+ console.log('[fetchData] MARKER_LOG_3 - Parsed data');
2539
+ return data;
2540
+ }
2541
+
2542
+ export function processData(data: any) {
2543
+ console.log('[processData] MARKER_LOG_4 - Processing', data.length, 'items');
2544
+ const result = data.map((x: any) => x.value);
2545
+ console.log('[processData] MARKER_LOG_5 - Mapped to', result.length, 'values');
2546
+ console.log('[processData] MARKER_LOG_6 - Done');
2547
+ return result;
2548
+ }`;
2549
+
2550
+ // Render TUI
2551
+ const toolData = createMockToolPermission({
2552
+ targetFile: testFile,
2553
+ codeEdit: codeEdit,
2554
+ currentFileContent: originalContent,
2555
+ fileExists: true,
2556
+ overwriteFile: true,
2557
+ });
2558
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2559
+ const { plainText } = displayTUI("CONTRACT: 6 console.logs test", instance);
2560
+
2561
+ // Apply edit
2562
+ await fs.writeFile(testFile, codeEdit, "utf-8");
2563
+ const actualContent = await fs.readFile(testFile, "utf-8");
2564
+
2565
+ // STRICT CONTRACT VERIFICATION
2566
+ const result = strictVerifyTUIContract(
2567
+ plainText,
2568
+ originalContent,
2569
+ actualContent,
2570
+ "6 Console.logs",
2571
+ );
2572
+
2573
+ // Verify ALL 6 MARKER_LOG_X are in file AND shown in TUI
2574
+ for (let i = 1; i <= 6; i++) {
2575
+ const marker = `MARKER_LOG_${i}`;
2576
+ expect(actualContent).toContain(marker);
2577
+ expect(plainText).toContain(marker);
2578
+ }
2579
+
2580
+ // Count console.logs in file vs TUI claims
2581
+ const fileConsoleLogCount = (actualContent.match(/console\.log/g) || []).length;
2582
+ const tuiAdditions = extractAddedLinesFromTUI(plainText);
2583
+ const tuiConsoleLogAdditions = tuiAdditions.filter((line) => line.includes("console.log"));
2584
+
2585
+ console.log(`[CONTRACT] File has ${fileConsoleLogCount} console.logs`);
2586
+ console.log(`[CONTRACT] TUI shows ${tuiConsoleLogAdditions.length} console.log additions`);
2587
+
2588
+ // TUI must show at least as many console.log additions as are new in the file
2589
+ expect(tuiConsoleLogAdditions.length).toBeGreaterThanOrEqual(6);
2590
+
2591
+ expect(result.passed).toBe(true);
2592
+ expect(result.errors).toHaveLength(0);
2593
+ });
2594
+
2595
+ it("CONTRACT: startLine/endLine edit - only specified lines should change", async () => {
2596
+ const testFile = path.join(testDir, "line-range-strict.js");
2597
+
2598
+ // Original with clearly marked lines
2599
+ const originalContent = `// LINE_1_UNCHANGED
2600
+ // LINE_2_UNCHANGED
2601
+ // LINE_3_TO_REPLACE
2602
+ // LINE_4_TO_REPLACE
2603
+ // LINE_5_TO_REPLACE
2604
+ // LINE_6_UNCHANGED
2605
+ // LINE_7_UNCHANGED`;
2606
+ await fs.writeFile(testFile, originalContent, "utf-8");
2607
+
2608
+ // Replace lines 3-5
2609
+ const codeEdit = `// LINE_3_REPLACED_NEW
2610
+ // LINE_4_REPLACED_NEW
2611
+ // LINE_5_REPLACED_NEW`;
2612
+
2613
+ // Render TUI
2614
+ const toolData = createMockToolPermission({
2615
+ targetFile: testFile,
2616
+ codeEdit: codeEdit,
2617
+ currentFileContent: originalContent,
2618
+ fileExists: true,
2619
+ startLine: 3,
2620
+ endLine: 5,
2621
+ });
2622
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2623
+ const { plainText } = displayTUI("CONTRACT: Line range strict test", instance);
2624
+
2625
+ // Apply edit using same logic as production
2626
+ const lines = originalContent.split("\n");
2627
+ const beforeLines = lines.slice(0, 2); // Lines 1-2
2628
+ const afterLines = lines.slice(5); // Lines 6-7
2629
+ const newLines = codeEdit.split("\n");
2630
+ const finalContent = [...beforeLines, ...newLines, ...afterLines].join("\n");
2631
+ await fs.writeFile(testFile, finalContent, "utf-8");
2632
+ const actualContent = await fs.readFile(testFile, "utf-8");
2633
+
2634
+ // STRICT CONTRACT VERIFICATION
2635
+ const result = strictVerifyTUIContract(
2636
+ plainText,
2637
+ originalContent,
2638
+ actualContent,
2639
+ "Line Range Strict",
2640
+ );
2641
+
2642
+ // Verify unchanged lines are STILL present
2643
+ expect(actualContent).toContain("LINE_1_UNCHANGED");
2644
+ expect(actualContent).toContain("LINE_2_UNCHANGED");
2645
+ expect(actualContent).toContain("LINE_6_UNCHANGED");
2646
+ expect(actualContent).toContain("LINE_7_UNCHANGED");
2647
+
2648
+ // Verify old lines are GONE
2649
+ expect(actualContent).not.toContain("LINE_3_TO_REPLACE");
2650
+ expect(actualContent).not.toContain("LINE_4_TO_REPLACE");
2651
+ expect(actualContent).not.toContain("LINE_5_TO_REPLACE");
2652
+
2653
+ // Verify new lines are PRESENT
2654
+ expect(actualContent).toContain("LINE_3_REPLACED_NEW");
2655
+ expect(actualContent).toContain("LINE_4_REPLACED_NEW");
2656
+ expect(actualContent).toContain("LINE_5_REPLACED_NEW");
2657
+
2658
+ // TUI must show the TO_REPLACE lines as removed
2659
+ expect(plainText).toContain("LINE_3_TO_REPLACE");
2660
+ expect(plainText).toContain("LINE_4_TO_REPLACE");
2661
+ expect(plainText).toContain("LINE_5_TO_REPLACE");
2662
+
2663
+ // TUI must show the REPLACED_NEW lines as added
2664
+ expect(plainText).toContain("LINE_3_REPLACED_NEW");
2665
+ expect(plainText).toContain("LINE_4_REPLACED_NEW");
2666
+ expect(plainText).toContain("LINE_5_REPLACED_NEW");
2667
+
2668
+ expect(result.passed).toBe(true);
2669
+ expect(result.errors).toHaveLength(0);
2670
+ });
2671
+
2672
+ it("CONTRACT: Real-world React component edit - every prop change must be reflected", async () => {
2673
+ const testFile = path.join(testDir, "Button.tsx");
2674
+
2675
+ const originalContent = `import React from 'react';
2676
+
2677
+ interface ButtonProps {
2678
+ label: string;
2679
+ }
2680
+
2681
+ export function Button({ label }: ButtonProps) {
2682
+ return (
2683
+ <button className="btn">
2684
+ {label}
2685
+ </button>
2686
+ );
2687
+ }`;
2688
+ await fs.writeFile(testFile, originalContent, "utf-8");
2689
+
2690
+ // Add multiple props and features
2691
+ const codeEdit = `import React from 'react';
2692
+
2693
+ interface ButtonProps {
2694
+ label: string;
2695
+ onClick: () => void;
2696
+ disabled?: boolean;
2697
+ variant?: 'primary' | 'secondary';
2698
+ size?: 'small' | 'medium' | 'large';
2699
+ }
2700
+
2701
+ export function Button({ label, onClick, disabled, variant = 'primary', size = 'medium' }: ButtonProps) {
2702
+ console.log('[Button] RENDER_LOG_1 - Rendering with variant:', variant);
2703
+
2704
+ const handleClick = () => {
2705
+ console.log('[Button] CLICK_LOG_2 - Button clicked');
2706
+ onClick();
2707
+ };
2708
+
2709
+ return (
2710
+ <button
2711
+ className={\`btn btn-\${variant} btn-\${size}\`}
2712
+ onClick={handleClick}
2713
+ disabled={disabled}
2714
+ >
2715
+ {label}
2716
+ </button>
2717
+ );
2718
+ }`;
2719
+
2720
+ // Render TUI
2721
+ const toolData = createMockToolPermission({
2722
+ targetFile: testFile,
2723
+ codeEdit: codeEdit,
2724
+ currentFileContent: originalContent,
2725
+ fileExists: true,
2726
+ overwriteFile: true,
2727
+ });
2728
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2729
+ const { plainText } = displayTUI("CONTRACT: React component test", instance);
2730
+
2731
+ // Apply edit
2732
+ await fs.writeFile(testFile, codeEdit, "utf-8");
2733
+ const actualContent = await fs.readFile(testFile, "utf-8");
2734
+
2735
+ // STRICT CONTRACT VERIFICATION
2736
+ const result = strictVerifyTUIContract(
2737
+ plainText,
2738
+ originalContent,
2739
+ actualContent,
2740
+ "React Component",
2741
+ );
2742
+
2743
+ // Verify NEW props are in file
2744
+ expect(actualContent).toContain("onClick");
2745
+ expect(actualContent).toContain("disabled");
2746
+ expect(actualContent).toContain("variant");
2747
+ expect(actualContent).toContain("size");
2748
+
2749
+ // Verify NEW features are in file
2750
+ expect(actualContent).toContain("RENDER_LOG_1");
2751
+ expect(actualContent).toContain("CLICK_LOG_2");
2752
+ expect(actualContent).toContain("handleClick");
2753
+
2754
+ // Verify TUI showed these additions
2755
+ expect(plainText).toContain("onClick");
2756
+ expect(plainText).toContain("disabled");
2757
+ expect(plainText).toContain("RENDER_LOG_1");
2758
+ expect(plainText).toContain("CLICK_LOG_2");
2759
+
2760
+ expect(result.passed).toBe(true);
2761
+ expect(result.errors).toHaveLength(0);
2762
+ });
2763
+
2764
+ it("CONTRACT VIOLATION TEST: Detect when TUI would lie about changes", async () => {
2765
+ /**
2766
+ * This test demonstrates that our contract verification CATCHES lies.
2767
+ * We simulate a scenario where TUI claims to add something but it doesn't appear.
2768
+ * The strictVerifyTUIContract function should return errors.
2769
+ */
2770
+ const testFile = path.join(testDir, "violation-test.js");
2771
+
2772
+ const originalContent = `const x = 1;`;
2773
+ const codeEdit = `const x = 1;\nconst SHOULD_BE_ADDED = 2;`;
2774
+
2775
+ await fs.writeFile(testFile, originalContent, "utf-8");
2776
+
2777
+ // Render TUI (which will show the addition)
2778
+ const toolData = createMockToolPermission({
2779
+ targetFile: testFile,
2780
+ codeEdit: codeEdit,
2781
+ currentFileContent: originalContent,
2782
+ fileExists: true,
2783
+ overwriteFile: true,
2784
+ });
2785
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2786
+ const { plainText } = displayTUI("CONTRACT VIOLATION TEST", instance);
2787
+
2788
+ // INTENTIONALLY write wrong content (simulating a bug)
2789
+ const buggyContent = `const x = 1;\nconst WRONG_CONTENT = 999;`;
2790
+ await fs.writeFile(testFile, buggyContent, "utf-8");
2791
+ const actualContent = await fs.readFile(testFile, "utf-8");
2792
+
2793
+ // Contract verification should FAIL because TUI promised SHOULD_BE_ADDED
2794
+ const result = strictVerifyTUIContract(
2795
+ plainText,
2796
+ originalContent,
2797
+ actualContent,
2798
+ "Violation Detection",
2799
+ );
2800
+
2801
+ console.log("[VIOLATION TEST] Expected: contract should FAIL");
2802
+ console.log("[VIOLATION TEST] Result passed:", result.passed);
2803
+ console.log("[VIOLATION TEST] Errors found:", result.errors.length);
2804
+
2805
+ // This should have failed - TUI said it would add SHOULD_BE_ADDED but it's not there
2806
+ // If the TUI showed the addition, the contract is violated
2807
+ const tuiAdditions = extractAddedLinesFromTUI(plainText);
2808
+ const tuiShowedAddition = tuiAdditions.some((line) => line.includes("SHOULD_BE_ADDED"));
2809
+
2810
+ if (tuiShowedAddition) {
2811
+ // TUI promised it, but file doesn't have it = VIOLATION
2812
+ expect(result.passed).toBe(false);
2813
+ expect(result.errors.length).toBeGreaterThan(0);
2814
+ expect(result.errors.some((e) => e.includes("SHOULD_BE_ADDED"))).toBe(true);
2815
+ console.log("[VIOLATION TEST] ✓ Correctly detected the lie!");
2816
+ } else {
2817
+ // TUI didn't show it, so no contract violation (different test case)
2818
+ console.log("[VIOLATION TEST] TUI did not show the addition (unexpected)");
2819
+ }
2820
+ });
2821
+
2822
+ it("CONTRACT: Comprehensive edit with 20 changes - ALL must be reflected", async () => {
2823
+ const testFile = path.join(testDir, "comprehensive.ts");
2824
+
2825
+ // Original file
2826
+ const originalContent = `export class DataProcessor {
2827
+ private data: string[] = [];
2828
+
2829
+ constructor() {
2830
+ // OLD_CONSTRUCTOR_COMMENT
2831
+ }
2832
+
2833
+ process(): void {
2834
+ // OLD_PROCESS_COMMENT
2835
+ }
2836
+
2837
+ save(): void {
2838
+ // OLD_SAVE_COMMENT
2839
+ }
2840
+ }`;
2841
+ await fs.writeFile(testFile, originalContent, "utf-8");
2842
+
2843
+ // Comprehensive edit with many changes
2844
+ const codeEdit = `export class DataProcessor {
2845
+ private data: string[] = [];
2846
+ private ADDED_FIELD_1: number = 0;
2847
+ private ADDED_FIELD_2: boolean = false;
2848
+ private ADDED_FIELD_3: Map<string, any> = new Map();
2849
+
2850
+ constructor() {
2851
+ console.log('ADDED_LOG_1: Constructor called');
2852
+ this.ADDED_FIELD_1 = Date.now();
2853
+ console.log('ADDED_LOG_2: Initialized timestamp');
2854
+ }
2855
+
2856
+ process(): void {
2857
+ console.log('ADDED_LOG_3: Processing started');
2858
+ this.ADDED_FIELD_2 = true;
2859
+ console.log('ADDED_LOG_4: Flag set');
2860
+ this.doProcess();
2861
+ console.log('ADDED_LOG_5: Processing complete');
2862
+ }
2863
+
2864
+ private doProcess(): void {
2865
+ console.log('ADDED_LOG_6: Internal process');
2866
+ for (const item of this.data) {
2867
+ console.log('ADDED_LOG_7: Processing item');
2868
+ this.ADDED_FIELD_3.set(item, true);
2869
+ }
2870
+ console.log('ADDED_LOG_8: All items processed');
2871
+ }
2872
+
2873
+ save(): void {
2874
+ console.log('ADDED_LOG_9: Saving data');
2875
+ console.log('ADDED_LOG_10: Save complete');
2876
+ }
2877
+ }`;
2878
+
2879
+ // Render TUI
2880
+ const toolData = createMockToolPermission({
2881
+ targetFile: testFile,
2882
+ codeEdit: codeEdit,
2883
+ currentFileContent: originalContent,
2884
+ fileExists: true,
2885
+ overwriteFile: true,
2886
+ });
2887
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
2888
+ const { plainText } = displayTUI("CONTRACT: Comprehensive 20+ changes test", instance);
2889
+
2890
+ // Apply edit
2891
+ await fs.writeFile(testFile, codeEdit, "utf-8");
2892
+ const actualContent = await fs.readFile(testFile, "utf-8");
2893
+
2894
+ // STRICT CONTRACT VERIFICATION
2895
+ const result = strictVerifyTUIContract(
2896
+ plainText,
2897
+ originalContent,
2898
+ actualContent,
2899
+ "Comprehensive 20+ Changes",
2900
+ );
2901
+
2902
+ // Verify ALL added fields are in file
2903
+ expect(actualContent).toContain("ADDED_FIELD_1");
2904
+ expect(actualContent).toContain("ADDED_FIELD_2");
2905
+ expect(actualContent).toContain("ADDED_FIELD_3");
2906
+
2907
+ // Verify ALL 10 logs are in file
2908
+ for (let i = 1; i <= 10; i++) {
2909
+ const marker = `ADDED_LOG_${i}`;
2910
+ expect(actualContent).toContain(marker);
2911
+ }
2912
+
2913
+ // Verify OLD comments are gone
2914
+ expect(actualContent).not.toContain("OLD_CONSTRUCTOR_COMMENT");
2915
+ expect(actualContent).not.toContain("OLD_PROCESS_COMMENT");
2916
+ expect(actualContent).not.toContain("OLD_SAVE_COMMENT");
2917
+
2918
+ // Verify NEW method exists
2919
+ expect(actualContent).toContain("doProcess");
2920
+
2921
+ // Count total changes
2922
+ const tuiAdditions = extractAddedLinesFromTUI(plainText);
2923
+ const tuiRemovals = extractRemovedLinesFromTUI(plainText);
2924
+
2925
+ console.log(`[COMPREHENSIVE] Total TUI additions: ${tuiAdditions.length}`);
2926
+ console.log(`[COMPREHENSIVE] Total TUI removals: ${tuiRemovals.length}`);
2927
+
2928
+ // We should have many additions (fields, logs, new method lines)
2929
+ expect(tuiAdditions.length).toBeGreaterThanOrEqual(15);
2930
+
2931
+ expect(result.passed).toBe(true);
2932
+ expect(result.errors).toHaveLength(0);
2933
+ });
2934
+ });
2935
+
2936
+ /**
2937
+ * =============================================================================
2938
+ * PRODUCTION vs TUI SYNC TESTS - THE CRITICAL BUG CATCHER
2939
+ * =============================================================================
2940
+ * These tests compare the TUI preview logic vs the production editTool logic.
2941
+ * If they produce DIFFERENT results for the same input, the test FAILS.
2942
+ *
2943
+ * This catches bugs where:
2944
+ * - TUI shows "I'll add 10 lines" but production only adds 5
2945
+ * - TUI shows one change but production applies a different change
2946
+ * - The 80% rule or other heuristics diverge
2947
+ *
2948
+ * HOW IT WORKS:
2949
+ * 1. Given the same inputs (currentContent, codeEdit, flags)
2950
+ * 2. Run both tuiPreviewLogic() and productionFallbackLogic()
2951
+ * 3. If outputs differ → TEST FAILS → BUG DETECTED
2952
+ * =============================================================================
2953
+ */
2954
+
2955
+ describe.concurrent("PRODUCTION vs TUI SYNC: Catch Logic Divergence Bugs", () => {
2956
+ /**
2957
+ * Helper to compare TUI preview vs production and detect divergence
2958
+ */
2959
+ function verifyTUIMatchesProduction(
2960
+ testName: string,
2961
+ currentContent: string,
2962
+ codeEdit: string,
2963
+ overwriteFile: boolean,
2964
+ startLine?: number,
2965
+ endLine?: number,
2966
+ instructions?: string,
2967
+ ): { match: boolean; tuiResult: string; prodResult: string } {
2968
+ const tuiResult = tuiPreviewLogic(
2969
+ currentContent,
2970
+ codeEdit,
2971
+ overwriteFile,
2972
+ startLine,
2973
+ endLine,
2974
+ instructions,
2975
+ );
2976
+ const prodResult = productionFallbackLogic(
2977
+ currentContent,
2978
+ codeEdit,
2979
+ overwriteFile,
2980
+ startLine,
2981
+ endLine,
2982
+ instructions,
2983
+ );
2984
+
2985
+ const match = tuiResult === prodResult;
2986
+
2987
+ console.log(`\n${"=".repeat(80)}`);
2988
+ console.log(`SYNC CHECK: ${testName}`);
2989
+ console.log("=".repeat(80));
2990
+ console.log(`[INPUT] currentContent length: ${currentContent.length}`);
2991
+ console.log(`[INPUT] codeEdit length: ${codeEdit.length}`);
2992
+ console.log(`[INPUT] overwriteFile: ${overwriteFile}`);
2993
+ console.log(`[INPUT] startLine: ${startLine}, endLine: ${endLine}`);
2994
+ console.log(`[INPUT] instructions: ${instructions ? "yes" : "no"}`);
2995
+ console.log("");
2996
+ console.log(`[TUI PREVIEW] Result length: ${tuiResult.length}`);
2997
+ console.log(`[PRODUCTION] Result length: ${prodResult.length}`);
2998
+ console.log("");
2999
+
3000
+ if (match) {
3001
+ console.log(`✓ MATCH: TUI preview and production produce SAME result`);
3002
+ } else {
3003
+ console.log(`✗ MISMATCH DETECTED! TUI lies to user!`);
3004
+ console.log("");
3005
+ console.log("[TUI PREVIEW OUTPUT]:");
3006
+ console.log(tuiResult);
3007
+ console.log("");
3008
+ console.log("[PRODUCTION OUTPUT]:");
3009
+ console.log(prodResult);
3010
+ console.log("");
3011
+ console.log("[DIFF]:");
3012
+ // Show where they differ
3013
+ const tuiLines = tuiResult.split("\n");
3014
+ const prodLines = prodResult.split("\n");
3015
+ const maxLines = Math.max(tuiLines.length, prodLines.length);
3016
+ for (let i = 0; i < maxLines; i++) {
3017
+ const tui = tuiLines[i] ?? "<missing>";
3018
+ const prod = prodLines[i] ?? "<missing>";
3019
+ if (tui !== prod) {
3020
+ console.log(` Line ${i + 1}:`);
3021
+ console.log(` TUI: "${tui}"`);
3022
+ console.log(` PROD: "${prod}"`);
3023
+ }
3024
+ }
3025
+ }
3026
+ console.log("=".repeat(80));
3027
+
3028
+ return { match, tuiResult, prodResult };
3029
+ }
3030
+
3031
+ it("SYNC: Overwrite mode - TUI must match production", () => {
3032
+ const currentContent = `function old() { return 1; }`;
3033
+ const codeEdit = `function new() { return 2; }`;
3034
+
3035
+ const result = verifyTUIMatchesProduction(
3036
+ "Overwrite mode",
3037
+ currentContent,
3038
+ codeEdit,
3039
+ true, // overwriteFile
3040
+ );
3041
+
3042
+ expect(result.match).toBe(true);
3043
+ });
3044
+
3045
+ it("SYNC: Line range replacement - TUI must match production", () => {
3046
+ const currentContent = `line 1\nline 2\nline 3\nline 4\nline 5`;
3047
+ const codeEdit = `NEW LINE 2\nNEW LINE 3`;
3048
+
3049
+ const result = verifyTUIMatchesProduction(
3050
+ "Line range replacement (2-3)",
3051
+ currentContent,
3052
+ codeEdit,
3053
+ false,
3054
+ 2, // startLine
3055
+ 3, // endLine
3056
+ );
3057
+
3058
+ expect(result.match).toBe(true);
3059
+ });
3060
+
3061
+ it("SYNC: Instructions append - TUI must match production", () => {
3062
+ const currentContent = `const x = 1;`;
3063
+ const codeEdit = `const y = 2;`;
3064
+
3065
+ const result = verifyTUIMatchesProduction(
3066
+ "Instructions append",
3067
+ currentContent,
3068
+ codeEdit,
3069
+ false,
3070
+ undefined,
3071
+ undefined,
3072
+ "Add a new variable",
3073
+ );
3074
+
3075
+ expect(result.match).toBe(true);
3076
+ });
3077
+
3078
+ it("SYNC: Content already present - TUI must match production", () => {
3079
+ const currentContent = `const x = 1;\nconsole.log(x);`;
3080
+ const codeEdit = `console.log(x);`;
3081
+
3082
+ const result = verifyTUIMatchesProduction(
3083
+ "Content already present",
3084
+ currentContent,
3085
+ codeEdit,
3086
+ false,
3087
+ );
3088
+
3089
+ expect(result.match).toBe(true);
3090
+ });
3091
+
3092
+ it("SYNC: Small append - TUI must match production", () => {
3093
+ const currentContent = `function main() {\n doStuff();\n}`;
3094
+ const codeEdit = `console.log('done');`;
3095
+
3096
+ const result = verifyTUIMatchesProduction("Small append", currentContent, codeEdit, false);
3097
+
3098
+ // NOTE: This might fail if 80% rule diverges!
3099
+ expect(result.match).toBe(true);
3100
+ });
3101
+
3102
+ it("SYNC WARNING: 80% rule - KNOWN DIVERGENCE POINT", () => {
3103
+ /**
3104
+ * THIS IS A KNOWN ISSUE:
3105
+ * - TUI preview has 80% rule (if codeEdit >= 80% of lines, replace all)
3106
+ * - Production fallback does NOT have 80% rule (always appends)
3107
+ *
3108
+ * This test documents the divergence. If it fails, we've caught a bug!
3109
+ */
3110
+ const currentContent = `a\nb\nc\nd\ne`; // 5 lines
3111
+ const codeEdit = `1\n2\n3\n4`; // 4 lines = 80% of 5
3112
+
3113
+ const result = verifyTUIMatchesProduction(
3114
+ "80% rule divergence check",
3115
+ currentContent,
3116
+ codeEdit,
3117
+ false,
3118
+ );
3119
+
3120
+ // Document what each does
3121
+ console.log("[80% RULE CHECK]");
3122
+ console.log(
3123
+ ` TUI preview thinks: ${result.tuiResult === codeEdit ? "REPLACE ALL (80% rule)" : "APPEND"}`,
3124
+ );
3125
+ console.log(
3126
+ ` Production thinks: ${result.prodResult === codeEdit ? "REPLACE ALL" : "APPEND"}`,
3127
+ );
3128
+
3129
+ if (!result.match) {
3130
+ console.log("");
3131
+ console.log("⚠️ WARNING: TUI and Production DISAGREE on 80% rule!");
3132
+ console.log(' User sees: "Replace entire file"');
3133
+ console.log(' Actually: "Append to end"');
3134
+ console.log("");
3135
+ console.log(" This is a KNOWN BUG - TUI lies to user!");
3136
+ }
3137
+
3138
+ // We expect this to potentially fail - that's the point!
3139
+ // If it fails, we've documented a real bug
3140
+ if (!result.match) {
3141
+ console.log("\n🐛 BUG DETECTED: TUI preview uses 80% rule, production does not!");
3142
+ }
3143
+ });
3144
+
3145
+ it("SYNC: Adding console.log via line replacement - must match", () => {
3146
+ const currentContent = `function greet(name) {\n return 'Hello ' + name;\n}`;
3147
+ const codeEdit = ` console.log('greet called');\n return 'Hello ' + name;`;
3148
+
3149
+ const result = verifyTUIMatchesProduction(
3150
+ "Console.log via line replacement",
3151
+ currentContent,
3152
+ codeEdit,
3153
+ false,
3154
+ 2, // startLine
3155
+ 2, // endLine
3156
+ );
3157
+
3158
+ expect(result.match).toBe(true);
3159
+ expect(result.tuiResult).toContain("console.log");
3160
+ expect(result.prodResult).toContain("console.log");
3161
+ });
3162
+
3163
+ it("SYNC: Multi-line insertion - must match", () => {
3164
+ const currentContent = `class Foo {\n constructor() {}\n}`;
3165
+ const codeEdit = ` constructor() {\n console.log('created');\n }\n\n doStuff() {\n console.log('doing');\n }`;
3166
+
3167
+ const result = verifyTUIMatchesProduction(
3168
+ "Multi-line insertion",
3169
+ currentContent,
3170
+ codeEdit,
3171
+ false,
3172
+ 2, // startLine
3173
+ 2, // endLine
3174
+ );
3175
+
3176
+ expect(result.match).toBe(true);
3177
+ });
3178
+
3179
+ it("SYNC: New file (no current content) - must match", () => {
3180
+ const currentContent = ""; // Empty/new file
3181
+ void currentContent; // Intentionally unused - documents the test scenario
3182
+ const codeEdit = `console.log('new file');\nmodule.exports = {};`;
3183
+
3184
+ // For new files, both should just use codeEdit
3185
+ const tuiResult = codeEdit; // Preview shows the new content
3186
+ const prodResult = codeEdit; // Production creates with codeEdit
3187
+
3188
+ expect(tuiResult).toBe(prodResult);
3189
+ });
3190
+
3191
+ it("SYNC: Complex React component edit - must match", () => {
3192
+ const currentContent = `function Button({ label }) {\n return <button>{label}</button>;\n}`;
3193
+ const codeEdit = `function Button({ label, onClick }) {\n return <button onClick={onClick}>{label}</button>;\n}`;
3194
+
3195
+ const result = verifyTUIMatchesProduction(
3196
+ "React component edit",
3197
+ currentContent,
3198
+ codeEdit,
3199
+ true, // Overwrite - safest option
3200
+ );
3201
+
3202
+ expect(result.match).toBe(true);
3203
+ });
3204
+ });
3205
+
3206
+ /**
3207
+ * =============================================================================
3208
+ * INTEGRATION TEST: Full Flow with Real Production Logic
3209
+ * =============================================================================
3210
+ * This test simulates the ACTUAL production flow:
3211
+ * 1. Create a real file
3212
+ * 2. Render TUI preview (what user sees)
3213
+ * 3. Apply edit using production logic
3214
+ * 4. Verify file matches what TUI showed
3215
+ * =============================================================================
3216
+ */
3217
+
3218
+ describe.concurrent("INTEGRATION: Real Production Flow", () => {
3219
+ let testDir: string;
3220
+
3221
+ beforeEach(async () => {
3222
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), "prod-flow-test-"));
3223
+ });
3224
+
3225
+ afterEach(async () => {
3226
+ cleanup();
3227
+ try {
3228
+ await fs.rm(testDir, { recursive: true, force: true });
3229
+ } catch {}
3230
+ });
3231
+
3232
+ it("INTEGRATION: TUI preview must match production file write", async () => {
3233
+ const testFile = path.join(testDir, "integration-test.js");
3234
+
3235
+ // Step 1: Create original file
3236
+ const originalContent = `function process(data) {
3237
+ return data.map(x => x.value);
3238
+ }`;
3239
+ await fs.writeFile(testFile, originalContent, "utf-8");
3240
+
3241
+ // Step 2: Define edit
3242
+ const codeEdit = `function process(data) {
3243
+ console.log('INTEGRATION_MARKER_1');
3244
+ console.log('INTEGRATION_MARKER_2');
3245
+ return data.map(x => x.value);
3246
+ }`;
3247
+
3248
+ // Step 3: Calculate what TUI would show
3249
+ const tuiExpected = tuiPreviewLogic(originalContent, codeEdit, true);
3250
+
3251
+ // Step 4: Calculate what production would write
3252
+ const prodExpected = productionFallbackLogic(originalContent, codeEdit, true);
3253
+
3254
+ // Step 5: Render TUI preview
3255
+ const toolData = createMockToolPermission({
3256
+ targetFile: testFile,
3257
+ codeEdit: codeEdit,
3258
+ currentFileContent: originalContent,
3259
+ fileExists: true,
3260
+ overwriteFile: true,
3261
+ });
3262
+ const instance = renderWithTheme(<EditToolPreview queuedTool={toolData} />);
3263
+ const { plainText } = displayTUI("INTEGRATION TEST", instance);
3264
+
3265
+ // Step 6: Apply production logic (simulate what editTool does)
3266
+ await fs.writeFile(testFile, prodExpected, "utf-8");
3267
+ const actualContent = await fs.readFile(testFile, "utf-8");
3268
+
3269
+ // Step 7: VERIFY ALL THREE MATCH
3270
+ console.log("\n[INTEGRATION] Verifying TUI, Production, and File all match...");
3271
+ console.log(` TUI expected length: ${tuiExpected.length}`);
3272
+ console.log(` Prod expected length: ${prodExpected.length}`);
3273
+ console.log(` Actual file length: ${actualContent.length}`);
3274
+
3275
+ // TUI must match production
3276
+ expect(tuiExpected).toBe(prodExpected);
3277
+
3278
+ // File must match production
3279
+ expect(actualContent).toBe(prodExpected);
3280
+
3281
+ // TUI must show the markers
3282
+ expect(plainText).toContain("INTEGRATION_MARKER_1");
3283
+ expect(plainText).toContain("INTEGRATION_MARKER_2");
3284
+
3285
+ // File must have the markers
3286
+ expect(actualContent).toContain("INTEGRATION_MARKER_1");
3287
+ expect(actualContent).toContain("INTEGRATION_MARKER_2");
3288
+
3289
+ console.log("[INTEGRATION] ✓ All three match - TUI told the truth!");
3290
+ });
3291
+
3292
+ it("INTEGRATION: Line range edit - TUI must predict exact file content", async () => {
3293
+ const testFile = path.join(testDir, "line-range-integration.js");
3294
+
3295
+ const originalContent = `// Header\nconst OLD_LINE = 1;\nconst UNCHANGED = 2;\n// Footer`;
3296
+ await fs.writeFile(testFile, originalContent, "utf-8");
3297
+
3298
+ const codeEdit = `const NEW_LINE_A = 'replaced';\nconst NEW_LINE_B = 'also replaced';`;
3299
+
3300
+ // TUI prediction
3301
+ const tuiExpected = tuiPreviewLogic(originalContent, codeEdit, false, 2, 2);
3302
+
3303
+ // Production prediction
3304
+ const prodExpected = productionFallbackLogic(originalContent, codeEdit, false, 2, 2);
3305
+
3306
+ // Verify they match BEFORE writing
3307
+ expect(tuiExpected).toBe(prodExpected);
3308
+
3309
+ // Write using production logic
3310
+ await fs.writeFile(testFile, prodExpected, "utf-8");
3311
+ const actualContent = await fs.readFile(testFile, "utf-8");
3312
+
3313
+ // File must match what we predicted
3314
+ expect(actualContent).toBe(tuiExpected);
3315
+
3316
+ // Verify structure
3317
+ expect(actualContent).toContain("// Header");
3318
+ expect(actualContent).toContain("NEW_LINE_A");
3319
+ expect(actualContent).toContain("NEW_LINE_B");
3320
+ expect(actualContent).toContain("UNCHANGED");
3321
+ expect(actualContent).toContain("// Footer");
3322
+ expect(actualContent).not.toContain("OLD_LINE");
3323
+
3324
+ console.log("[INTEGRATION] ✓ Line range edit matches prediction");
3325
+ });
3326
+
3327
+ it("INTEGRATION: If this test fails, PRODUCTION HAS A BUG", async () => {
3328
+ /**
3329
+ * This test is designed to fail if there's any divergence.
3330
+ * It tests multiple scenarios and reports ALL failures.
3331
+ */
3332
+ const testCases = [
3333
+ {
3334
+ name: "Simple overwrite",
3335
+ original: "old content",
3336
+ codeEdit: "new content",
3337
+ overwrite: true,
3338
+ },
3339
+ {
3340
+ name: "Line replacement",
3341
+ original: "line1\nline2\nline3",
3342
+ codeEdit: "REPLACED",
3343
+ overwrite: false,
3344
+ startLine: 2,
3345
+ endLine: 2,
3346
+ },
3347
+ {
3348
+ name: "Append with instructions",
3349
+ original: "const x = 1;",
3350
+ codeEdit: "const y = 2;",
3351
+ overwrite: false,
3352
+ instructions: "Add variable",
3353
+ },
3354
+ {
3355
+ name: "Content already present",
3356
+ original: "const x = 1;\nconsole.log(x);",
3357
+ codeEdit: "console.log(x);",
3358
+ overwrite: false,
3359
+ },
3360
+ ];
3361
+
3362
+ const failures: string[] = [];
3363
+
3364
+ for (const tc of testCases) {
3365
+ const tuiResult = tuiPreviewLogic(
3366
+ tc.original,
3367
+ tc.codeEdit,
3368
+ tc.overwrite,
3369
+ tc.startLine,
3370
+ tc.endLine,
3371
+ tc.instructions,
3372
+ );
3373
+ const prodResult = productionFallbackLogic(
3374
+ tc.original,
3375
+ tc.codeEdit,
3376
+ tc.overwrite,
3377
+ tc.startLine,
3378
+ tc.endLine,
3379
+ tc.instructions,
3380
+ );
3381
+
3382
+ if (tuiResult !== prodResult) {
3383
+ failures.push(`${tc.name}: TUI != Production`);
3384
+ console.log(`\n[FAILURE] ${tc.name}`);
3385
+ console.log(` TUI: ${JSON.stringify(tuiResult)}`);
3386
+ console.log(` PROD: ${JSON.stringify(prodResult)}`);
3387
+ } else {
3388
+ console.log(`[PASS] ${tc.name}`);
3389
+ }
3390
+ }
3391
+
3392
+ if (failures.length > 0) {
3393
+ console.log(`\n🐛 ${failures.length} PRODUCTION BUGS DETECTED:`);
3394
+ failures.forEach((f) => console.log(` - ${f}`));
3395
+ }
3396
+
3397
+ expect(failures).toHaveLength(0);
3398
+ });
3399
+ });