|
| 1 | +# Tool 系统测试计划 |
| 2 | + |
| 3 | +## 概述 |
| 4 | + |
| 5 | +Tool 系统是 Claude Code 的核心,负责工具的定义、注册、发现和过滤。本计划覆盖 `src/Tool.ts` 中的工具接口与工具函数、`src/tools.ts` 中的注册/过滤逻辑,以及各工具目录下可独立测试的纯函数。 |
| 6 | + |
| 7 | +## 被测文件 |
| 8 | + |
| 9 | +| 文件 | 关键导出 | |
| 10 | +|------|----------| |
| 11 | +| `src/Tool.ts` | `buildTool`, `toolMatchesName`, `findToolByName`, `getEmptyToolPermissionContext`, `filterToolProgressMessages` | |
| 12 | +| `src/tools.ts` | `parseToolPreset`, `filterToolsByDenyRules`, `getAllBaseTools`, `getTools`, `assembleToolPool` | |
| 13 | +| `src/tools/shared/gitOperationTracking.ts` | `parseGitCommitId`, `detectGitOperation` | |
| 14 | +| `src/tools/shared/spawnMultiAgent.ts` | `resolveTeammateModel`, `generateUniqueTeammateName` | |
| 15 | +| `src/tools/GrepTool/GrepTool.ts` | `applyHeadLimit`, `formatLimitInfo`(内部辅助函数) | |
| 16 | +| `src/tools/FileEditTool/utils.ts` | 字符串匹配/补丁相关纯函数 | |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## 测试用例 |
| 21 | + |
| 22 | +### src/Tool.ts |
| 23 | + |
| 24 | +#### describe('buildTool') |
| 25 | + |
| 26 | +- test('fills in default isEnabled as true') — 不传 isEnabled 时,构建的 tool.isEnabled() 应返回 true |
| 27 | +- test('fills in default isConcurrencySafe as false') — 默认值应为 false(fail-closed) |
| 28 | +- test('fills in default isReadOnly as false') — 默认假设有写操作 |
| 29 | +- test('fills in default isDestructive as false') — 默认非破坏性 |
| 30 | +- test('fills in default checkPermissions as allow') — 默认 checkPermissions 应返回 `{ behavior: 'allow', updatedInput }` |
| 31 | +- test('fills in default userFacingName from tool name') — userFacingName 默认应返回 tool.name |
| 32 | +- test('preserves explicitly provided methods') — 传入自定义 isEnabled 等方法时应覆盖默认值 |
| 33 | +- test('preserves all non-defaultable properties') — name, inputSchema, call, description 等属性原样保留 |
| 34 | + |
| 35 | +#### describe('toolMatchesName') |
| 36 | + |
| 37 | +- test('returns true for exact name match') — `{ name: 'Bash' }` 匹配 'Bash' |
| 38 | +- test('returns false for non-matching name') — `{ name: 'Bash' }` 不匹配 'Read' |
| 39 | +- test('returns true when name matches an alias') — `{ name: 'Bash', aliases: ['BashTool'] }` 匹配 'BashTool' |
| 40 | +- test('returns false when aliases is undefined') — `{ name: 'Bash' }` 不匹配 'BashTool' |
| 41 | +- test('returns false when aliases is empty') — `{ name: 'Bash', aliases: [] }` 不匹配 'BashTool' |
| 42 | + |
| 43 | +#### describe('findToolByName') |
| 44 | + |
| 45 | +- test('finds tool by primary name') — 从 tools 列表中按 name 找到工具 |
| 46 | +- test('finds tool by alias') — 从 tools 列表中按 alias 找到工具 |
| 47 | +- test('returns undefined when no match') — 找不到时返回 undefined |
| 48 | +- test('returns first match when duplicates exist') — 多个同名工具时返回第一个 |
| 49 | + |
| 50 | +#### describe('getEmptyToolPermissionContext') |
| 51 | + |
| 52 | +- test('returns default permission mode') — mode 应为 'default' |
| 53 | +- test('returns empty maps and arrays') — additionalWorkingDirectories 为空 Map,rules 为空对象 |
| 54 | +- test('returns isBypassPermissionsModeAvailable as false') |
| 55 | + |
| 56 | +#### describe('filterToolProgressMessages') |
| 57 | + |
| 58 | +- test('filters out hook_progress messages') — 移除 type 为 hook_progress 的消息 |
| 59 | +- test('keeps tool progress messages') — 保留非 hook_progress 的消息 |
| 60 | +- test('returns empty array for empty input') |
| 61 | +- test('handles messages without type field') — data 不含 type 时应保留 |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +### src/tools.ts |
| 66 | + |
| 67 | +#### describe('parseToolPreset') |
| 68 | + |
| 69 | +- test('returns "default" for "default" input') — 精确匹配 |
| 70 | +- test('returns "default" for "Default" input') — 大小写不敏感 |
| 71 | +- test('returns null for unknown preset') — 未知字符串返回 null |
| 72 | +- test('returns null for empty string') |
| 73 | + |
| 74 | +#### describe('filterToolsByDenyRules') |
| 75 | + |
| 76 | +- test('returns all tools when no deny rules') — 空 deny 规则不过滤任何工具 |
| 77 | +- test('filters out tools matching blanket deny rule') — deny rule `{ toolName: 'Bash' }` 应移除 Bash |
| 78 | +- test('does not filter tools with content-specific deny rules') — deny rule `{ toolName: 'Bash', ruleContent: 'rm -rf' }` 不移除 Bash(只在运行时阻止特定命令) |
| 79 | +- test('filters MCP tools by server name prefix') — deny rule `mcp__server` 应移除该 server 下所有工具 |
| 80 | +- test('preserves tools not matching any deny rule') |
| 81 | + |
| 82 | +#### describe('getAllBaseTools') |
| 83 | + |
| 84 | +- test('returns a non-empty array of tools') — 至少包含核心工具 |
| 85 | +- test('each tool has required properties') — 每个工具应有 name, inputSchema, call 等属性 |
| 86 | +- test('includes BashTool, FileReadTool, FileEditTool') — 核心工具始终存在 |
| 87 | +- test('includes TestingPermissionTool when NODE_ENV is test') — 需设置 env |
| 88 | + |
| 89 | +#### describe('getTools') |
| 90 | + |
| 91 | +- test('returns filtered tools based on permission context') — 根据 deny rules 过滤 |
| 92 | +- test('returns simple tools in CLAUDE_CODE_SIMPLE mode') — 仅返回 Bash/Read/Edit |
| 93 | +- test('filters disabled tools via isEnabled') — isEnabled 返回 false 的工具被排除 |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +### src/tools/shared/gitOperationTracking.ts |
| 98 | + |
| 99 | +#### describe('parseGitCommitId') |
| 100 | + |
| 101 | +- test('extracts commit hash from git commit output') — 从 `[main abc1234] message` 中提取 `abc1234` |
| 102 | +- test('returns null for non-commit output') — 无法解析时返回 null |
| 103 | +- test('handles various branch name formats') — `[feature/foo abc1234]` 等 |
| 104 | + |
| 105 | +#### describe('detectGitOperation') |
| 106 | + |
| 107 | +- test('detects git commit operation') — 命令含 `git commit` 时识别为 commit |
| 108 | +- test('detects git push operation') — 命令含 `git push` 时识别 |
| 109 | +- test('returns null for non-git commands') — 非 git 命令返回 null |
| 110 | +- test('detects git merge operation') |
| 111 | +- test('detects git rebase operation') |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +### src/tools/shared/spawnMultiAgent.ts |
| 116 | + |
| 117 | +#### describe('resolveTeammateModel') |
| 118 | + |
| 119 | +- test('returns specified model when provided') |
| 120 | +- test('falls back to default model when not specified') |
| 121 | + |
| 122 | +#### describe('generateUniqueTeammateName') |
| 123 | + |
| 124 | +- test('generates a name when no existing names') — 无冲突时返回基础名 |
| 125 | +- test('appends suffix when name conflicts') — 与已有名称冲突时添加后缀 |
| 126 | +- test('handles multiple conflicts') — 多次冲突时递增后缀 |
| 127 | + |
| 128 | +--- |
| 129 | + |
| 130 | +## Mock 需求 |
| 131 | + |
| 132 | +| 依赖 | Mock 方式 | 说明 | |
| 133 | +|------|-----------|------| |
| 134 | +| `bun:bundle` (feature) | 已 polyfill 为 `() => false` | 不需额外 mock | |
| 135 | +| `process.env` | `bun:test` mock | 测试 `USER_TYPE`、`NODE_ENV`、`CLAUDE_CODE_SIMPLE` | |
| 136 | +| `getDenyRuleForTool` | mock module | `filterToolsByDenyRules` 测试中需控制返回值 | |
| 137 | +| `isToolSearchEnabledOptimistic` | mock module | `getAllBaseTools` 中条件加载 | |
| 138 | + |
| 139 | +## 集成测试场景 |
| 140 | + |
| 141 | +放在 `tests/integration/tool-chain.test.ts`: |
| 142 | + |
| 143 | +### describe('Tool registration and discovery') |
| 144 | + |
| 145 | +- test('getAllBaseTools returns tools that can be found by findToolByName') — 注册 → 查找完整链路 |
| 146 | +- test('filterToolsByDenyRules + getTools produces consistent results') — 过滤管线一致性 |
| 147 | +- test('assembleToolPool deduplicates built-in and MCP tools') — 合并去重逻辑 |
0 commit comments