From 6f5623b26ce08fb319e11f1b53ec7027753bbee6 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Thu, 2 Apr 2026 17:37:06 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E5=AE=8C=E6=88=90=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/test-plans/phase19-batch1-micro-utils.md | 435 ++++++++++++++++++ .../phase19-batch2-utils-state-commands.md | 287 ++++++++++++ .../phase19-batch3-tool-submodules.md | 258 +++++++++++ docs/test-plans/phase19-batch4-services.md | 215 +++++++++ docs/test-plans/phase19-batch5-mcp-config.md | 200 ++++++++ 5 files changed, 1395 insertions(+) create mode 100644 docs/test-plans/phase19-batch1-micro-utils.md create mode 100644 docs/test-plans/phase19-batch2-utils-state-commands.md create mode 100644 docs/test-plans/phase19-batch3-tool-submodules.md create mode 100644 docs/test-plans/phase19-batch4-services.md create mode 100644 docs/test-plans/phase19-batch5-mcp-config.md diff --git a/docs/test-plans/phase19-batch1-micro-utils.md b/docs/test-plans/phase19-batch1-micro-utils.md new file mode 100644 index 0000000..4b8c4c6 --- /dev/null +++ b/docs/test-plans/phase19-batch1-micro-utils.md @@ -0,0 +1,435 @@ +# Phase 19 - Batch 1: 零依赖微型 utils + +> 预计 ~154 tests / 13 文件 | 全部纯函数,无需 mock + +--- + +## 1. `src/utils/__tests__/semanticBoolean.test.ts` (~8 tests) + +**源文件**: `src/utils/semanticBoolean.ts` (30 行) +**依赖**: `zod/v4` + +### 测试用例 + +```typescript +describe("semanticBoolean", () => { + // 基本 Zod 行为 + test("parses boolean true to true") + test("parses boolean false to false") + test("parses string 'true' to true") + test("parses string 'false' to false") + // 边界 + test("rejects string 'TRUE' (case-sensitive)") + test("rejects string 'FALSE' (case-sensitive)") + test("rejects number 1") + test("rejects null") + test("rejects undefined") + // 自定义 inner schema + test("works with custom inner schema (z.boolean().optional())") +}) +``` + +### Mock 需求 +无 + +--- + +## 2. `src/utils/__tests__/semanticNumber.test.ts` (~10 tests) + +**源文件**: `src/utils/semanticNumber.ts` (37 行) +**依赖**: `zod/v4` + +### 测试用例 + +```typescript +describe("semanticNumber", () => { + test("parses number 42") + test("parses number 0") + test("parses negative number -5") + test("parses float 3.14") + test("parses string '42' to 42") + test("parses string '-7.5' to -7.5") + test("rejects string 'abc'") + test("rejects empty string ''") + test("rejects null") + test("rejects boolean true") + test("works with custom inner schema (z.number().int().min(0))") +}) +``` + +### Mock 需求 +无 + +--- + +## 3. `src/utils/__tests__/lazySchema.test.ts` (~6 tests) + +**源文件**: `src/utils/lazySchema.ts` (9 行) +**依赖**: 无 + +### 测试用例 + +```typescript +describe("lazySchema", () => { + test("returns a function") + test("calls factory on first invocation") + test("returns cached result on subsequent invocations") + test("factory is called only once (call count verification)") + test("works with different return types") + test("each call to lazySchema returns independent cache") +}) +``` + +### Mock 需求 +无 + +--- + +## 4. `src/utils/__tests__/withResolvers.test.ts` (~8 tests) + +**源文件**: `src/utils/withResolvers.ts` (14 行) +**依赖**: 无 + +### 测试用例 + +```typescript +describe("withResolvers", () => { + test("returns object with promise, resolve, reject") + test("promise resolves when resolve is called") + test("promise rejects when reject is called") + test("resolve passes value through") + test("reject passes error through") + test("promise is instanceof Promise") + test("works with generic type parameter") + test("resolve/reject can be called asynchronously") +}) +``` + +### Mock 需求 +无 + +--- + +## 5. `src/utils/__tests__/userPromptKeywords.test.ts` (~12 tests) + +**源文件**: `src/utils/userPromptKeywords.ts` (28 行) +**依赖**: 无 + +### 测试用例 + +```typescript +describe("matchesNegativeKeyword", () => { + test("matches 'wtf'") + test("matches 'shit'") + test("matches 'fucking broken'") + test("does not match normal input like 'fix the bug'") + test("is case-insensitive") + test("matches partial word in sentence") +}) + +describe("matchesKeepGoingKeyword", () => { + test("matches exact 'continue'") + test("matches 'keep going'") + test("matches 'go on'") + test("does not match 'cont'") + test("does not match empty string") + test("matches within larger sentence 'please continue'") +}) +``` + +### Mock 需求 +无 + +--- + +## 6. `src/utils/__tests__/xdg.test.ts` (~15 tests) + +**源文件**: `src/utils/xdg.ts` (66 行) +**依赖**: 无(通过 options 参数注入) + +### 测试用例 + +```typescript +describe("getXDGStateHome", () => { + test("returns ~/.local/state by default") + test("respects XDG_STATE_HOME env var") + test("uses custom homedir from options") +}) + +describe("getXDGCacheHome", () => { + test("returns ~/.cache by default") + test("respects XDG_CACHE_HOME env var") +}) + +describe("getXDGDataHome", () => { + test("returns ~/.local/share by default") + test("respects XDG_DATA_HOME env var") +}) + +describe("getUserBinDir", () => { + test("returns ~/.local/bin") + test("uses custom homedir from options") +}) + +describe("resolveOptions", () => { + test("defaults env to process.env") + test("defaults homedir to os.homedir()") + test("merges partial options") +}) + +describe("path construction", () => { + test("all paths end with correct subdirectory") + test("respects HOME env via homedir override") +}) +``` + +### Mock 需求 +无(通过 options.env 和 options.homedir 注入) + +--- + +## 7. `src/utils/__tests__/horizontalScroll.test.ts` (~20 tests) + +**源文件**: `src/utils/horizontalScroll.ts` (138 行) +**依赖**: 无 + +### 测试用例 + +```typescript +describe("calculateHorizontalScrollWindow", () => { + // 基本场景 + test("all items fit within available width") + test("single item selected within view") + test("selected item at beginning") + test("selected item at end") + test("selected item beyond visible range scrolls right") + test("selected item before visible range scrolls left") + + // 箭头指示器 + test("showLeftArrow when items hidden on left") + test("showRightArrow when items hidden on right") + test("no arrows when all items visible") + test("both arrows when items hidden on both sides") + + // 边界条件 + test("empty itemWidths array") + test("single item") + test("available width is 0") + test("item wider than available width") + test("all items same width") + test("varying item widths") + test("firstItemHasSeparator adds separator width to first item") + test("selectedIdx in middle of overflow") + test("scroll snaps to show selected at left edge") + test("scroll snaps to show selected at right edge") +}) +``` + +### Mock 需求 +无 + +--- + +## 8. `src/utils/__tests__/generators.test.ts` (~18 tests) + +**源文件**: `src/utils/generators.ts` (89 行) +**依赖**: 无 + +### 测试用例 + +```typescript +describe("lastX", () => { + test("returns last yielded value") + test("returns only value from single-yield generator") + test("throws on empty generator") +}) + +describe("returnValue", () => { + test("returns generator return value") + test("returns undefined for void return") +}) + +describe("toArray", () => { + test("collects all yielded values") + test("returns empty array for empty generator") + test("preserves order") +}) + +describe("fromArray", () => { + test("yields all array elements") + test("yields nothing for empty array") +}) + +describe("all", () => { + test("merges multiple generators preserving yield order") + test("respects concurrency cap") + test("handles empty generator array") + test("handles single generator") + test("handles generators of different lengths") + test("yields all values from all generators") +}) +``` + +### Mock 需求 +无(用 fromArray 构造测试数据) + +--- + +## 9. `src/utils/__tests__/sequential.test.ts` (~12 tests) + +**源文件**: `src/utils/sequential.ts` (57 行) +**依赖**: 无 + +### 测试用例 + +```typescript +describe("sequential", () => { + test("wraps async function, returns same result") + test("single call resolves normally") + test("concurrent calls execute sequentially (FIFO order)") + test("preserves arguments correctly") + test("error in first call does not block subsequent calls") + test("preserves rejection reason") + test("multiple args passed correctly") + test("returns different wrapper for each call to sequential") + test("handles rapid concurrent calls") + test("execution order matches call order") + test("works with functions returning different types") + test("wrapper has same arity expectations") +}) +``` + +### Mock 需求 +无 + +--- + +## 10. `src/utils/__tests__/fingerprint.test.ts` (~15 tests) + +**源文件**: `src/utils/fingerprint.ts` (77 行) +**依赖**: `crypto` (内置) + +### 测试用例 + +```typescript +describe("FINGERPRINT_SALT", () => { + test("has expected value '59cf53e54c78'") +}) + +describe("extractFirstMessageText", () => { + test("extracts text from first user message") + test("extracts text from single user message with array content") + test("returns empty string when no user messages") + test("skips assistant messages") + test("handles mixed content blocks (text + image)") +}) + +describe("computeFingerprint", () => { + test("returns deterministic 3-char hex string") + test("same input produces same fingerprint") + test("different message text produces different fingerprint") + test("different version produces different fingerprint") + test("handles short strings (length < 21)") + test("handles empty string") + test("fingerprint is valid hex") +}) + +describe("computeFingerprintFromMessages", () => { + test("end-to-end: messages -> fingerprint") +}) +``` + +### Mock 需求 +需要 `mock.module` 处理 `UserMessage`/`AssistantMessage` 类型依赖(查看实际 import 情况) + +--- + +## 11. `src/utils/__tests__/configConstants.test.ts` (~8 tests) + +**源文件**: `src/utils/configConstants.ts` (22 行) +**依赖**: 无 + +### 测试用例 + +```typescript +describe("NOTIFICATION_CHANNELS", () => { + test("contains expected channels") + test("is readonly array") + test("includes 'auto', 'iterm2', 'terminal_bell'") +}) + +describe("EDITOR_MODES", () => { + test("contains 'normal' and 'vim'") + test("has exactly 2 entries") +}) + +describe("TEAMMATE_MODES", () => { + test("contains 'auto', 'tmux', 'in-process'") + test("has exactly 3 entries") +}) +``` + +### Mock 需求 +无 + +--- + +## 12. `src/utils/__tests__/directMemberMessage.test.ts` (~12 tests) + +**源文件**: `src/utils/directMemberMessage.ts` (70 行) +**依赖**: 仅类型(可 mock) + +### 测试用例 + +```typescript +describe("parseDirectMemberMessage", () => { + test("parses '@agent-name hello world'") + test("parses '@agent-name single-word'") + test("returns null for non-matching input") + test("returns null for empty string") + test("returns null for '@name' without message") + test("handles hyphenated agent names like '@my-agent msg'") + test("handles multiline message content") + test("extracts correct recipientName and message") +}) + +// sendDirectMemberMessage 需要 mock teamContext/writeToMailbox +describe("sendDirectMemberMessage", () => { + test("returns error when no team context") + test("returns error for unknown recipient") + test("calls writeToMailbox with correct args for valid recipient") + test("returns success for valid message") +}) +``` + +### Mock 需求 +`sendDirectMemberMessage` 需要 mock `AppState['teamContext']` 和 `WriteToMailboxFn` + +--- + +## 13. `src/utils/__tests__/collapseHookSummaries.test.ts` (~12 tests) + +**源文件**: `src/utils/collapseHookSummaries.ts` (60 行) +**依赖**: 仅类型 + +### 测试用例 + +```typescript +describe("collapseHookSummaries", () => { + test("returns same messages when no hook summaries") + test("collapses consecutive messages with same hookLabel") + test("does not collapse messages with different hookLabels") + test("aggregates hookCount across collapsed messages") + test("merges hookInfos arrays") + test("merges hookErrors arrays") + test("takes max totalDurationMs") + test("takes any truthy preventContinuation") + test("leaves single hook summary unchanged") + test("handles three consecutive same-label summaries") + test("preserves non-hook messages in between") + test("returns empty array for empty input") +}) +``` + +### Mock 需求 +需要构造 `RenderableMessage` mock 对象 diff --git a/docs/test-plans/phase19-batch2-utils-state-commands.md b/docs/test-plans/phase19-batch2-utils-state-commands.md new file mode 100644 index 0000000..bbf69c3 --- /dev/null +++ b/docs/test-plans/phase19-batch2-utils-state-commands.md @@ -0,0 +1,287 @@ +# Phase 19 - Batch 2: 更多 utils + state + commands + +> 预计 ~120 tests / 8 文件 | 部分需轻量 mock + +--- + +## 1. `src/utils/__tests__/collapseTeammateShutdowns.test.ts` (~10 tests) + +**源文件**: `src/utils/collapseTeammateShutdowns.ts` (56 行) +**依赖**: 仅类型 + +### 测试用例 + +```typescript +describe("collapseTeammateShutdowns", () => { + test("returns same messages when no teammate shutdowns") + test("leaves single shutdown message unchanged") + test("collapses consecutive shutdown messages into batch") + test("batch attachment has correct count") + test("does not collapse non-consecutive shutdowns") + test("preserves non-shutdown messages between shutdowns") + test("handles empty array") + test("handles mixed message types") + test("collapses more than 2 consecutive shutdowns") + test("non-teammate task_status messages are not collapsed") +}) +``` + +### Mock 需求 +构造 `RenderableMessage` mock 对象(带 `task_status` attachment,`status=completed`,`taskType=in_process_teammate`) + +--- + +## 2. `src/utils/__tests__/privacyLevel.test.ts` (~12 tests) + +**源文件**: `src/utils/privacyLevel.ts` (56 行) +**依赖**: `process.env` + +### 测试用例 + +```typescript +describe("getPrivacyLevel", () => { + test("returns 'default' when no env vars set") + test("returns 'essential-traffic' when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set") + test("returns 'no-telemetry' when DISABLE_TELEMETRY is set") + test("'essential-traffic' takes priority over 'no-telemetry'") +}) + +describe("isEssentialTrafficOnly", () => { + test("returns true for 'essential-traffic' level") + test("returns false for 'default' level") + test("returns false for 'no-telemetry' level") +}) + +describe("isTelemetryDisabled", () => { + test("returns true for 'no-telemetry' level") + test("returns true for 'essential-traffic' level") + test("returns false for 'default' level") +}) + +describe("getEssentialTrafficOnlyReason", () => { + test("returns env var name when restricted") + test("returns null when unrestricted") +}) +``` + +### Mock 需求 +`process.env` 保存/恢复模式(参考现有 `envUtils.test.ts`) + +--- + +## 3. `src/utils/__tests__/textHighlighting.test.ts` (~18 tests) + +**源文件**: `src/utils/textHighlighting.ts` (167 行) +**依赖**: `@alcalzone/ansi-tokenize` + +### 测试用例 + +```typescript +describe("segmentTextByHighlights", () => { + // 基本 + test("returns single segment with no highlights") + test("returns highlighted segment for single highlight") + test("returns two segments for highlight covering middle portion") + test("returns three segments for highlight in the middle") + + // 多高亮 + test("handles non-overlapping highlights") + test("handles overlapping highlights (priority-based)") + test("handles adjacent highlights") + + // 边界 + test("highlight starting at 0") + test("highlight ending at text length") + test("highlight covering entire text") + test("empty text with highlights") + test("empty highlights array returns single segment") + + // ANSI 处理 + test("correctly segments text with ANSI escape codes") + test("handles text with mixed ANSI and highlights") + + // 属性 + test("preserves highlight color property") + test("preserves highlight priority property") + test("preserves dimColor and inverse flags") + test("highlights with start > end are handled gracefully") +}) +``` + +### Mock 需求 +可能需要 mock `@alcalzone/ansi-tokenize`,或直接使用(如果有安装) + +--- + +## 4. `src/utils/__tests__/detectRepository.test.ts` (~15 tests) + +**源文件**: `src/utils/detectRepository.ts` (179 行) +**依赖**: git 命令(`getRemoteUrl`) + +### 重点测试函数 + +**`parseGitRemote(input: string): ParsedRepository | null`** — 纯正则解析 +**`parseGitHubRepository(input: string): string | null`** — 纯函数 + +### 测试用例 + +```typescript +describe("parseGitRemote", () => { + // HTTPS + test("parses HTTPS URL: https://github.com/owner/repo.git") + test("parses HTTPS URL without .git suffix") + test("parses HTTPS URL with subdirectory path (only takes first 2 segments)") + + // SSH + test("parses SSH URL: git@github.com:owner/repo.git") + test("parses SSH URL without .git suffix") + + // ssh:// + test("parses ssh:// URL: ssh://git@github.com/owner/repo.git") + + // git:// + test("parses git:// URL") + + // 边界 + test("returns null for invalid URL") + test("returns null for empty string") + test("handles GHE hostname") + test("handles port number in URL") +}) + +describe("parseGitHubRepository", () => { + test("extracts 'owner/repo' from valid remote URL") + test("handles plain 'owner/repo' string input") + test("returns null for non-GitHub host (if restricted)") + test("returns null for invalid input") + test("is case-sensitive for owner/repo") +}) +``` + +### Mock 需求 +仅测试 `parseGitRemote` 和 `parseGitHubRepository`(纯函数),不需要 mock git + +--- + +## 5. `src/utils/__tests__/markdown.test.ts` (~20 tests) + +**源文件**: `src/utils/markdown.ts` (382 行) +**依赖**: `marked`, `cli-highlight`, theme types + +### 重点测试函数 + +**`padAligned(content, displayWidth, targetWidth, align)`** — 纯函数 + +### 测试用例 + +```typescript +describe("padAligned", () => { + test("left-aligns: pads with spaces on right") + test("right-aligns: pads with spaces on left") + test("center-aligns: pads with spaces on both sides") + test("no padding when displayWidth equals targetWidth") + test("handles content wider than targetWidth") + test("null/undefined align defaults to left") + test("handles empty string content") + test("handles zero displayWidth") + test("handles zero targetWidth") + test("center alignment with odd padding distribution") +}) +``` + +注意:`numberToLetter`/`numberToRoman`/`getListNumber` 是私有函数,除非从模块导出否则无法直接测试。如果确实私有,则通过 `applyMarkdown` 间接测试列表渲染: + +```typescript +describe("list numbering (via applyMarkdown)", () => { + test("numbered list renders with digits") + test("nested ordered list uses letters (a, b, c)") + test("deep nested list uses roman numerals") + test("unordered list uses bullet markers") +}) +``` + +### Mock 需求 +`padAligned` 无需 mock。`applyMarkdown` 可能需要 mock theme 依赖。 + +--- + +## 6. `src/state/__tests__/store.test.ts` (~15 tests) + +**源文件**: `src/state/store.ts` (35 行) +**依赖**: 无 + +### 测试用例 + +```typescript +describe("createStore", () => { + test("returns object with getState, setState, subscribe") + test("getState returns initial state") + test("setState updates state via updater function") + test("setState does not notify when state unchanged (Object.is)") + test("setState notifies subscribers on change") + test("subscribe returns unsubscribe function") + test("unsubscribe stops notifications") + test("multiple subscribers all get notified") + test("onChange callback is called on state change") + test("onChange is not called when state unchanged") + test("works with complex state objects") + test("works with primitive state") + test("updater receives previous state") + test("sequential setState calls produce final state") + test("subscriber called after all state changes in synchronous batch") +}) +``` + +### Mock 需求 +无 + +--- + +## 7. `src/commands/plugin/__tests__/parseArgs.test.ts` (~18 tests) + +**源文件**: `src/commands/plugin/parseArgs.ts` (104 行) +**依赖**: 无 + +### 测试用例 + +```typescript +describe("parsePluginArgs", () => { + // 无参数 + test("returns { type: 'menu' } for undefined") + test("returns { type: 'menu' } for empty string") + test("returns { type: 'menu' } for whitespace only") + + // help + test("returns { type: 'help' } for 'help'") + + // install + test("parses 'install my-plugin' -> { type: 'install', name: 'my-plugin' }") + test("parses 'install my-plugin@github' with marketplace") + test("parses 'install https://github.com/...' as URL marketplace") + + // uninstall + test("returns { type: 'uninstall', name: '...' }") + + // enable/disable + test("returns { type: 'enable', name: '...' }") + test("returns { type: 'disable', name: '...' }") + + // validate + test("returns { type: 'validate', name: '...' }") + + // manage + test("returns { type: 'manage' }") + + // marketplace 子命令 + test("parses 'marketplace add ...'") + test("parses 'marketplace remove ...'") + test("parses 'marketplace list'") + + // 边界 + test("handles extra whitespace") + test("handles unknown subcommand gracefully") +}) +``` + +### Mock 需求 +无 diff --git a/docs/test-plans/phase19-batch3-tool-submodules.md b/docs/test-plans/phase19-batch3-tool-submodules.md new file mode 100644 index 0000000..f19e70e --- /dev/null +++ b/docs/test-plans/phase19-batch3-tool-submodules.md @@ -0,0 +1,258 @@ +# Phase 19 - Batch 3: Tool 子模块纯逻辑 + +> 预计 ~113 tests / 6 文件 | 采用 `mock.module()` + `await import()` 模式 + +--- + +## 1. `src/tools/GrepTool/__tests__/headLimit.test.ts` (~20 tests) + +**源文件**: `src/tools/GrepTool/GrepTool.ts` (578 行) +**目标函数**: `applyHeadLimit`, `formatLimitInfo` (非导出,需确认可测性) + +### 测试策略 +如果函数是文件内导出的,直接 `await import()` 获取。如果私有,则通过 GrepTool 的输出间接测试,或提取到独立文件。 + +### 测试用例 + +```typescript +describe("applyHeadLimit", () => { + test("returns full array when limit is undefined (default 250)") + test("applies limit correctly: limits to N items") + test("limit=0 means no limit (returns all)") + test("applies offset correctly") + test("offset + limit combined") + test("offset beyond array length returns empty") + test("returns appliedLimit when truncation occurred") + test("returns appliedLimit=undefined when no truncation") + test("limit larger than array returns all items with appliedLimit=undefined") + test("empty array returns empty with appliedLimit=undefined") + test("offset=0 is default") + test("negative limit behavior") +}) + +describe("formatLimitInfo", () => { + test("formats 'limit: N, offset: M' when both present") + test("formats 'limit: N' when only limit") + test("formats 'offset: M' when only offset") + test("returns empty string when both undefined") + test("handles limit=0 (no limit, should not appear)") +}) +``` + +### Mock 需求 +需 mock 重依赖链(`log`, `slowOperations` 等),通过 `mock.module()` + `await import()` 只取目标函数 + +--- + +## 2. `src/tools/MCPTool/__tests__/classifyForCollapse.test.ts` (~25 tests) + +**源文件**: `src/tools/MCPTool/classifyForCollapse.ts` (605 行) +**目标函数**: `classifyMcpToolForCollapse`, `normalize` + +### 测试用例 + +```typescript +describe("normalize", () => { + test("leaves snake_case unchanged: 'search_issues'") + test("converts camelCase to snake_case: 'searchIssues' -> 'search_issues'") + test("converts kebab-case to snake_case: 'search-issues' -> 'search_issues'") + test("handles mixed: 'searchIssuesByStatus' -> 'search_issues_by_status'") + test("handles already lowercase single word") + test("handles empty string") + test("handles PascalCase: 'SearchIssues' -> 'search_issues'") +}) + +describe("classifyMcpToolForCollapse", () => { + // 搜索工具 + test("classifies Slack search_messages as search") + test("classifies GitHub search_code as search") + test("classifies Linear search_issues as search") + test("classifies Datadog search_logs as search") + test("classifies Notion search as search") + + // 读取工具 + test("classifies Slack get_message as read") + test("classifies GitHub get_file_contents as read") + test("classifies Linear get_issue as read") + test("classifies Filesystem read_file as read") + + // 双重分类 + test("some tools are both search and read") + test("some tools are neither search nor read") + + // 未知工具 + test("unknown tool returns { isSearch: false, isRead: false }") + test("tool name with camelCase variant still matches") + test("tool name with kebab-case variant still matches") + + // server name 不影响分类 + test("server name parameter is accepted but unused in current logic") + + // 边界 + test("empty tool name returns false/false") + test("case sensitivity check (should match after normalize)") + test("handles tool names with numbers") +}) +``` + +### Mock 需求 +文件自包含(仅内部 Set + normalize 函数),需确认 `normalize` 是否导出 + +--- + +## 3. `src/tools/FileReadTool/__tests__/blockedPaths.test.ts` (~18 tests) + +**源文件**: `src/tools/FileReadTool/FileReadTool.ts` (1184 行) +**目标函数**: `isBlockedDevicePath`, `getAlternateScreenshotPath` + +### 测试用例 + +```typescript +describe("isBlockedDevicePath", () => { + // 阻止的设备 + test("blocks /dev/zero") + test("blocks /dev/random") + test("blocks /dev/urandom") + test("blocks /dev/full") + test("blocks /dev/stdin") + test("blocks /dev/tty") + test("blocks /dev/console") + test("blocks /dev/stdout") + test("blocks /dev/stderr") + test("blocks /dev/fd/0") + test("blocks /dev/fd/1") + test("blocks /dev/fd/2") + + // 阻止 /proc + test("blocks /proc/self/fd/0") + test("blocks /proc/123/fd/2") + + // 允许的路径 + test("allows /dev/null") + test("allows regular file paths") + test("allows /home/user/file.txt") +}) + +describe("getAlternateScreenshotPath", () => { + test("returns undefined for path without AM/PM") + test("returns alternate path for macOS screenshot with regular space before AM") + test("returns alternate path for macOS screenshot with U+202F before PM") + test("handles path without time component") + test("handles multiple AM/PM occurrences") + test("returns undefined when no space variant difference") +}) +``` + +### Mock 需求 +需 mock 重依赖链,通过 `await import()` 获取函数 + +--- + +## 4. `src/tools/AgentTool/__tests__/agentDisplay.test.ts` (~15 tests) + +**源文件**: `src/tools/AgentTool/agentDisplay.ts` (105 行) +**目标函数**: `resolveAgentOverrides`, `compareAgentsByName` + +### 测试用例 + +```typescript +describe("resolveAgentOverrides", () => { + test("marks no overrides when all agents active") + test("marks inactive agent as overridden") + test("overriddenBy shows the overriding agent source") + test("deduplicates agents by (agentType, source)") + test("preserves agent definition properties") + test("handles empty arrays") + test("handles agent from git worktree (duplicate detection)") +}) + +describe("compareAgentsByName", () => { + test("sorts alphabetically ascending") + test("returns negative when a.name < b.name") + test("returns positive when a.name > b.name") + test("returns 0 for same name") + test("is case-sensitive") +}) + +describe("AGENT_SOURCE_GROUPS", () => { + test("contains expected source groups in order") + test("has unique labels") +}) +``` + +### Mock 需求 +需 mock `AgentDefinition`, `AgentSource` 类型依赖 + +--- + +## 5. `src/tools/AgentTool/__tests__/agentToolUtils.test.ts` (~20 tests) + +**源文件**: `src/tools/AgentTool/agentToolUtils.ts` (688 行) +**目标函数**: `countToolUses`, `getLastToolUseName`, `extractPartialResult` + +### 测试用例 + +```typescript +describe("countToolUses", () => { + test("counts tool_use blocks in messages") + test("returns 0 for messages without tool_use") + test("returns 0 for empty array") + test("counts multiple tool_use blocks across messages") + test("counts tool_use in single message with multiple blocks") +}) + +describe("getLastToolUseName", () => { + test("returns last tool name from assistant message") + test("returns undefined for message without tool_use") + test("returns the last tool when multiple tool_uses present") + test("handles message with non-array content") +}) + +describe("extractPartialResult", () => { + test("extracts text from last assistant message") + test("returns undefined for messages without assistant content") + test("handles interrupted agent with partial text") + test("returns undefined for empty messages") + test("concatenates multiple text blocks") + test("skips non-text content blocks") +}) +``` + +### Mock 需求 +需 mock 消息类型依赖 + +--- + +## 6. `src/tools/SkillTool/__tests__/skillSafety.test.ts` (~15 tests) + +**源文件**: `src/tools/SkillTool/SkillTool.ts` (1110 行) +**目标函数**: `skillHasOnlySafeProperties`, `extractUrlScheme` + +### 测试用例 + +```typescript +describe("skillHasOnlySafeProperties", () => { + test("returns true for command with only safe properties") + test("returns true for command with undefined extra properties") + test("returns false for command with unsafe meaningful property") + test("returns true for command with null extra properties") + test("returns true for command with empty array extra property") + test("returns true for command with empty object extra property") + test("returns false for command with non-empty unsafe array") + test("returns false for command with non-empty unsafe object") + test("returns true for empty command object") +}) + +describe("extractUrlScheme", () => { + test("extracts 'gs' from 'gs://bucket/path'") + test("extracts 'https' from 'https://example.com'") + test("extracts 'http' from 'http://example.com'") + test("extracts 's3' from 's3://bucket/path'") + test("defaults to 'gs' for unknown scheme") + test("defaults to 'gs' for path without scheme") + test("defaults to 'gs' for empty string") +}) +``` + +### Mock 需求 +需 mock 重依赖链,`await import()` 获取函数 diff --git a/docs/test-plans/phase19-batch4-services.md b/docs/test-plans/phase19-batch4-services.md new file mode 100644 index 0000000..132e220 --- /dev/null +++ b/docs/test-plans/phase19-batch4-services.md @@ -0,0 +1,215 @@ +# Phase 19 - Batch 4: Services 纯逻辑 + +> 预计 ~84 tests / 5 文件 | 部分需轻量 mock + +--- + +## 1. `src/services/compact/__tests__/grouping.test.ts` (~15 tests) + +**源文件**: `src/services/compact/grouping.ts` (64 行) +**目标函数**: `groupMessagesByApiRound` + +### 测试用例 + +```typescript +describe("groupMessagesByApiRound", () => { + test("returns single group for single API round") + test("splits at new assistant message ID") + test("keeps tool_result messages with their parent assistant message") + test("handles streaming chunks (same assistant ID stays grouped)") + test("returns empty array for empty input") + test("handles all user messages (no assistant)") + test("handles alternating assistant IDs") + test("three API rounds produce three groups") + test("user messages before first assistant go in first group") + test("consecutive user messages stay in same group") + test("does not produce empty groups") + test("handles single message") + test("preserves message order within groups") + test("handles system messages") + test("tool_result after assistant stays in same round") +}) +``` + +### Mock 需求 +需构造 `Message` mock 对象(type: 'user'/'assistant', message: { id, content }) + +--- + +## 2. `src/services/compact/__tests__/stripMessages.test.ts` (~20 tests) + +**源文件**: `src/services/compact/compact.ts` (1709 行) +**目标函数**: `stripImagesFromMessages`, `collectReadToolFilePaths` (私有) + +### 测试用例 + +```typescript +describe("stripImagesFromMessages", () => { + // user 消息处理 + test("replaces image block with [image] text") + test("replaces document block with [document] text") + test("preserves text blocks unchanged") + test("handles multiple image/document blocks in single message") + test("returns original message when no media blocks") + + // tool_result 内嵌套 + test("replaces image inside tool_result content") + test("replaces document inside tool_result content") + test("preserves non-media tool_result content") + + // 非用户消息 + test("passes through assistant messages unchanged") + test("passes through system messages unchanged") + + // 边界 + test("handles empty message array") + test("handles string content (non-array) in user message") + test("does not mutate original messages") +}) + +describe("collectReadToolFilePaths", () => { + // 注意:这是私有函数,可能需要通过 stripImagesFromMessages 或其他导出间接测试 + // 如果不可直接测试,则跳过或通过集成测试覆盖 + test("collects file_path from Read tool_use blocks") + test("skips tool_use with FILE_UNCHANGED_STUB result") + test("returns empty set for messages without Read tool_use") + test("handles multiple Read calls across messages") + test("normalizes paths via expandPath") +}) +``` + +### Mock 需求 +需 mock `expandPath`(如果 collectReadToolFilePaths 要测) +需 mock `log`, `slowOperations` 等重依赖 +构造 `Message` mock 对象 + +--- + +## 3. `src/services/compact/__tests__/prompt.test.ts` (~12 tests) + +**源文件**: `src/services/compact/prompt.ts` (375 行) +**目标函数**: `formatCompactSummary` + +### 测试用例 + +```typescript +describe("formatCompactSummary", () => { + test("strips ... block") + test("replaces ... with 'Summary:\\n' prefix") + test("handles analysis + summary together") + test("handles summary without analysis") + test("handles analysis without summary") + test("collapses multiple newlines to double") + test("trims leading/trailing whitespace") + test("handles empty string") + test("handles plain text without tags") + test("handles multiline analysis content") + test("preserves content between analysis and summary") + test("handles nested-like tags gracefully") +}) +``` + +### Mock 需求 +需 mock 重依赖链(`log`, feature flags 等) +`formatCompactSummary` 是纯字符串处理,如果 import 链不太重则无需复杂 mock + +--- + +## 4. `src/services/mcp/__tests__/channelPermissions.test.ts` (~25 tests) + +**源文件**: `src/services/mcp/channelPermissions.ts` (241 行) +**目标函数**: `hashToId`, `shortRequestId`, `truncateForPreview`, `filterPermissionRelayClients` + +### 测试用例 + +```typescript +describe("hashToId", () => { + test("returns 5-char string") + test("uses only letters a-z excluding 'l'") + test("is deterministic (same input = same output)") + test("different inputs produce different outputs (with high probability)") + test("handles empty string") +}) + +describe("shortRequestId", () => { + test("returns 5-char string from tool use ID") + test("is deterministic") + test("avoids profanity substrings (retries with salt)") + test("returns a valid ID even if all retries hit bad words (unlikely)") +}) + +describe("truncateForPreview", () => { + test("returns JSON string for object input") + test("truncates to <=200 chars when input is long") + test("adds ellipsis or truncation indicator") + test("returns short input unchanged") + test("handles string input") + test("handles null/undefined input") +}) + +describe("filterPermissionRelayClients", () => { + test("keeps connected clients in allowlist with correct capabilities") + test("filters out disconnected clients") + test("filters out clients not in allowlist") + test("filters out clients missing required capabilities") + test("returns empty array for empty input") + test("type predicate narrows correctly") +}) + +describe("PERMISSION_REPLY_RE", () => { + test("matches 'y abcde'") + test("matches 'yes abcde'") + test("matches 'n abcde'") + test("matches 'no abcde'") + test("is case-insensitive") + test("does not match without ID") +}) +``` + +### Mock 需求 +`hashToId` 可能需要确认导出状态 +`filterPermissionRelayClients` 需要 mock 客户端类型 +`truncateForPreview` 可能依赖 `jsonStringify`(需 mock `slowOperations`) + +--- + +## 5. `src/services/mcp/__tests__/officialRegistry.test.ts` (~12 tests) + +**源文件**: `src/services/mcp/officialRegistry.ts` (73 行) +**目标函数**: `normalizeUrl` (私有), `isOfficialMcpUrl`, `resetOfficialMcpUrlsForTesting` + +### 测试用例 + +```typescript +describe("normalizeUrl", () => { + // 注意:如果是私有的,通过 isOfficialMcpUrl 间接测试 + test("removes trailing slash") + test("removes query parameters") + test("preserves path") + test("handles URL with port") + test("handles URL with hash fragment") +}) + +describe("isOfficialMcpUrl", () => { + test("returns false when registry not loaded (initial state)") + test("returns true for URL added to registry") + test("returns false for non-registered URL") + test("uses normalized URL for comparison") +}) + +describe("resetOfficialMcpUrlsForTesting", () => { + test("clears the cached URLs") + test("allows fresh start after reset") +}) + +describe("URL normalization + lookup integration", () => { + test("URL with trailing slash matches normalized version") + test("URL with query params matches normalized version") + test("different URLs do not match") + test("case sensitivity check") +}) +``` + +### Mock 需求 +需 mock `axios`(避免网络请求) +使用 `resetOfficialMcpUrlsForTesting` 做测试隔离 diff --git a/docs/test-plans/phase19-batch5-mcp-config.md b/docs/test-plans/phase19-batch5-mcp-config.md new file mode 100644 index 0000000..1763cb4 --- /dev/null +++ b/docs/test-plans/phase19-batch5-mcp-config.md @@ -0,0 +1,200 @@ +# Phase 19 - Batch 5: MCP 配置 + modelCost + +> 预计 ~80 tests / 4 文件 | 需中等 mock + +--- + +## 1. `src/services/mcp/__tests__/configUtils.test.ts` (~30 tests) + +**源文件**: `src/services/mcp/config.ts` (1580 行) +**目标函数**: `unwrapCcrProxyUrl`, `urlPatternToRegex` (私有), `commandArraysMatch` (私有), `toggleMembership` (私有), `addScopeToServers` (私有), `dedupPluginMcpServers`, `getMcpServerSignature` (如导出) + +### 测试策略 +私有函数如不可直接测试,通过公开的 `dedupPluginMcpServers` 间接覆盖。导出函数直接测。 + +### 测试用例 + +```typescript +describe("unwrapCcrProxyUrl", () => { + test("returns original URL when no CCR proxy markers") + test("extracts mcp_url from CCR proxy URL with /v2/session_ingress/shttp/mcp/") + test("extracts mcp_url from CCR proxy URL with /v2/ccr-sessions/") + test("returns original URL when mcp_url param is missing") + test("handles malformed URL gracefully") + test("handles URL with both proxy marker and mcp_url") + test("preserves non-CCR URLs unchanged") +}) + +describe("dedupPluginMcpServers", () => { + test("keeps unique plugin servers") + test("suppresses plugin server duplicated by manual config") + test("suppresses plugin server duplicated by earlier plugin") + test("keeps servers with null signature") + test("returns empty for empty inputs") + test("reports suppressed with correct duplicateOf name") + test("handles multiple plugins with same config") +}) + +describe("toggleMembership (via integration)", () => { + test("adds item when shouldContain=true and not present") + test("removes item when shouldContain=false and present") + test("returns same array when already in desired state") +}) + +describe("addScopeToServers (via integration)", () => { + test("adds scope to each server config") + test("returns empty object for undefined input") + test("returns empty object for empty input") + test("preserves all original config properties") +}) + +describe("urlPatternToRegex (via integration)", () => { + test("matches exact URL") + test("matches wildcard pattern *.example.com") + test("matches multiple wildcards") + test("does not match non-matching URL") + test("escapes regex special characters in pattern") +}) + +describe("commandArraysMatch (via integration)", () => { + test("returns true for identical arrays") + test("returns false for different lengths") + test("returns false for same length different elements") + test("returns true for empty arrays") +}) +``` + +### Mock 需求 +需 mock `feature()` (bun:bundle), `jsonStringify`, `safeParseJSON`, `log` 等 +通过 `mock.module()` + `await import()` 解锁 + +--- + +## 2. `src/services/mcp/__tests__/filterUtils.test.ts` (~20 tests) + +**源文件**: `src/services/mcp/utils.ts` (576 行) +**目标函数**: `filterToolsByServer`, `hashMcpConfig`, `isToolFromMcpServer`, `isMcpTool`, `parseHeaders` + +### 测试用例 + +```typescript +describe("filterToolsByServer", () => { + test("filters tools matching server name prefix") + test("returns empty for no matching tools") + test("handles empty tools array") + test("normalizes server name for matching") +}) + +describe("hashMcpConfig", () => { + test("returns 16-char hex string") + test("is deterministic") + test("excludes scope from hash") + test("different configs produce different hashes") + test("key order does not affect hash (sorted)") +}) + +describe("isToolFromMcpServer", () => { + test("returns true when tool belongs to specified server") + test("returns false for different server") + test("returns false for non-MCP tool name") + test("handles empty tool name") +}) + +describe("isMcpTool", () => { + test("returns true for tool name starting with 'mcp__'") + test("returns true when tool.isMcp is true") + test("returns false for regular tool") + test("returns false when neither condition met") +}) + +describe("parseHeaders", () => { + test("parses 'Key: Value' format") + test("parses multiple headers") + test("trims whitespace around key and value") + test("throws on missing colon") + test("throws on empty key") + test("handles value with colons (like URLs)") + test("returns empty object for empty array") + test("handles duplicate keys (last wins)") +}) +``` + +### Mock 需求 +需 mock `normalizeNameForMCP`, `mcpInfoFromString`, `jsonStringify`, `createHash` 等 +`parseHeaders` 是最独立的,可能不需要太多 mock + +--- + +## 3. `src/services/mcp/__tests__/channelNotification.test.ts` (~15 tests) + +**源文件**: `src/services/mcp/channelNotification.ts` (317 行) +**目标函数**: `wrapChannelMessage`, `findChannelEntry` + +### 测试用例 + +```typescript +describe("wrapChannelMessage", () => { + test("wraps content in tag with source attribute") + test("escapes server name in attribute") + test("includes meta attributes when provided") + test("escapes meta values via escapeXmlAttr") + test("filters out meta keys not matching SAFE_META_KEY pattern") + test("handles empty meta") + test("handles content with special characters") + test("formats with newlines between tags and content") +}) + +describe("findChannelEntry", () => { + test("finds server entry by exact name match") + test("finds plugin entry by matching second segment") + test("returns undefined for no match") + test("handles empty channels array") + test("handles server name without colon") + test("handles 'plugin:name' format correctly") + test("prefers exact match over partial match") +}) +``` + +### Mock 需求 +需 mock `escapeXmlAttr`(来自 xml.ts,已有测试)或直接使用 +`CHANNEL_TAG` 常量需确认导出 + +--- + +## 4. `src/utils/__tests__/modelCost.test.ts` (~15 tests) + +**源文件**: `src/utils/modelCost.ts` (232 行) +**目标函数**: `formatModelPricing`, `COST_TIER_*` 常量 + +### 测试用例 + +```typescript +describe("COST_TIER constants", () => { + test("COST_TIER_3_15 has inputTokens=3, outputTokens=15") + test("COST_TIER_15_75 has inputTokens=15, outputTokens=75") + test("COST_TIER_5_25 has inputTokens=5, outputTokens=25") + test("COST_TIER_30_150 has inputTokens=30, outputTokens=150") + test("COST_HAIKU_35 has inputTokens=0.8, outputTokens=4") + test("COST_HAIKU_45 has inputTokens=1, outputTokens=5") +}) + +describe("formatModelPricing", () => { + test("formats integer prices without decimals: '$3/$15 per Mtok'") + test("formats float prices with 2 decimals: '$0.80/$4.00 per Mtok'") + test("formats mixed: '$5/$25 per Mtok'") + test("formats large prices: '$30/$150 per Mtok'") + test("formats $1/$5 correctly (integer but small)") + test("handles zero prices: '$0/$0 per Mtok'") +}) + +describe("MODEL_COSTS", () => { + test("maps known model names to cost tiers") + test("contains entries for claude-sonnet-4-6") + test("contains entries for claude-opus-4-6") + test("contains entries for claude-haiku-4-5") +}) +``` + +### Mock 需求 +需 mock `log`, `slowOperations` 等重依赖(modelCost.ts 通常 import 链较重) +`formatModelPricing` 和 `COST_TIER_*` 是纯数据/纯函数,mock 成功后直接测