From 67baea3c7f45938ff592021c56791ab70f72df99 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Wed, 1 Apr 2026 21:32:45 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=20Tool=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=20(=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E8=AE=A1=E5=88=92=2001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖 buildTool、toolMatchesName、findToolByName、getEmptyToolPermissionContext、 filterToolProgressMessages、parseToolPreset、parseGitCommitId、detectGitOperation 共 46 个测试用例全部通过。 Co-Authored-By: Claude Opus 4.6 --- src/__tests__/Tool.test.ts | 201 ++++++++++++++++++ src/__tests__/tools.test.ts | 24 +++ .../__tests__/gitOperationTracking.test.ts | 134 ++++++++++++ 3 files changed, 359 insertions(+) create mode 100644 src/__tests__/Tool.test.ts create mode 100644 src/__tests__/tools.test.ts create mode 100644 src/tools/shared/__tests__/gitOperationTracking.test.ts diff --git a/src/__tests__/Tool.test.ts b/src/__tests__/Tool.test.ts new file mode 100644 index 0000000..07398b2 --- /dev/null +++ b/src/__tests__/Tool.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, test } from "bun:test"; +import { + buildTool, + toolMatchesName, + findToolByName, + getEmptyToolPermissionContext, + filterToolProgressMessages, +} from "../Tool"; + +// Minimal tool definition for testing buildTool +function makeMinimalToolDef(overrides: Record = {}) { + return { + name: "TestTool", + inputSchema: { type: "object" as const } as any, + maxResultSizeChars: 10000, + call: async () => ({ data: "ok" }), + description: async () => "A test tool", + prompt: async () => "test prompt", + mapToolResultToToolResultBlockParam: (content: unknown, toolUseID: string) => ({ + type: "tool_result" as const, + tool_use_id: toolUseID, + content: String(content), + }), + renderToolUseMessage: () => null, + ...overrides, + }; +} + +describe("buildTool", () => { + test("fills in default isEnabled as true", () => { + const tool = buildTool(makeMinimalToolDef()); + expect(tool.isEnabled()).toBe(true); + }); + + test("fills in default isConcurrencySafe as false", () => { + const tool = buildTool(makeMinimalToolDef()); + expect(tool.isConcurrencySafe({})).toBe(false); + }); + + test("fills in default isReadOnly as false", () => { + const tool = buildTool(makeMinimalToolDef()); + expect(tool.isReadOnly({})).toBe(false); + }); + + test("fills in default isDestructive as false", () => { + const tool = buildTool(makeMinimalToolDef()); + expect(tool.isDestructive!({})).toBe(false); + }); + + test("fills in default checkPermissions as allow", async () => { + const tool = buildTool(makeMinimalToolDef()); + const input = { foo: "bar" }; + const result = await tool.checkPermissions(input, {} as any); + expect(result).toEqual({ behavior: "allow", updatedInput: input }); + }); + + test("fills in default userFacingName from tool name", () => { + const tool = buildTool(makeMinimalToolDef()); + expect(tool.userFacingName(undefined)).toBe("TestTool"); + }); + + test("fills in default toAutoClassifierInput as empty string", () => { + const tool = buildTool(makeMinimalToolDef()); + expect(tool.toAutoClassifierInput({})).toBe(""); + }); + + test("preserves explicitly provided methods", () => { + const tool = buildTool( + makeMinimalToolDef({ + isEnabled: () => false, + isConcurrencySafe: () => true, + isReadOnly: () => true, + }) + ); + expect(tool.isEnabled()).toBe(false); + expect(tool.isConcurrencySafe({})).toBe(true); + expect(tool.isReadOnly({})).toBe(true); + }); + + test("preserves all non-defaultable properties", () => { + const tool = buildTool(makeMinimalToolDef()); + expect(tool.name).toBe("TestTool"); + expect(tool.maxResultSizeChars).toBe(10000); + expect(typeof tool.call).toBe("function"); + expect(typeof tool.description).toBe("function"); + expect(typeof tool.prompt).toBe("function"); + }); +}); + +describe("toolMatchesName", () => { + test("returns true for exact name match", () => { + expect(toolMatchesName({ name: "Bash" }, "Bash")).toBe(true); + }); + + test("returns false for non-matching name", () => { + expect(toolMatchesName({ name: "Bash" }, "Read")).toBe(false); + }); + + test("returns true when name matches an alias", () => { + expect( + toolMatchesName({ name: "Bash", aliases: ["BashTool", "Shell"] }, "BashTool") + ).toBe(true); + }); + + test("returns false when aliases is undefined", () => { + expect(toolMatchesName({ name: "Bash" }, "BashTool")).toBe(false); + }); + + test("returns false when aliases is empty", () => { + expect( + toolMatchesName({ name: "Bash", aliases: [] }, "BashTool") + ).toBe(false); + }); +}); + +describe("findToolByName", () => { + const mockTools = [ + buildTool(makeMinimalToolDef({ name: "Bash" })), + buildTool(makeMinimalToolDef({ name: "Read", aliases: ["FileRead"] })), + buildTool(makeMinimalToolDef({ name: "Edit" })), + ]; + + test("finds tool by primary name", () => { + const tool = findToolByName(mockTools, "Bash"); + expect(tool).toBeDefined(); + expect(tool!.name).toBe("Bash"); + }); + + test("finds tool by alias", () => { + const tool = findToolByName(mockTools, "FileRead"); + expect(tool).toBeDefined(); + expect(tool!.name).toBe("Read"); + }); + + test("returns undefined when no match", () => { + expect(findToolByName(mockTools, "NonExistent")).toBeUndefined(); + }); + + test("returns first match when duplicates exist", () => { + const dupeTools = [ + buildTool(makeMinimalToolDef({ name: "Bash", maxResultSizeChars: 100 })), + buildTool(makeMinimalToolDef({ name: "Bash", maxResultSizeChars: 200 })), + ]; + const tool = findToolByName(dupeTools, "Bash"); + expect(tool!.maxResultSizeChars).toBe(100); + }); +}); + +describe("getEmptyToolPermissionContext", () => { + test("returns default permission mode", () => { + const ctx = getEmptyToolPermissionContext(); + expect(ctx.mode).toBe("default"); + }); + + test("returns empty maps and arrays", () => { + const ctx = getEmptyToolPermissionContext(); + expect(ctx.additionalWorkingDirectories.size).toBe(0); + expect(ctx.alwaysAllowRules).toEqual({}); + expect(ctx.alwaysDenyRules).toEqual({}); + expect(ctx.alwaysAskRules).toEqual({}); + }); + + test("returns isBypassPermissionsModeAvailable as false", () => { + const ctx = getEmptyToolPermissionContext(); + expect(ctx.isBypassPermissionsModeAvailable).toBe(false); + }); +}); + +describe("filterToolProgressMessages", () => { + test("filters out hook_progress messages", () => { + const messages = [ + { data: { type: "hook_progress", hookName: "pre" } }, + { data: { type: "tool_progress", toolName: "Bash" } }, + ] as any[]; + const result = filterToolProgressMessages(messages); + expect(result).toHaveLength(1); + expect((result[0]!.data as any).type).toBe("tool_progress"); + }); + + test("keeps tool progress messages", () => { + const messages = [ + { data: { type: "tool_progress", toolName: "Bash" } }, + { data: { type: "tool_progress", toolName: "Read" } }, + ] as any[]; + const result = filterToolProgressMessages(messages); + expect(result).toHaveLength(2); + }); + + test("returns empty array for empty input", () => { + expect(filterToolProgressMessages([])).toEqual([]); + }); + + test("handles messages without type field", () => { + const messages = [ + { data: { toolName: "Bash" } }, + { data: { type: "hook_progress" } }, + ] as any[]; + const result = filterToolProgressMessages(messages); + expect(result).toHaveLength(1); + }); +}); diff --git a/src/__tests__/tools.test.ts b/src/__tests__/tools.test.ts new file mode 100644 index 0000000..a15a160 --- /dev/null +++ b/src/__tests__/tools.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test"; +import { parseToolPreset } from "../tools"; + +describe("parseToolPreset", () => { + test('returns "default" for "default" input', () => { + expect(parseToolPreset("default")).toBe("default"); + }); + + test('returns "default" for "Default" input (case-insensitive)', () => { + expect(parseToolPreset("Default")).toBe("default"); + }); + + test("returns null for unknown preset", () => { + expect(parseToolPreset("unknown")).toBeNull(); + }); + + test("returns null for empty string", () => { + expect(parseToolPreset("")).toBeNull(); + }); + + test("returns null for random string", () => { + expect(parseToolPreset("custom-preset")).toBeNull(); + }); +}); diff --git a/src/tools/shared/__tests__/gitOperationTracking.test.ts b/src/tools/shared/__tests__/gitOperationTracking.test.ts new file mode 100644 index 0000000..8ea2950 --- /dev/null +++ b/src/tools/shared/__tests__/gitOperationTracking.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, test } from "bun:test"; +import { parseGitCommitId, detectGitOperation } from "../gitOperationTracking"; + +describe("parseGitCommitId", () => { + test("extracts commit hash from git commit output", () => { + expect(parseGitCommitId("[main abc1234] fix: some message")).toBe("abc1234"); + }); + + test("extracts hash from root commit output", () => { + expect( + parseGitCommitId("[main (root-commit) abc1234] initial commit") + ).toBe("abc1234"); + }); + + test("returns undefined for non-commit output", () => { + expect(parseGitCommitId("nothing to commit")).toBeUndefined(); + }); + + test("handles various branch name formats", () => { + expect(parseGitCommitId("[feature/foo abc1234] message")).toBe("abc1234"); + expect(parseGitCommitId("[fix/bar-baz abc1234] message")).toBe("abc1234"); + expect(parseGitCommitId("[v1.0.0 abc1234] message")).toBe("abc1234"); + }); + + test("returns undefined for empty string", () => { + expect(parseGitCommitId("")).toBeUndefined(); + }); +}); + +describe("detectGitOperation", () => { + test("detects git commit operation", () => { + const result = detectGitOperation( + "git commit -m 'fix bug'", + "[main abc1234] fix bug" + ); + expect(result.commit).toBeDefined(); + expect(result.commit!.sha).toBe("abc123"); + expect(result.commit!.kind).toBe("committed"); + }); + + test("detects git commit --amend operation", () => { + const result = detectGitOperation( + "git commit --amend -m 'updated'", + "[main def5678] updated" + ); + expect(result.commit).toBeDefined(); + expect(result.commit!.kind).toBe("amended"); + }); + + test("detects git cherry-pick operation", () => { + const result = detectGitOperation( + "git cherry-pick abc1234", + "[main def5678] cherry picked commit" + ); + expect(result.commit).toBeDefined(); + expect(result.commit!.kind).toBe("cherry-picked"); + }); + + test("detects git push operation", () => { + const result = detectGitOperation( + "git push origin main", + " abc1234..def5678 main -> main" + ); + expect(result.push).toBeDefined(); + expect(result.push!.branch).toBe("main"); + }); + + test("detects git merge operation", () => { + const result = detectGitOperation( + "git merge feature-branch", + "Merge made by the 'ort' strategy." + ); + expect(result.branch).toBeDefined(); + expect(result.branch!.action).toBe("merged"); + expect(result.branch!.ref).toBe("feature-branch"); + }); + + test("detects git rebase operation", () => { + const result = detectGitOperation( + "git rebase main", + "Successfully rebased and updated refs/heads/feature." + ); + expect(result.branch).toBeDefined(); + expect(result.branch!.action).toBe("rebased"); + expect(result.branch!.ref).toBe("main"); + }); + + test("returns null for non-git commands", () => { + const result = detectGitOperation("ls -la", "total 100\ndrwxr-xr-x"); + expect(result.commit).toBeUndefined(); + expect(result.push).toBeUndefined(); + expect(result.branch).toBeUndefined(); + expect(result.pr).toBeUndefined(); + }); + + test("detects gh pr create operation", () => { + const result = detectGitOperation( + "gh pr create --title 'fix' --body 'desc'", + "https://github.com/owner/repo/pull/42" + ); + expect(result.pr).toBeDefined(); + expect(result.pr!.number).toBe(42); + expect(result.pr!.action).toBe("created"); + }); + + test("detects gh pr merge operation", () => { + const result = detectGitOperation( + "gh pr merge 42", + "✓ Merged pull request owner/repo#42" + ); + expect(result.pr).toBeDefined(); + expect(result.pr!.number).toBe(42); + expect(result.pr!.action).toBe("merged"); + }); + + test("handles git commit with -c options", () => { + const result = detectGitOperation( + "git -c commit.gpgsign=false commit -m 'msg'", + "[main aaa1111] msg" + ); + expect(result.commit).toBeDefined(); + expect(result.commit!.sha).toBe("aaa111"); + }); + + test("detects fast-forward merge", () => { + const result = detectGitOperation( + "git merge develop", + "Fast-forward\n file.txt | 1 +\n 1 file changed" + ); + expect(result.branch).toBeDefined(); + expect(result.branch!.action).toBe("merged"); + expect(result.branch!.ref).toBe("develop"); + }); +});