diff --git a/src/services/tools/file_edit_insert.test.ts b/src/services/tools/file_edit_insert.test.ts index c056e5e8dc..01b14dc87d 100644 --- a/src/services/tools/file_edit_insert.test.ts +++ b/src/services/tools/file_edit_insert.test.ts @@ -329,4 +329,77 @@ describe("file_edit_insert tool", () => { expect(result.error).toContain("beyond file length"); } }); + + it("should handle content with trailing newline correctly (no double newlines)", async () => { + // This test verifies the fix for the terminal-bench "hello-world" bug + // where content with \n at the end was getting an extra newline added + using testEnv = createTestFileEditInsertTool({ cwd: testDir }); + const tool = testEnv.tool; + const args: FileEditInsertToolArgs = { + file_path: "newfile.txt", + line_offset: 0, + content: "Hello, world!\n", // Content already has trailing newline + create: true, + }; + + // Execute + const result = (await tool.execute!(args, mockToolCallOptions)) as FileEditInsertToolResult; + + // Assert + expect(result.success).toBe(true); + + const fileContent = await fs.readFile(path.join(testDir, "newfile.txt"), "utf-8"); + // Should NOT have double newline - the trailing \n in content should be preserved as-is + expect(fileContent).toBe("Hello, world!\n"); + expect(fileContent).not.toBe("Hello, world!\n\n"); + }); + + it("should handle multiline content with trailing newline", async () => { + // Setup + const initialContent = "line1\nline2"; + await fs.writeFile(testFilePath, initialContent); + + using testEnv = createTestFileEditInsertTool({ cwd: testDir }); + const tool = testEnv.tool; + const args: FileEditInsertToolArgs = { + file_path: "test.txt", + line_offset: 1, + content: "INSERTED1\nINSERTED2\n", // Multiline with trailing newline + }; + + // Execute + const result = (await tool.execute!(args, mockToolCallOptions)) as FileEditInsertToolResult; + + // Assert + expect(result.success).toBe(true); + + const updatedContent = await fs.readFile(testFilePath, "utf-8"); + // Should respect the trailing newline in content + expect(updatedContent).toBe("line1\nINSERTED1\nINSERTED2\nline2"); + }); + + it("should preserve trailing newline when appending to file without trailing newline", async () => { + // Regression test for Codex feedback: when inserting at EOF with content ending in \n, + // the newline should be preserved even if the original file doesn't end with one + const initialContent = "line1\nline2"; // No trailing newline + await fs.writeFile(testFilePath, initialContent); + + using testEnv = createTestFileEditInsertTool({ cwd: testDir }); + const tool = testEnv.tool; + const args: FileEditInsertToolArgs = { + file_path: "test.txt", + line_offset: 2, // Append at end + content: "line3\n", // With trailing newline + }; + + // Execute + const result = (await tool.execute!(args, mockToolCallOptions)) as FileEditInsertToolResult; + + // Assert + expect(result.success).toBe(true); + + const updatedContent = await fs.readFile(testFilePath, "utf-8"); + // Should preserve the trailing newline from content even at EOF + expect(updatedContent).toBe("line1\nline2\nline3\n"); + }); }); diff --git a/src/services/tools/file_edit_insert.ts b/src/services/tools/file_edit_insert.ts index 340dc838a4..6a2b049525 100644 --- a/src/services/tools/file_edit_insert.ts +++ b/src/services/tools/file_edit_insert.ts @@ -93,7 +93,20 @@ export const createFileEditInsertTool: ToolFactory = (config: ToolConfiguration) }; } - const newLines = [...lines.slice(0, line_offset), content, ...lines.slice(line_offset)]; + // Handle newline behavior: + // - If content ends with \n and we're not at EOF, strip it (join will add it back) + // - If content ends with \n and we're at EOF, keep it (join won't add trailing newline) + // - If content doesn't end with \n, keep as-is (join will add newlines between lines) + const contentEndsWithNewline = content.endsWith("\n"); + const insertingAtEnd = line_offset === lines.length; + const shouldStripTrailingNewline = contentEndsWithNewline && !insertingAtEnd; + const normalizedContent = shouldStripTrailingNewline ? content.slice(0, -1) : content; + + const newLines = [ + ...lines.slice(0, line_offset), + normalizedContent, + ...lines.slice(line_offset), + ]; const newContent = newLines.join("\n"); return {