@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.
- package/.oxlintrc.json +8 -0
- package/dist/index.mjs +12603 -0
- package/package.json +7 -39
- package/resources/abacus.ico +0 -0
- package/resources/entitlements.plist +9 -0
- package/src/__e2e__/README.md +196 -0
- package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
- package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
- package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
- package/src/__e2e__/conversation.e2e.test.tsx +56 -0
- package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
- package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
- package/src/__e2e__/helpers/test-helpers.ts +450 -0
- package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
- package/src/__e2e__/llm-models.e2e.test.ts +402 -0
- package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
- package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
- package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
- package/src/__e2e__/repl.e2e.test.tsx +78 -0
- package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
- package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
- package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
- package/src/args.ts +22 -0
- package/src/components/__tests__/react-compiler.test.tsx +78 -0
- package/src/components/__tests__/status-indicator.test.tsx +403 -0
- package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
- package/src/components/composer/agent-mode-indicator.tsx +63 -0
- package/src/components/composer/bash-runner.tsx +54 -0
- package/src/components/composer/commands/default-commands.tsx +615 -0
- package/src/components/composer/commands/handler.tsx +59 -0
- package/src/components/composer/commands/picker.tsx +273 -0
- package/src/components/composer/commands/registry.ts +233 -0
- package/src/components/composer/commands/types.ts +33 -0
- package/src/components/composer/context.tsx +88 -0
- package/src/components/composer/file-mention-picker.tsx +83 -0
- package/src/components/composer/help.tsx +44 -0
- package/src/components/composer/index.tsx +1006 -0
- package/src/components/composer/mentions.ts +57 -0
- package/src/components/composer/message-queue.tsx +70 -0
- package/src/components/composer/mode-panel.tsx +35 -0
- package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
- package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
- package/src/components/composer/modes/bash-handler.tsx +132 -0
- package/src/components/composer/modes/bash-renderer.tsx +175 -0
- package/src/components/composer/modes/default-handlers.tsx +33 -0
- package/src/components/composer/modes/index.ts +41 -0
- package/src/components/composer/modes/types.ts +21 -0
- package/src/components/composer/persistent-shell.ts +283 -0
- package/src/components/composer/process.ts +65 -0
- package/src/components/composer/types.ts +9 -0
- package/src/components/composer/use-mention-search.ts +68 -0
- package/src/components/error-boundry.tsx +60 -0
- package/src/components/exit-message.tsx +29 -0
- package/src/components/expanded-view.tsx +74 -0
- package/src/components/file-completion.tsx +127 -0
- package/src/components/header.tsx +47 -0
- package/src/components/logo.tsx +37 -0
- package/src/components/segments.tsx +356 -0
- package/src/components/status-indicator.tsx +306 -0
- package/src/components/tool-group-summary.tsx +263 -0
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
- package/src/components/tool-permissions/diff-preview.tsx +355 -0
- package/src/components/tool-permissions/index.ts +5 -0
- package/src/components/tool-permissions/permission-options.tsx +375 -0
- package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
- package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
- package/src/components/tools/agent/ask-user-question.tsx +101 -0
- package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
- package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
- package/src/components/tools/agent/handoff-to-main.tsx +27 -0
- package/src/components/tools/agent/subagent.tsx +37 -0
- package/src/components/tools/agent/todo-write.tsx +104 -0
- package/src/components/tools/browser/close-tab.tsx +58 -0
- package/src/components/tools/browser/computer.tsx +70 -0
- package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
- package/src/components/tools/browser/get-tab-content.tsx +51 -0
- package/src/components/tools/browser/navigate-to.tsx +59 -0
- package/src/components/tools/browser/new-tab.tsx +60 -0
- package/src/components/tools/browser/perform-action.tsx +63 -0
- package/src/components/tools/browser/refresh-tab.tsx +43 -0
- package/src/components/tools/browser/switch-tab.tsx +58 -0
- package/src/components/tools/filesystem/delete-file.tsx +104 -0
- package/src/components/tools/filesystem/edit.tsx +220 -0
- package/src/components/tools/filesystem/list-dir.tsx +78 -0
- package/src/components/tools/filesystem/read-file.tsx +180 -0
- package/src/components/tools/filesystem/upload-image.tsx +76 -0
- package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
- package/src/components/tools/index.ts +91 -0
- package/src/components/tools/mcp/mcp-tool.tsx +158 -0
- package/src/components/tools/search/fetch-url.tsx +73 -0
- package/src/components/tools/search/file-search.tsx +78 -0
- package/src/components/tools/search/grep.tsx +90 -0
- package/src/components/tools/search/semantic-search.tsx +66 -0
- package/src/components/tools/search/web-search.tsx +71 -0
- package/src/components/tools/shared/index.tsx +48 -0
- package/src/components/tools/shared/zod-coercion.ts +35 -0
- package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
- package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
- package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
- package/src/components/tools/types.ts +16 -0
- package/src/components/tools.tsx +66 -0
- package/src/components/ui/__tests__/divider.test.tsx +61 -0
- package/src/components/ui/__tests__/gradient.test.tsx +125 -0
- package/src/components/ui/__tests__/input.test.tsx +166 -0
- package/src/components/ui/__tests__/select.test.tsx +273 -0
- package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
- package/src/components/ui/blinking-indicator.tsx +25 -0
- package/src/components/ui/divider.tsx +162 -0
- package/src/components/ui/gradient.tsx +56 -0
- package/src/components/ui/input.tsx +228 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/shimmer.tsx +84 -0
- package/src/context/agent-mode.tsx +95 -0
- package/src/context/extension-file.tsx +136 -0
- package/src/context/network-activity.tsx +45 -0
- package/src/context/notification.tsx +62 -0
- package/src/context/shell-size.tsx +49 -0
- package/src/context/shell-title.tsx +38 -0
- package/src/entrypoints/print-mode.ts +312 -0
- package/src/entrypoints/repl.tsx +401 -0
- package/src/hooks/use-agent.ts +15 -0
- package/src/hooks/use-api-client.ts +1 -0
- package/src/hooks/use-available-height.ts +8 -0
- package/src/hooks/use-cleanup.ts +29 -0
- package/src/hooks/use-interrupt-manager.ts +242 -0
- package/src/hooks/use-models.ts +22 -0
- package/src/index.ts +217 -0
- package/src/lib/__tests__/ansi.test.ts +255 -0
- package/src/lib/__tests__/cli.test.ts +122 -0
- package/src/lib/__tests__/commands.test.ts +325 -0
- package/src/lib/__tests__/constants.test.ts +15 -0
- package/src/lib/__tests__/focusables.test.ts +25 -0
- package/src/lib/__tests__/fs.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.tsx +348 -0
- package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
- package/src/lib/__tests__/mcpManagement.test.ts +38 -0
- package/src/lib/__tests__/path-paste.test.ts +144 -0
- package/src/lib/__tests__/path.test.ts +300 -0
- package/src/lib/__tests__/queries.test.ts +39 -0
- package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
- package/src/lib/__tests__/text-buffer.test.ts +328 -0
- package/src/lib/__tests__/text-utils.test.ts +32 -0
- package/src/lib/__tests__/timing.test.ts +78 -0
- package/src/lib/__tests__/utils.test.ts +238 -0
- package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
- package/src/lib/ansi.ts +150 -0
- package/src/lib/cli-push-server.ts +112 -0
- package/src/lib/cli.ts +44 -0
- package/src/lib/clipboard.ts +226 -0
- package/src/lib/command-utils.ts +93 -0
- package/src/lib/commands.ts +270 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/extension-connection.ts +181 -0
- package/src/lib/focusables.ts +7 -0
- package/src/lib/fs.ts +533 -0
- package/src/lib/markdown/code-block.tsx +63 -0
- package/src/lib/markdown/index.ts +4 -0
- package/src/lib/markdown/link.tsx +19 -0
- package/src/lib/markdown/markdown.tsx +372 -0
- package/src/lib/markdown/types.ts +15 -0
- package/src/lib/mcpCommandHandler.ts +121 -0
- package/src/lib/mcpManagement.ts +44 -0
- package/src/lib/path-paste.ts +185 -0
- package/src/lib/path.ts +179 -0
- package/src/lib/queries.ts +15 -0
- package/src/lib/standaloneMcpService.ts +688 -0
- package/src/lib/status-utils.ts +237 -0
- package/src/lib/test-utils.tsx +72 -0
- package/src/lib/text-buffer.ts +2415 -0
- package/src/lib/text-utils.ts +272 -0
- package/src/lib/timing.ts +63 -0
- package/src/lib/types.ts +295 -0
- package/src/lib/utils.ts +182 -0
- package/src/lib/vim-buffer-actions.ts +732 -0
- package/src/providers/agent.tsx +1075 -0
- package/src/providers/api-client.tsx +43 -0
- package/src/services/logger.ts +85 -0
- package/src/terminal/detection.ts +187 -0
- package/src/terminal/exit.ts +279 -0
- package/src/terminal/notification.ts +83 -0
- package/src/terminal/progress.ts +201 -0
- package/src/terminal/setup.ts +797 -0
- package/src/terminal/suspend.ts +58 -0
- package/src/terminal/types.ts +51 -0
- package/src/theme/context.tsx +57 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/themed.tsx +35 -0
- package/src/theme/themes.json +546 -0
- package/src/theme/types.ts +110 -0
- package/src/tools/types.ts +59 -0
- package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
- package/src/tools/utils/tool-ui-components.tsx +631 -0
- package/src/tools/utils/zod-coercion.ts +35 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +29 -0
- package/tsconfig.test.json +27 -0
- package/tsdown.config.ts +17 -0
- package/vitest.config.ts +76 -0
- package/README.md +0 -28
- 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
|
+
});
|