diff --git a/src/services/tools/file_edit_replace.test.ts b/src/services/tools/file_edit_replace.test.ts index dba87e8eb2..1b1851fff0 100644 --- a/src/services/tools/file_edit_replace.test.ts +++ b/src/services/tools/file_edit_replace.test.ts @@ -13,6 +13,27 @@ const mockToolCallOptions: ToolCallOptions = { messages: [], }; +// Test helpers +const setupFile = async (filePath: string, content: string): Promise => { + await fs.writeFile(filePath, content); + const stats = await fs.stat(filePath); + return leaseFromStat(stats); +}; + +const readFile = async (filePath: string): Promise => { + return await fs.readFile(filePath, "utf-8"); +}; + +const executeReplace = async ( + tool: ReturnType, + filePath: string, + edits: FileEditReplaceToolArgs["edits"], + lease: string +): Promise => { + const args: FileEditReplaceToolArgs = { file_path: filePath, edits, lease }; + return (await tool.execute!(args, mockToolCallOptions)) as FileEditReplaceToolResult; +}; + describe("file_edit_replace tool", () => { let testDir: string; let testFilePath: string; @@ -29,81 +50,48 @@ describe("file_edit_replace tool", () => { }); it("should apply a single edit successfully", async () => { - // Setup - const initialContent = "Hello world\nThis is a test\nGoodbye world"; - await fs.writeFile(testFilePath, initialContent); - - // Get lease by reading file stats - const stats = await fs.stat(testFilePath); - const lease = leaseFromStat(stats); - + const lease = await setupFile(testFilePath, "Hello world\nThis is a test\nGoodbye world"); const tool = createFileEditReplaceTool({ cwd: testDir }); - const args: FileEditReplaceToolArgs = { - file_path: testFilePath, - edits: [ - { - old_string: "Hello world", - new_string: "Hello universe", - }, - ], - lease, - }; - // Execute - const result = (await tool.execute!(args, mockToolCallOptions)) as FileEditReplaceToolResult; + const result = await executeReplace( + tool, + testFilePath, + [{ old_string: "Hello world", new_string: "Hello universe" }], + lease + ); - // Assert expect(result.success).toBe(true); if (result.success) { expect(result.edits_applied).toBe(1); expect(result.lease).toMatch(/^[0-9a-f]{6}$/); - expect(result.lease).not.toBe(lease); // New lease should be different + expect(result.lease).not.toBe(lease); } - const updatedContent = await fs.readFile(testFilePath, "utf-8"); - expect(updatedContent).toBe("Hello universe\nThis is a test\nGoodbye world"); + expect(await readFile(testFilePath)).toBe("Hello universe\nThis is a test\nGoodbye world"); }); it("should apply multiple edits sequentially", async () => { - // Setup - const initialContent = "foo bar baz"; - await fs.writeFile(testFilePath, initialContent); - - const stats = await fs.stat(testFilePath); - const lease = leaseFromStat(stats); - + const lease = await setupFile(testFilePath, "foo bar baz"); const tool = createFileEditReplaceTool({ cwd: testDir }); - const args: FileEditReplaceToolArgs = { - file_path: testFilePath, - edits: [ - { - old_string: "foo", - new_string: "FOO", - }, - { - old_string: "bar", - new_string: "BAR", - }, - { - old_string: "baz", - new_string: "BAZ", - }, - ], - lease, - }; - // Execute - const result = (await tool.execute!(args, mockToolCallOptions)) as FileEditReplaceToolResult; + const result = await executeReplace( + tool, + testFilePath, + [ + { old_string: "foo", new_string: "FOO" }, + { old_string: "bar", new_string: "BAR" }, + { old_string: "baz", new_string: "BAZ" }, + ], + lease + ); - // Assert expect(result.success).toBe(true); if (result.success) { expect(result.edits_applied).toBe(3); expect(result.lease).toMatch(/^[0-9a-f]{6}$/); } - const updatedContent = await fs.readFile(testFilePath, "utf-8"); - expect(updatedContent).toBe("FOO BAR BAZ"); + expect(await readFile(testFilePath)).toBe("FOO BAR BAZ"); }); it("should rollback if later edit fails (first edit breaks second edit search)", async () => { diff --git a/src/utils/slashCommands/parser.test.ts b/src/utils/slashCommands/parser.test.ts index 9c9cd39afe..72679a8438 100644 --- a/src/utils/slashCommands/parser.test.ts +++ b/src/utils/slashCommands/parser.test.ts @@ -1,6 +1,19 @@ import { describe, it, expect } from "bun:test"; import { parseCommand, setNestedProperty } from "./parser"; +// Test helpers +const expectParse = (input: string, expected: ReturnType) => { + expect(parseCommand(input)).toEqual(expected); +}; + +const expectProvidersSet = (input: string, provider: string, keyPath: string[], value: string) => { + expectParse(input, { type: "providers-set", provider, keyPath, value }); +}; + +const expectModelSet = (input: string, modelString: string) => { + expectParse(input, { type: "model-set", modelString }); +}; + describe("commandParser", () => { describe("parseCommand", () => { it("should return null for non-command input", () => { @@ -10,95 +23,80 @@ describe("commandParser", () => { }); it("should parse /clear command", () => { - const result = parseCommand("/clear"); - expect(result).toEqual({ - type: "clear", - }); + expectParse("/clear", { type: "clear" }); }); it("should parse /providers help when no subcommand", () => { - const result = parseCommand("/providers"); - expect(result).toEqual({ - type: "providers-help", - }); + expectParse("/providers", { type: "providers-help" }); }); it("should parse /providers with invalid subcommand", () => { - const result = parseCommand("/providers invalid"); - expect(result).toEqual({ + expectParse("/providers invalid", { type: "providers-invalid-subcommand", subcommand: "invalid", }); }); it("should parse /providers set with missing args", () => { - expect(parseCommand("/providers set")).toEqual({ - type: "providers-missing-args", - subcommand: "set", - argCount: 0, - }); + const missingArgsCases = [ + { input: "/providers set", argCount: 0 }, + { input: "/providers set anthropic", argCount: 1 }, + { input: "/providers set anthropic apiKey", argCount: 2 }, + ]; - expect(parseCommand("/providers set anthropic")).toEqual({ - type: "providers-missing-args", - subcommand: "set", - argCount: 1, - }); - - expect(parseCommand("/providers set anthropic apiKey")).toEqual({ - type: "providers-missing-args", - subcommand: "set", - argCount: 2, + missingArgsCases.forEach(({ input, argCount }) => { + expectParse(input, { + type: "providers-missing-args", + subcommand: "set", + argCount, + }); }); }); it("should parse /providers set with all arguments", () => { - const result = parseCommand("/providers set anthropic apiKey sk-123"); - expect(result).toEqual({ - type: "providers-set", - provider: "anthropic", - keyPath: ["apiKey"], - value: "sk-123", - }); + expectProvidersSet( + "/providers set anthropic apiKey sk-123", + "anthropic", + ["apiKey"], + "sk-123" + ); }); it("should handle quoted arguments", () => { - const result = parseCommand('/providers set anthropic apiKey "my key with spaces"'); - expect(result).toEqual({ - type: "providers-set", - provider: "anthropic", - keyPath: ["apiKey"], - value: "my key with spaces", - }); + expectProvidersSet( + '/providers set anthropic apiKey "my key with spaces"', + "anthropic", + ["apiKey"], + "my key with spaces" + ); }); it("should handle multiple spaces in value", () => { - const result = parseCommand("/providers set anthropic apiKey My Anthropic API"); - expect(result).toEqual({ - type: "providers-set", - provider: "anthropic", - keyPath: ["apiKey"], - value: "My Anthropic API", - }); + expectProvidersSet( + "/providers set anthropic apiKey My Anthropic API", + "anthropic", + ["apiKey"], + "My Anthropic API" + ); }); it("should handle nested key paths", () => { - const result = parseCommand("/providers set anthropic baseUrl.scheme https"); - expect(result).toEqual({ - type: "providers-set", - provider: "anthropic", - keyPath: ["baseUrl", "scheme"], - value: "https", - }); + expectProvidersSet( + "/providers set anthropic baseUrl.scheme https", + "anthropic", + ["baseUrl", "scheme"], + "https" + ); }); it("should parse unknown commands", () => { - expect(parseCommand("/foo")).toEqual({ + expectParse("/foo", { type: "unknown-command", command: "foo", subcommand: undefined, }); - expect(parseCommand("/foo bar")).toEqual({ + expectParse("/foo bar", { type: "unknown-command", command: "foo", subcommand: "bar", @@ -106,61 +104,41 @@ describe("commandParser", () => { }); it("should handle multiple spaces between arguments", () => { - const result = parseCommand("/providers set anthropic apiKey sk-12345"); - expect(result).toEqual({ - type: "providers-set", - provider: "anthropic", - keyPath: ["apiKey"], - value: "sk-12345", - }); + expectProvidersSet( + "/providers set anthropic apiKey sk-12345", + "anthropic", + ["apiKey"], + "sk-12345" + ); }); it("should handle quoted URL values", () => { - const result = parseCommand( - '/providers set anthropic baseUrl "https://api.anthropic.com/v1"' + expectProvidersSet( + '/providers set anthropic baseUrl "https://api.anthropic.com/v1"', + "anthropic", + ["baseUrl"], + "https://api.anthropic.com/v1" ); - expect(result).toEqual({ - type: "providers-set", - provider: "anthropic", - keyPath: ["baseUrl"], - value: "https://api.anthropic.com/v1", - }); }); it("should parse /model with abbreviation", () => { - const result = parseCommand("/model opus"); - expect(result).toEqual({ - type: "model-set", - modelString: "anthropic:claude-opus-4-1", - }); + expectModelSet("/model opus", "anthropic:claude-opus-4-1"); }); it("should parse /model with full provider:model format", () => { - const result = parseCommand("/model anthropic:claude-sonnet-4-5"); - expect(result).toEqual({ - type: "model-set", - modelString: "anthropic:claude-sonnet-4-5", - }); + expectModelSet("/model anthropic:claude-sonnet-4-5", "anthropic:claude-sonnet-4-5"); }); it("should parse /model help when no args", () => { - const result = parseCommand("/model"); - expect(result).toEqual({ - type: "model-help", - }); + expectParse("/model", { type: "model-help" }); }); it("should handle unknown abbreviation as full model string", () => { - const result = parseCommand("/model custom:model-name"); - expect(result).toEqual({ - type: "model-set", - modelString: "custom:model-name", - }); + expectModelSet("/model custom:model-name", "custom:model-name"); }); it("should reject /model with too many arguments", () => { - const result = parseCommand("/model anthropic claude extra"); - expect(result).toEqual({ + expectParse("/model anthropic claude extra", { type: "unknown-command", command: "model", subcommand: "claude",