288 lines
9.0 KiB
TypeScript
288 lines
9.0 KiB
TypeScript
// 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);
|
|
});
|
|
});
|
|
});
|