gadget/docs/archive/tools/file/edit.test.ts

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);
});
});
});