diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index ea1de4a8..c285c5c4 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -39,6 +39,8 @@ import { createContextCompactionCompleteUpdate, createContextCompactionStartUpdate, createDynamicToolCallUpdate, + createFileChangeCompleteUpdate, + createFileChangePatchUpdate, createFileChangeUpdate, createGuardianApprovalReviewToolCall, createGuardianApprovalReviewToolCallUpdate, @@ -185,6 +187,8 @@ export class CodexEventHandler { return this.createThreadGoalClearedEvent(notification.params); case "item/commandExecution/terminalInteraction": return this.createTerminalInteractionEvent(notification.params); + case "item/fileChange/patchUpdated": + return await createFileChangePatchUpdate(notification.params); // ignored events case "thread/deleted": case "command/exec/outputDelta": @@ -193,7 +197,6 @@ export class CodexEventHandler { case "turn/diff/updated": case "turn/moderationMetadata": case "item/fileChange/outputDelta": - case "item/fileChange/patchUpdated": case "account/updated": case "fs/changed": case "mcpServer/startupStatus/updated": @@ -360,6 +363,7 @@ export class CodexEventHandler { private async completeItemEvent(event: ItemCompletedNotification): Promise { switch (event.item.type) { case "fileChange": + return createFileChangeCompleteUpdate(event.item); case "dynamicToolCall": return { sessionUpdate: "tool_call_update", diff --git a/src/CodexToolCallMapper.ts b/src/CodexToolCallMapper.ts index 20465fc2..810202bd 100644 --- a/src/CodexToolCallMapper.ts +++ b/src/CodexToolCallMapper.ts @@ -1,6 +1,5 @@ -import type { ContentBlock, ToolCallContent } from "@agentclientprotocol/sdk"; -import { applyPatch, parsePatch, reversePatch } from "diff"; -import { readFile } from "node:fs/promises"; +import type { ContentBlock, ToolCallContent, ToolCallLocation } from "@agentclientprotocol/sdk"; +import { parsePatch, type StructuredPatchHunk } from "diff"; import path from "node:path"; import type { UpdateSessionEvent } from "./ACPSessionConnection"; import { stripShellPrefix } from "./CommandUtils"; @@ -13,6 +12,7 @@ import type { CommandAction, CommandExecutionStatus, DynamicToolCallStatus, + FileChangePatchUpdatedNotification, FileUpdateChange, GuardianApprovalReview, GuardianApprovalReviewAction, @@ -41,6 +41,7 @@ type GuardianApprovalReviewNotification = type WebSearchItem = ThreadItem & { type: "webSearch" }; type CollabAgentToolCallItem = ThreadItem & { type: "collabAgentToolCall" }; type CommandExecutionItem = ThreadItem & { type: "commandExecution" }; +type FileChangeItem = ThreadItem & { type: "fileChange" }; type ContextCompactionItem = ThreadItem & { type: "contextCompaction" }; type AcpToolCallEvent = Extract; @@ -59,21 +60,41 @@ function toAcpStatus(status: CodexItemStatus): AcpToolCallStatus { } export async function createFileChangeUpdate( - item: ThreadItem & { type: "fileChange" } + item: FileChangeItem ): Promise { - const patches: ToolCallContent[] = []; - for (const change of item.changes) { - const content = await createPatchContent(change); - if (content) patches.push(content); - // ignore unparseable diffs - } + const rawOutput = createFileChangeRawOutput(item); return { sessionUpdate: "tool_call", toolCallId: item.id, title: "Editing files", kind: "edit", status: toAcpStatus(item.status), - content: patches, + content: await createFileChangeContent(item.changes), + locations: createFileChangeLocations(item.changes), + rawInput: createFileChangeRawInput(item.changes), + ...(rawOutput === undefined ? {} : { rawOutput }), + }; +} + +export async function createFileChangePatchUpdate( + notification: FileChangePatchUpdatedNotification, +): Promise { + return { + sessionUpdate: "tool_call_update", + toolCallId: notification.itemId, + status: "in_progress", + content: await createFileChangeContent(notification.changes), + locations: createFileChangeLocations(notification.changes), + rawInput: createFileChangeRawInput(notification.changes), + }; +} + +export function createFileChangeCompleteUpdate(item: FileChangeItem): UpdateSessionEvent { + return { + sessionUpdate: "tool_call_update", + toolCallId: item.id, + status: toAcpStatus(item.status), + rawOutput: createFileChangeRawOutput(item), }; } @@ -734,23 +755,31 @@ function createContent(content: ContentBlock): ToolCallContent { }; } -async function createPatchContent(change: FileUpdateChange): Promise { +async function createFileChangeContent(changes: FileUpdateChange[]): Promise { + const content: ToolCallContent[] = []; + for (const change of changes) { + content.push(...await createPatchContent(change)); + } + return content; +} + +async function createPatchContent(change: FileUpdateChange): Promise { try { switch (change.kind.type) { case "add": - return await createAddFileContent(change); + return [createAddFileContent(change)]; case "delete": - return await createDeleteFileContent(change); + return [createDeleteFileContent(change)]; case "update": - return await createUpdateFileContent(change); + return createUpdateFileContent(change); } } catch (error) { logger.log(`Error processing file update change: ${error}`); - return null; + return []; } } -async function createAddFileContent(change: FileUpdateChange): Promise { +function createAddFileContent(change: FileUpdateChange): ToolCallContent { return { type: "diff", oldText: null, @@ -762,63 +791,46 @@ async function createAddFileContent(change: FileUpdateChange): Promise { - if (change.kind.type !== "update") return null; - - const unifiedDiff = recoverCorruptedDiff(change.diff); - const movePath = change.kind.move_path; - - const oldContent = await readFileContent(change.path); - if (oldContent !== null) { - const patchedContent = applyPatch(oldContent, unifiedDiff); - if (patchedContent === false) { - // If Codex runs in full access mode, the file might already be patched. - // we can verify this by checking if the reverted patch applies. - const revertedPatch = revertPatch(unifiedDiff); - if (revertedPatch) { - const revertedContent = applyPatch(oldContent, revertedPatch); - if (revertedContent !== false) { - return createUpdateDiffContent(change.path, revertedContent, oldContent); - } - } - return null; - } - return createUpdateDiffContent(movePath ?? change.path, oldContent, patchedContent); - } - - if (!movePath) return null; - const newContent = await readFileContent(movePath); - if (newContent === null) return null; - - const revertedPatch = revertPatch(unifiedDiff); - if (!revertedPatch) return null; - - const revertedContent = applyPatch(newContent, revertedPatch); - if (revertedContent === false) return null; - - return createUpdateDiffContent(movePath, revertedContent, newContent); -} - -function revertPatch(unifiedDiff: string) { - const [patch] = parsePatch(unifiedDiff); - if (!patch) return null; +function createUpdateFileContent(change: FileUpdateChange): ToolCallContent[] { + if (change.kind.type !== "update") return []; - return reversePatch(patch); + const patches = parsePatch(recoverCorruptedDiff(change.diff)); + const targetPath = change.kind.move_path ?? change.path; + return patches.flatMap((patch) => patch.hunks.map((hunk) => createUpdateDiffContent(targetPath, hunk))); } -function createUpdateDiffContent(path: string, oldText: string, newText: string): ToolCallContent { +function createUpdateDiffContent(path: string, hunk: StructuredPatchHunk): ToolCallContent { return { type: "diff", - oldText, - newText, + oldText: createHunkText(hunk, "old"), + newText: createHunkText(hunk, "new"), path, _meta: { kind: "update", + old_start: hunk.oldStart, + new_start: hunk.newStart, }, }; } -async function createDeleteFileContent(change: FileUpdateChange): Promise { +function createHunkText(hunk: StructuredPatchHunk, side: "old" | "new"): string { + return hunk.lines.flatMap((line): string[] => { + switch (line[0]) { + case " ": + return [line.slice(1)]; + case "-": + return side === "old" ? [line.slice(1)] : []; + case "+": + return side === "new" ? [line.slice(1)] : []; + case "\\": + return []; + default: + return []; + } + }).join("\n"); +} + +function createDeleteFileContent(change: FileUpdateChange): ToolCallContent { return { type: "diff", oldText: change.diff, // app-server always returns file content instead of diff @@ -830,8 +842,56 @@ async function createDeleteFileContent(change: FileUpdateChange): Promise { - return await readFile(filePath, { encoding: "utf8" }).catch(() => null); +function createFileChangeLocations(changes: FileUpdateChange[]): ToolCallLocation[] { + const locations = new Map(); + const addLocation = (filePath: string, line?: number) => { + const current = locations.get(filePath); + if (current?.line !== undefined || (current && line === undefined)) { + return; + } + locations.set(filePath, line === undefined ? { path: filePath } : { path: filePath, line }); + }; + + for (const change of changes) { + switch (change.kind.type) { + case "add": + case "delete": + addLocation(change.path); + break; + case "update": { + const firstHunk = firstUpdateHunk(change); + if (change.kind.move_path && change.kind.move_path !== change.path) { + addLocation(change.path, firstHunk?.oldStart); + } + addLocation(change.kind.move_path ?? change.path, firstHunk?.newStart); + break; + } + } + } + + return [...locations.values()]; +} + +function firstUpdateHunk(change: FileUpdateChange): StructuredPatchHunk | undefined { + try { + return parsePatch(recoverCorruptedDiff(change.diff))[0]?.hunks[0]; + } catch { + return undefined; + } +} + +function createFileChangeRawInput(changes: FileUpdateChange[]) { + return { changes }; +} + +function createFileChangeRawOutput(item: FileChangeItem): Record | undefined { + if (item.status === "inProgress") { + return undefined; + } + return { + status: item.status, + success: item.status === "completed", + }; } /** diff --git a/src/__tests__/CodexACPAgent/data/file-change-add-multiple-files.json b/src/__tests__/CodexACPAgent/data/file-change-add-multiple-files.json index 41b58b87..f6f8a75e 100644 --- a/src/__tests__/CodexACPAgent/data/file-change-add-multiple-files.json +++ b/src/__tests__/CodexACPAgent/data/file-change-add-multiple-files.json @@ -28,7 +28,37 @@ "kind": "add" } } - ] + ], + "locations": [ + { + "path": "/test/project/FileA.kt" + }, + { + "path": "/test/project/FileB.kt" + } + ], + "rawInput": { + "changes": [ + { + "path": "/test/project/FileA.kt", + "kind": { + "type": "add" + }, + "diff": "class FileA\n" + }, + { + "path": "/test/project/FileB.kt", + "kind": { + "type": "add" + }, + "diff": "class FileB\n" + } + ] + }, + "rawOutput": { + "status": "completed", + "success": true + } } } ] diff --git a/src/__tests__/CodexACPAgent/data/file-change-add-new-file.json b/src/__tests__/CodexACPAgent/data/file-change-add-new-file.json index 4bf59ca9..9b092e1c 100644 --- a/src/__tests__/CodexACPAgent/data/file-change-add-new-file.json +++ b/src/__tests__/CodexACPAgent/data/file-change-add-new-file.json @@ -19,7 +19,27 @@ "kind": "add" } } - ] + ], + "locations": [ + { + "path": "/test/project/NewFile.kt" + } + ], + "rawInput": { + "changes": [ + { + "path": "/test/project/NewFile.kt", + "kind": { + "type": "add" + }, + "diff": "package test.project\n\nclass NewFile {\n fun hello() = \"Hello\"\n}\n" + } + ] + }, + "rawOutput": { + "status": "completed", + "success": true + } } } ] diff --git a/src/__tests__/CodexACPAgent/data/file-change-add-raw-content.json b/src/__tests__/CodexACPAgent/data/file-change-add-raw-content.json index 31f18f5a..3fd138bf 100644 --- a/src/__tests__/CodexACPAgent/data/file-change-add-raw-content.json +++ b/src/__tests__/CodexACPAgent/data/file-change-add-raw-content.json @@ -19,7 +19,27 @@ "kind": "add" } } - ] + ], + "locations": [ + { + "path": "/test/project/RawFile.kt" + } + ], + "rawInput": { + "changes": [ + { + "path": "/test/project/RawFile.kt", + "kind": { + "type": "add" + }, + "diff": "fun main() {\n println(\"Hello, World!\")\n}\n" + } + ] + }, + "rawOutput": { + "status": "completed", + "success": true + } } } ] diff --git a/src/__tests__/CodexACPAgent/data/file-change-delete-file.json b/src/__tests__/CodexACPAgent/data/file-change-delete-file.json index ea6dd487..4e19711a 100644 --- a/src/__tests__/CodexACPAgent/data/file-change-delete-file.json +++ b/src/__tests__/CodexACPAgent/data/file-change-delete-file.json @@ -19,7 +19,27 @@ "kind": "delete" } } - ] + ], + "locations": [ + { + "path": "/test/project/OldFile.kt" + } + ], + "rawInput": { + "changes": [ + { + "path": "/test/project/OldFile.kt", + "kind": { + "type": "delete" + }, + "diff": "package test.project\n\nclass OldFile {}" + } + ] + }, + "rawOutput": { + "status": "completed", + "success": true + } } } ] diff --git a/src/__tests__/CodexACPAgent/data/file-change-delete-raw-content.json b/src/__tests__/CodexACPAgent/data/file-change-delete-raw-content.json index 60f83da7..cbb2f0ad 100644 --- a/src/__tests__/CodexACPAgent/data/file-change-delete-raw-content.json +++ b/src/__tests__/CodexACPAgent/data/file-change-delete-raw-content.json @@ -19,7 +19,27 @@ "kind": "delete" } } - ] + ], + "locations": [ + { + "path": "/test/project/RawDeleteFile.kt" + } + ], + "rawInput": { + "changes": [ + { + "path": "/test/project/RawDeleteFile.kt", + "kind": { + "type": "delete" + }, + "diff": "fun main() {\n println(\"Hello, World!\")\n}\n" + } + ] + }, + "rawOutput": { + "status": "completed", + "success": true + } } } ] diff --git a/src/__tests__/CodexACPAgent/data/file-change-patch-updated.json b/src/__tests__/CodexACPAgent/data/file-change-patch-updated.json new file mode 100644 index 00000000..14e5eeb1 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/file-change-patch-updated.json @@ -0,0 +1,44 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call_update", + "toolCallId": "file-change-patch-updated", + "status": "in_progress", + "content": [ + { + "type": "diff", + "oldText": "before context\nold value\nafter context", + "newText": "before context\nnew value\nafter context", + "path": "/test/project/UpdatedFile.kt", + "_meta": { + "kind": "update", + "old_start": 40, + "new_start": 40 + } + } + ], + "locations": [ + { + "path": "/test/project/UpdatedFile.kt", + "line": 40 + } + ], + "rawInput": { + "changes": [ + { + "path": "/test/project/UpdatedFile.kt", + "kind": { + "type": "update", + "move_path": null + }, + "diff": "@@ -40,3 +40,3 @@\n before context\n-old value\n+new value\n after context\n" + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/data/load-session-history.json b/src/__tests__/CodexACPAgent/data/load-session-history.json index bc5b3f96..c8292aa6 100644 --- a/src/__tests__/CodexACPAgent/data/load-session-history.json +++ b/src/__tests__/CodexACPAgent/data/load-session-history.json @@ -220,7 +220,27 @@ "kind": "add" } } - ] + ], + "locations": [ + { + "path": "/test/project/Added.txt" + } + ], + "rawInput": { + "changes": [ + { + "path": "/test/project/Added.txt", + "kind": { + "type": "add" + }, + "diff": "Hello\nWorld\n" + } + ] + }, + "rawOutput": { + "status": "completed", + "success": true + } } } ] diff --git a/src/__tests__/CodexACPAgent/file-change-events.test.ts b/src/__tests__/CodexACPAgent/file-change-events.test.ts index 795d71e3..d80ddc06 100644 --- a/src/__tests__/CodexACPAgent/file-change-events.test.ts +++ b/src/__tests__/CodexACPAgent/file-change-events.test.ts @@ -6,44 +6,12 @@ import type { ThreadItem } from '../../app-server/v2'; import { createCodexMockTestFixture, createTestSessionState, setupPromptAndSendNotifications, type CodexMockTestFixture } from '../acp-test-utils'; import {AgentMode} from "../../AgentMode"; -const { mockFiles, mockReadDelays, mockFileContent, delayMockFileRead, removeMockFile, clearMockFiles } = vi.hoisted(() => { - const files = new Map(); - const readDelays = new Map>(); - return { - mockFiles: files, - mockReadDelays: readDelays, - mockFileContent: (path: string, content: string) => files.set(path, content), - delayMockFileRead: (path: string, delay: Promise) => readDelays.set(path, delay), - removeMockFile: (path: string) => files.delete(path), - clearMockFiles: () => { - files.clear(); - readDelays.clear(); - }, - }; -}); - -vi.mock('node:fs/promises', () => ({ - readFile: async (path: string) => { - const delay = mockReadDelays.get(path); - if (delay) { - await delay; - } - const content = mockFiles.get(path); - if (content !== undefined) { - return content; - } - throw new Error(`ENOENT: no such file or directory, open '${path}'`); - }, -})); - describe('CodexEventHandler - file change events', () => { let mockFixture: CodexMockTestFixture; const sessionId = 'test-session-id'; beforeEach(() => { mockFixture = createCodexMockTestFixture(); - clearMockFiles(); - mockFileContent('/test/project/OldFile.kt', 'package test.project\n\nclass OldFile {}'); }); const sessionState: SessionState = createTestSessionState({ @@ -175,8 +143,6 @@ describe('CodexEventHandler - file change events', () => { }); it('should handle file deletion with raw content', async () => { - mockFileContent('/test/project/RawDeleteFile.kt', 'fun main() {\n println("Hello, World!")\n}\n'); - // Codex sends raw file content (not unified diff) for deleted files const deletedFileNotification: ServerNotification = { method: 'item/started', @@ -207,8 +173,6 @@ describe('CodexEventHandler - file change events', () => { }); it('should handle file deletion when old file is already missing', async () => { - removeMockFile('/test/project/OldFile.kt'); - const deleteFileNotification: ServerNotification = { method: 'item/started', params: { @@ -238,8 +202,6 @@ describe('CodexEventHandler - file change events', () => { }); it('should handle file deletion with raw content when old file is already missing', async () => { - removeMockFile('/test/project/RawDeleteFile.kt'); - const deletedFileNotification: ServerNotification = { method: 'item/started', params: { @@ -268,7 +230,7 @@ describe('CodexEventHandler - file change events', () => { ); }); - it('should ignore broken unified diffs in update file changes', async () => { + it('should preserve metadata when an update diff is invalid', async () => { const fileChange: ThreadItem & { type: 'fileChange' } = { type: 'fileChange', id: 'file-change-broken-diff', @@ -290,13 +252,17 @@ describe('CodexEventHandler - file change events', () => { const updateEvent = await createFileChangeUpdate(fileChange); expect(updateEvent).toMatchObject({ content: [], + locations: [{ path: '/test/project/OldFile.kt' }], + rawInput: { changes: fileChange.changes }, + rawOutput: { + status: 'completed', + success: true, + }, }); }); - it('should handle update diffs when the file was already patched', async () => { - mockFileContent('/test/project/OldFile.kt', 'package test.project\n\nclass UpdatedFile {}\n'); - - const updateFileNotification: ServerNotification = { + it('should emit localized update content when the source file is unavailable', async () => { + const updateFileNotification = { method: 'item/started', params: { threadId: sessionId, @@ -310,18 +276,24 @@ describe('CodexEventHandler - file change events', () => { path: '/test/project/OldFile.kt', kind: { type: 'update', move_path: null }, diff: -`@@ -1,3 +1,3 @@ - package test.project - --class OldFile {} -+class UpdatedFile {} +`@@ -18,7 +18,7 @@ + modified_section_4: experiment_id=new_xyz456 status=replaced +-random_operation_3: tool_call_count=10 agent_test=true +-random_operation_4: data_point=value_7390 ++random_operation_3: tool_call_count=12 agent_test=true ++random_operation_4: data_point=value_8462 + random_operation_5: final_entry timestamp=2026-05-02T11:31:23Z + updated_entry_4: replaced_lines=22-23 round=2 op=1 +-updated_entry_5: metrics=[reads=20,writes=10,duration_ms=99999] ++updated_entry_5: metrics=[reads=23,writes=11,duration_ms=98417] + round2_operation_3: test_phase=integration_test status=running `, }, ], status: 'completed', }, }, - }; + } satisfies ServerNotification; await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [updateFileNotification]); @@ -333,24 +305,38 @@ describe('CodexEventHandler - file change events', () => { status: 'completed', content: [ { - oldText: 'package test.project\n\nclass OldFile {}\n', - newText: 'package test.project\n\nclass UpdatedFile {}\n', + oldText: [ + 'modified_section_4: experiment_id=new_xyz456 status=replaced', + 'random_operation_3: tool_call_count=10 agent_test=true', + 'random_operation_4: data_point=value_7390', + 'random_operation_5: final_entry timestamp=2026-05-02T11:31:23Z', + 'updated_entry_4: replaced_lines=22-23 round=2 op=1', + 'updated_entry_5: metrics=[reads=20,writes=10,duration_ms=99999]', + 'round2_operation_3: test_phase=integration_test status=running', + ].join('\n'), + newText: [ + 'modified_section_4: experiment_id=new_xyz456 status=replaced', + 'random_operation_3: tool_call_count=12 agent_test=true', + 'random_operation_4: data_point=value_8462', + 'random_operation_5: final_entry timestamp=2026-05-02T11:31:23Z', + 'updated_entry_4: replaced_lines=22-23 round=2 op=1', + 'updated_entry_5: metrics=[reads=23,writes=11,duration_ms=98417]', + 'round2_operation_3: test_phase=integration_test status=running', + ].join('\n'), path: '/test/project/OldFile.kt', }, ], + locations: [{ path: '/test/project/OldFile.kt', line: 18 }], + rawInput: { changes: updateFileNotification.params.item.changes }, + rawOutput: { + status: 'completed', + success: true, + }, }, ]); }); - it('should not emit completion before a slow file-change start event', async () => { - mockFileContent('/test/project/OldFile.kt', 'package test.project\n\nclass OldFile {}\n'); - - let releaseRead = () => {}; - const blockedRead = new Promise((resolve) => { - releaseRead = resolve; - }); - delayMockFileRead('/test/project/OldFile.kt', blockedRead); - + it('should preserve start-before-completion ordering without file reads', async () => { const fileChange = { type: 'fileChange', id: 'file-change-slow-start', @@ -413,10 +399,6 @@ describe('CodexEventHandler - file change events', () => { mockFixture.sendServerNotification(fileChangeStarted); mockFixture.sendServerNotification(fileChangeCompleted); - await new Promise((resolve) => setTimeout(resolve, 0)); - expect(mockFixture.getAcpConnectionEvents([])).toEqual([]); - - releaseRead(); await vi.waitFor(() => { expect(mockFixture.getAcpConnectionEvents([])).toHaveLength(2); }); @@ -429,8 +411,8 @@ describe('CodexEventHandler - file change events', () => { status: 'in_progress', content: [ { - oldText: 'package test.project\n\nclass OldFile {}\n', - newText: 'package test.project\n\nclass UpdatedFile {}\n', + oldText: 'package test.project\n\nclass OldFile {}', + newText: 'package test.project\n\nclass UpdatedFile {}', path: '/test/project/OldFile.kt', }, ], @@ -439,13 +421,45 @@ describe('CodexEventHandler - file change events', () => { sessionUpdate: 'tool_call_update', toolCallId: 'file-change-slow-start', status: 'completed', + rawOutput: { + status: 'completed', + success: true, + }, }, ]); }); - it('should parse update diffs with move metadata appended', async () => { - mockFileContent('/test/project/OriginalFile.kt', 'old code line\n'); + it('should map file-change patch updates with compact content and metadata', async () => { + const patchUpdated: ServerNotification = { + method: 'item/fileChange/patchUpdated', + params: { + threadId: sessionId, + turnId: 'turn-1', + itemId: 'file-change-patch-updated', + changes: [ + { + path: '/test/project/UpdatedFile.kt', + kind: { type: 'update', move_path: null }, + diff: +`@@ -40,3 +40,3 @@ + before context +-old value ++new value + after context +`, + }, + ], + }, + }; + + await setupPromptAndSendNotifications(mockFixture, sessionId, sessionState, [patchUpdated]); + + await expect(mockFixture.getAcpConnectionDump(['id'])).toMatchFileSnapshot( + 'data/file-change-patch-updated.json' + ); + }); + it('should parse update diffs with move metadata appended', async () => { const fileChange: ThreadItem = { type: 'fileChange', id: 'file-change-move-metadata', @@ -472,8 +486,8 @@ Moved to: /test/project/NewFile.kt`, expect(updateEvent).toMatchObject({ content: [ { - oldText: 'old code line\n', - newText: 'new code line\n', + oldText: 'old code line', + newText: 'new code line', path: '/test/project/NewFile.kt', }, ], @@ -481,8 +495,6 @@ Moved to: /test/project/NewFile.kt`, }); it('should parse update diffs when the original file was moved already', async () => { - mockFileContent('/test/project/NewFile.kt', 'new code line\n'); - const fileChange: ThreadItem = { type: 'fileChange', id: 'file-change-moved-file-exists', @@ -509,8 +521,8 @@ Moved to: /test/project/NewFile.kt`, expect(updateEvent).toMatchObject({ content: [ { - oldText: 'old code line\n', - newText: 'new code line\n', + oldText: 'old code line', + newText: 'new code line', path: '/test/project/NewFile.kt', }, ],