Files
claude-code/packages/builtin-tools/src/tools/AgentTool/__tests__/agentToolUtils.test.ts
claude-code-best 2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00

254 lines
7.6 KiB
TypeScript

import { mock, describe, expect, test } from "bun:test";
// ─── Mocks for agentToolUtils.ts dependencies ───
// Only mock modules that are truly unavailable or cause side effects.
// Do NOT mock common/shared modules (zod/v4, bootstrap/state, etc.) to avoid
// corrupting the module cache for other test files in the same Bun process.
const noop = () => {};
mock.module("bun:bundle", () => ({ feature: () => false }));
mock.module("src/constants/tools.js", () => ({
ALL_AGENT_DISALLOWED_TOOLS: new Set(),
ASYNC_AGENT_ALLOWED_TOOLS: new Set(),
CUSTOM_AGENT_DISALLOWED_TOOLS: new Set(),
IN_PROCESS_TEAMMATE_ALLOWED_TOOLS: new Set(),
}));
mock.module("src/services/AgentSummary/agentSummary.js", () => ({
startAgentSummarization: noop,
}));
mock.module("src/services/analytics/index.js", () => ({
logEvent: noop,
logEventAsync: async () => {},
stripProtoFields: (v: any) => v,
attachAnalyticsSink: noop,
_resetForTesting: noop,
AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS: undefined,
}));
mock.module("src/services/api/dumpPrompts.js", () => ({
clearDumpState: noop,
}));
mock.module("src/Tool.js", () => ({
toolMatchesName: () => false,
findToolByName: noop,
}));
// messages.ts is complex - provide stubs for all named exports
mock.module("src/utils/messages.ts", () => ({
extractTextContent: (content: any[]) =>
content?.filter?.((b: any) => b.type === "text")?.map?.((b: any) => b.text)?.join("") ?? "",
getLastAssistantMessage: () => null,
SYNTHETIC_MESSAGES: new Set(),
INTERRUPT_MESSAGE: "",
INTERRUPT_MESSAGE_FOR_TOOL_USE: "",
CANCEL_MESSAGE: "",
REJECT_MESSAGE: "",
REJECT_MESSAGE_WITH_REASON_PREFIX: "",
SUBAGENT_REJECT_MESSAGE: "",
SUBAGENT_REJECT_MESSAGE_WITH_REASON_PREFIX: "",
PLAN_REJECTION_PREFIX: "",
DENIAL_WORKAROUND_GUIDANCE: "",
NO_RESPONSE_REQUESTED: "",
SYNTHETIC_TOOL_RESULT_PLACEHOLDER: "",
SYNTHETIC_MODEL: "",
AUTO_REJECT_MESSAGE: noop,
DONT_ASK_REJECT_MESSAGE: noop,
withMemoryCorrectionHint: (s: string) => s,
deriveShortMessageId: () => "",
isClassifierDenial: () => false,
buildYoloRejectionMessage: () => "",
buildClassifierUnavailableMessage: () => "",
isEmptyMessageText: () => true,
createAssistantMessage: noop,
createAssistantAPIErrorMessage: noop,
createUserMessage: noop,
prepareUserContent: noop,
createUserInterruptionMessage: noop,
createSyntheticUserCaveatMessage: noop,
formatCommandInputTags: noop,
}));
mock.module("src/tasks/LocalAgentTask/LocalAgentTask.js", () => ({
completeAgentTask: noop,
createActivityDescriptionResolver: () => ({}),
createProgressTracker: () => ({}),
enqueueAgentNotification: noop,
failAgentTask: noop,
getProgressUpdate: () => ({ tokenCount: 0, toolUseCount: 0 }),
getTokenCountFromTracker: () => 0,
isLocalAgentTask: () => false,
killAsyncAgent: noop,
updateAgentProgress: noop,
updateProgressFromMessage: noop,
}));
mock.module("src/utils/debug.js", () => ({
getMinDebugLogLevel: () => "warn",
isDebugMode: () => false,
enableDebugLogging: () => false,
getDebugFilter: () => null,
isDebugToStdErr: () => false,
getDebugFilePath: () => null,
setHasFormattedOutput: noop,
getHasFormattedOutput: () => false,
flushDebugLogs: async () => {},
logForDebugging: noop,
getDebugLogPath: () => "",
logAntError: noop,
}));
mock.module("src/utils/errors.js", () => ({
ClaudeError: class extends Error {},
MalformedCommandError: class extends Error {},
AbortError: class extends Error {},
ConfigParseError: class extends Error {},
ShellError: class extends Error {},
TeleportOperationError: class extends Error {},
TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS: class extends Error {},
isAbortError: () => false,
hasExactErrorMessage: () => false,
toError: (e: any) => e instanceof Error ? e : new Error(String(e)),
errorMessage: (e: any) => String(e),
getErrnoCode: () => undefined,
isENOENT: () => false,
getErrnoPath: () => undefined,
shortErrorStack: () => "",
isFsInaccessible: () => false,
classifyAxiosError: () => ({ category: "unknown" }),
}));
mock.module("src/utils/forkedAgent.js", () => ({}));
mock.module("src/utils/permissions/yoloClassifier.js", () => ({
buildTranscriptForClassifier: () => "",
classifyYoloAction: () => null,
}));
mock.module("src/utils/task/sdkProgress.js", () => ({
emitTaskProgress: noop,
}));
mock.module("src/utils/tokens.js", () => ({
getTokenCountFromUsage: () => 0,
}));
mock.module("src/tools/ExitPlanModeTool/constants.js", () => ({
EXIT_PLAN_MODE_V2_TOOL_NAME: "exit_plan_mode",
}));
mock.module("src/tools/AgentTool/constants.js", () => ({
AGENT_TOOL_NAME: "agent",
LEGACY_AGENT_TOOL_NAME: "task",
}));
mock.module("src/tools/AgentTool/loadAgentsDir.js", () => ({}));
mock.module("src/state/AppState.js", () => ({}));
mock.module("src/types/ids.js", () => ({
asAgentId: (id: string) => id,
}));
// Break circular dep
mock.module("src/tools/AgentTool/AgentTool.tsx", () => ({
AgentTool: {},
inputSchema: {},
outputSchema: {},
default: {},
}));
const {
countToolUses,
getLastToolUseName,
} = await import("../agentToolUtils");
function makeAssistantMessage(content: any[]): any {
return { type: "assistant", message: { content } };
}
function makeUserMessage(text: string): any {
return { type: "user", message: { content: text } };
}
describe("countToolUses", () => {
test("counts tool_use blocks in messages", () => {
const messages = [
makeAssistantMessage([
{ type: "tool_use", name: "Read" },
{ type: "text", text: "hello" },
]),
];
expect(countToolUses(messages)).toBe(1);
});
test("returns 0 for messages without tool_use", () => {
const messages = [
makeAssistantMessage([{ type: "text", text: "hello" }]),
];
expect(countToolUses(messages)).toBe(0);
});
test("returns 0 for empty array", () => {
expect(countToolUses([])).toBe(0);
});
test("counts multiple tool_use blocks across messages", () => {
const messages = [
makeAssistantMessage([{ type: "tool_use", name: "Read" }]),
makeUserMessage("ok"),
makeAssistantMessage([{ type: "tool_use", name: "Write" }]),
];
expect(countToolUses(messages)).toBe(2);
});
test("counts tool_use in single message with multiple blocks", () => {
const messages = [
makeAssistantMessage([
{ type: "tool_use", name: "Read" },
{ type: "tool_use", name: "Grep" },
{ type: "tool_use", name: "Write" },
]),
];
expect(countToolUses(messages)).toBe(3);
});
});
describe("getLastToolUseName", () => {
test("returns last tool name from assistant message", () => {
const msg = makeAssistantMessage([
{ type: "tool_use", name: "Read" },
{ type: "tool_use", name: "Write" },
]);
expect(getLastToolUseName(msg)).toBe("Write");
});
test("returns undefined for message without tool_use", () => {
const msg = makeAssistantMessage([{ type: "text", text: "hello" }]);
expect(getLastToolUseName(msg)).toBeUndefined();
});
test("returns the last tool when multiple tool_uses present", () => {
const msg = makeAssistantMessage([
{ type: "tool_use", name: "Read" },
{ type: "tool_use", name: "Grep" },
{ type: "tool_use", name: "Edit" },
]);
expect(getLastToolUseName(msg)).toBe("Edit");
});
test("returns undefined for non-assistant message", () => {
const msg = makeUserMessage("hello");
expect(getLastToolUseName(msg)).toBeUndefined();
});
test("handles message with null content", () => {
const msg = { type: "assistant", message: { content: null } } as any;
expect(getLastToolUseName(msg)).toBeUndefined();
});
});