// src/tools/file/edit.test.ts // Copyright (C) 2025 DTP Technologies, LLC // All Rights Reserved import { describe, it, expect, beforeEach, afterEach } from "vitest"; import fs from "node:fs/promises"; import path from "node:path"; import { FileEditTool } from "./edit.js"; import { PROJECT_ROOT } from "../../config/env.js"; describe("FileEditTool", () => { let tool: FileEditTool; let mockSession: any; const testFilePath = "test-edit-file.txt"; const testFileContent = "Hello World\nThis is a test file\nGoodbye World\n"; beforeEach(async () => { tool = new FileEditTool(); mockSession = { _id: "test-session-id", user: "test-user-id", }; // Create test file await fs.writeFile( path.join(PROJECT_ROOT, testFilePath), testFileContent, "utf-8", ); }); afterEach(async () => { // Clean up test file try { await fs.unlink(path.join(PROJECT_ROOT, testFilePath)); } catch { // Ignore if file doesn't exist } }); describe("definition", () => { it("should have correct tool name", () => { expect(tool.definition.function.name).toBe("file_edit"); }); it("should have correct definition structure for AI clients", () => { const def = tool.definition; expect(def.type).toBe("function"); expect(def.function).toBeDefined(); expect(def.function.name).toBe("file_edit"); expect(def.function.description).toBeDefined(); expect(def.function.parameters).toBeDefined(); const params = def.function.parameters as any; expect(params.type).toBe("object"); expect(params.properties).toBeDefined(); expect(params.properties.path).toBeDefined(); expect(params.properties.search).toBeDefined(); expect(params.properties.replace).toBeDefined(); expect(params.required).toContain("path"); expect(params.required).toContain("search"); expect(params.required).toContain("replace"); }); }); describe("execute", () => { it("should return error for missing path", async () => { const result = await tool.execute( { session: mockSession }, { path: "", search: "test", replace: "test" }, ); expect(result).toContain("MISSING_PARAMETER"); expect(result).toContain("File path must not be empty"); }); it("should return error for missing search string", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "", replace: "test" }, ); expect(result).toContain("MISSING_PARAMETER"); expect(result).toContain("Search string must not be empty"); }); it("should return error for undefined replace string", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "test", replace: undefined as any }, ); expect(result).toContain("MISSING_PARAMETER"); expect(result).toContain("Replace string must not be undefined"); }); it("should return error for non-existent file", async () => { const result = await tool.execute( { session: mockSession }, { path: "non/existent/file.txt", search: "test", replace: "test" }, ); expect(result).toContain("NOT_FOUND"); expect(result).toContain("File not found"); }); it("should return error when search string not found", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "NotFound", replace: "test" }, ); expect(result).toContain("NOT_FOUND"); expect(result).toContain("Search string not found"); }); it("should show file content context when search not found", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "NotFound", replace: "test" }, ); // Should show file content context expect(result).toContain("File content"); expect(result).toContain("line"); }); it("should successfully edit a file and return plain text response", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "World", replace: "Universe" }, ); // Verify plain text format with header expect(result).toContain("PATH:"); expect(result).toContain("FILE OPERATION: edit"); expect(result).toContain("SEARCH FOUND: true"); expect(result).toContain("---"); // Verify the file was actually edited const fileContent = await fs.readFile( path.join(PROJECT_ROOT, testFilePath), "utf-8", ); expect(fileContent).toContain("Hello Universe"); expect(fileContent).not.toContain("Hello World"); }); it("should include diff context in the response", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "World", replace: "Universe" }, ); // Response should contain diff context expect(result).toContain("Changed line"); expect(result).toContain("Removed"); expect(result).toContain("Added"); }); it("should show context before and after the change", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "test", replace: "sample" }, ); // Response should contain context expect(result).toContain("Context"); }); it("should handle multi-line search and replace", async () => { const multiLineContent = "Line 1\nLine 2\nLine 3\nLine 4\n"; await fs.writeFile( path.join(PROJECT_ROOT, testFilePath), multiLineContent, "utf-8", ); const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "Line 2\nLine 3", replace: "Replacement", }, ); expect(result).toContain("Changed lines"); expect(result).toContain("Search spanned 2 lines"); // Verify the file was edited correctly const fileContent = await fs.readFile( path.join(PROJECT_ROOT, testFilePath), "utf-8", ); expect(fileContent).toBe("Line 1\nReplacement\nLine 4\n"); }); it("should show all affected lines for multi-line changes", async () => { const multiLineContent = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n"; await fs.writeFile( path.join(PROJECT_ROOT, testFilePath), multiLineContent, "utf-8", ); const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "Line 2\nLine 3\nLine 4", replace: "New Line 2\nNew Line 3", }, ); // Should show all changed lines expect(result).toContain("Changed lines 2-4"); expect(result).toContain("Search spanned 3 lines"); expect(result).toContain("Line 2"); expect(result).toContain("Line 3"); expect(result).toContain("Line 4"); }); it("should handle multi-line replacement with different line count", async () => { const content = "Start\nMiddle\nEnd\n"; await fs.writeFile( path.join(PROJECT_ROOT, testFilePath), content, "utf-8", ); const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "Middle", replace: "New Middle 1\nNew Middle 2\nNew Middle 3", }, ); expect(result).toContain("Changed line"); // Verify the file was edited correctly const fileContent = await fs.readFile( path.join(PROJECT_ROOT, testFilePath), "utf-8", ); expect(fileContent).toBe( "Start\nNew Middle 1\nNew Middle 2\nNew Middle 3\nEnd\n", ); }); }); describe("response format for AI clients", () => { it("should return a string that can be used in message content", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "World", replace: "Universe" }, ); expect(typeof result).toBe("string"); expect(result.length).toBeGreaterThan(0); }); it("should return plain text with header metadata", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "World", replace: "Universe" }, ); // Verify plain text format with header expect(result).toMatch(/^PATH:/m); expect(result).toContain("FILE OPERATION: edit"); expect(result).toContain("SEARCH FOUND: true"); expect(result).toContain("---"); }); it("should not return JSON format", async () => { const result = await tool.execute( { session: mockSession }, { path: testFilePath, search: "World", replace: "Universe" }, ); // Should not contain JSON structure expect(result).not.toMatch(/^\{.*"success".*\}$/s); expect(result).not.toMatch(/^\{.*"data".*\}$/s); }); }); });