## Keyboard shortcuts

Press `←` or `→` to navigate between chapters

Press `S` or `/` to search in the book

Press `?` to show this help

Press `Esc` to hide this help

# 第 22 章：测试策略

> “测试 AI Agent 系统是软件工程中最具挑战性的任务之一 —— 当被测对象本身是非确定性的，传统的断言模型几乎全部失效。”

Claude Code 面临一个独特的测试难题：它的核心行为依赖于大型语言模型的输出，而这个输出是非确定性的。如何在这种条件下构建可靠的测试体系？本章将揭示 Claude Code 的测试策略。

## 22.1 测试分层

### 22.1.1 测试金字塔

Claude Code 的测试分为四个层次：

    graph TD
        subgraph "测试金字塔"
            E2E[端到端测试<br/>真实 API 集成]
            Integration[集成测试<br/>模块间交互]
            Component[组件测试<br/>React 组件]
            Unit[单元测试<br/>纯函数逻辑]
        end

        E2E --> |少量，高成本| Real[真实 API 调用]
        Integration --> |中等数量| Mock[模拟 API 响应]
        Component --> |大量| Snapshot[快照测试]
        Unit --> |最多| Pure[纯逻辑测试]

        style E2E fill:#ff9999
        style Integration fill:#ffcc99
        style Component fill:#ffff99
        style Unit fill:#99ff99

### 22.1.2 单元测试策略

纯函数是最容易测试的部分。消息处理、Token 估算、路径归一化等函数有确定性的输入输出映射：

    // 示例：compact grouping 的测试
    describe('groupMessagesByApiRound', () => {
      it('should group messages by assistant message id boundaries', () => {
        const messages = [
          createUserMessage({ content: 'hello' }),
          createAssistantMessage({ id: 'a1', content: 'response 1' }),
          createUserMessage({ content: 'follow up' }),
          createAssistantMessage({ id: 'a2', content: 'response 2' }),
        ]
        const groups = groupMessagesByApiRound(messages)
        expect(groups).toHaveLength(2)
        expect(groups[0]).toHaveLength(2)  // user + assistant a1
        expect(groups[1]).toHaveLength(2)  // user + assistant a2
      })
    })

### 22.1.3 权限系统测试

权限分类器是 Claude Code 的安全关键路径，需要特别详尽的测试：

    // 测试 Bash 命令的安全分类
    describe('bashPermissions', () => {
      it('should block rm -rf /', () => {
        const result = classifyCommand('rm -rf /')
        expect(result).toBe('deny')
      })

      it('should allow ls -la', () => {
        const result = classifyCommand('ls -la')
        expect(result).toBe('allow')
      })

      it('should require confirmation for pip install', () => {
        const result = classifyCommand('pip install requests')
        expect(result).toBe('needs_confirm')
      })
    })

## 22.2 Harness Engineering —— 测试基础设施

### 22.2.1 测试工具库

Claude Code 的测试中有一个重要的概念 —— Harness（测试夹具）。这些是可复用的测试基础设施，处理常见的测试场景：

    // 消息工厂函数
    function createTestUserMessage(content: string): UserMessage {
      return {
        type: 'user',
        message: { role: 'user', content },
        uuid: randomUUID(),
        timestamp: new Date().toISOString(),
      }
    }

    function createTestAssistantMessage(content: string): AssistantMessage {
      return {
        type: 'assistant',
        message: {
          id: `msg_${randomUUID()}`,
          type: 'message',
          role: 'assistant',
          content: [{ type: 'text', text: content }],
        },
        uuid: randomUUID(),
        timestamp: new Date().toISOString(),
      }
    }

### 22.2.2 Store 测试

Store 的测试验证状态管理的核心契约：

    describe('createStore', () => {
      it('should not notify when state is identical (Object.is)', () => {
        const listener = jest.fn()
        const store = createStore({ count: 0 })
        store.subscribe(listener)

        // 返回相同引用 → 不通知
        store.setState(prev => prev)
        expect(listener).not.toHaveBeenCalled()

        // 返回不同引用 → 通知
        store.setState(prev => ({ ...prev, count: 1 }))
        expect(listener).toHaveBeenCalledTimes(1)
      })

      it('should call onChange with old and new state', () => {
        const onChange = jest.fn()
        const store = createStore({ count: 0 }, onChange)

        store.setState(prev => ({ ...prev, count: 1 }))

        expect(onChange).toHaveBeenCalledWith({
          newState: { count: 1 },
          oldState: { count: 0 },
        })
      })
    })

### 22.2.3 会话恢复测试

会话恢复涉及复杂的边界情况，需要系统性测试：

    describe('deserializeMessages', () => {
      it('should filter unresolved tool uses', () => {
        const messages = [
          createTestUserMessage('hello'),
          createTestAssistantMessage('I will read a file'),
          // tool_use 没有对应的 tool_result（崩溃场景）
          createToolUseMessage('Read', { file_path: '/tmp/test' }),
        ]
        const result = deserializeMessages(messages)
        // tool_use 没有匹配的 result → 被过滤
        expect(result).toHaveLength(2)
      })

      it('should detect interrupted prompt', () => {
        const messages = [
          createTestUserMessage('hello'),
          createTestAssistantMessage('response'),
          createTestUserMessage('do something'),
          // 没有后续 assistant 响应 → 中断
        ]
        const result = deserializeMessagesWithInterruptDetection(messages)
        expect(result.turnInterruptionState.kind).toBe('interrupted_prompt')
      })
    })

## 22.3 AI 系统测试

### 22.3.1 非确定性系统的测试策略

测试 AI Agent 的关键挑战在于非确定性。Claude Code 采用以下策略：

    graph TD
        subgraph "确定性层（完全可测）"
            A1[消息序列化/反序列化]
            A2[Token 计算]
            A3[权限分类]
            A4[路径处理]
        end

        subgraph "半确定性层（结构化测试）"
            B1[压缩摘要格式]
            B2[工具输入验证]
            B3[系统提示构建]
        end

        subgraph "非确定性层（行为测试）"
            C1[模型响应质量]
            C2[工具选择策略]
            C3[多轮对话一致性]
        end

确定性层 —— 使用传统的断言测试。消息处理、Token 计算等函数有确定性的输入输出映射。

半确定性层 —— 使用结构验证。例如压缩摘要必须包含 `<summary>` 标签，但标签内的内容是非确定性的。

非确定性层 —— 使用行为特征测试。不断言精确输出，而是断言输出的结构特征（如“应该包含文件名“、“不应该包含被删除的上下文”）。

### 22.3.2 Compact 摘要测试

    describe('formatCompactSummary', () => {
      it('should strip analysis section', () => {
        const summary = `<analysis>
    thinking process here...
    </analysis>

    <summary>
    1. Primary Request: Fix the bug
    2. Key Technical Concepts: React, TypeScript
    </summary>`

        const result = formatCompactSummary(summary)
        expect(result).not.toContain('<analysis>')
        expect(result).toContain('Summary:')
        expect(result).toContain('Fix the bug')
      })
    })

### 22.3.3 Feature Flag 测试

`feature()` 宏在不同构建配置下会产生不同的代码路径。测试需要覆盖两种构建：

    // 外部构建：feature('VOICE_MODE') === false
    describe('external build', () => {
      it('should use noop voice integration', () => {
        const integration = useVoiceIntegration()
        expect(integration.handleKeyEvent).toBeDefined()
        // 空实现不应抛出
        integration.handleKeyEvent()
      })
    })

### 22.3.4 FileStateCache 测试

    describe('FileStateCache', () => {
      it('should normalize paths for consistent access', () => {
        const cache = createFileStateCacheWithSizeLimit(10)
        cache.set('/a/b/../c/file.ts', { content: 'test', timestamp: 1, offset: undefined, limit: undefined })

        // 归一化路径应命中同一条目
        expect(cache.get('/a/c/file.ts')).toBeDefined()
        expect(cache.get('/a/c/file.ts')!.content).toBe('test')
      })

      it('should evict by size before reaching max entries', () => {
        // 1MB 大小限制，100 条目限制
        const cache = createFileStateCacheWithSizeLimit(100, 1024 * 1024)

        // 写入超过大小限制的内容
        for (let i = 0; i < 50; i++) {
          cache.set(`/file${i}.ts`, {
            content: 'x'.repeat(100 * 1024), // 100KB each
            timestamp: i,
            offset: undefined,
            limit: undefined,
          })
        }

        // 由于大小限制，不会保留全部 50 个
        expect(cache.size).toBeLessThan(50)
      })
    })

## 22.4 测试文化与实践

### 22.4.1 测试命名规范

从代码注释中可以看到测试的命名传统：

    // @internal Exported for testing - use loadConversationForResume instead
    export function deserializeMessages(serializedMessages: Message[]): Message[] { ... }

内部函数如果需要测试，会标记为 `@internal Exported for testing`，并注明正常调用方应使用的公共接口。

### 22.4.2 数据驱动的测试决策

代码中频繁出现的 BQ（BigQuery）数据引用说明团队使用生产数据指导测试优先级：

    // BQ 2026-03-10: 1,279 sessions had 50+ consecutive failures
    // → 引出了断路器测试

    // BQ 2026-03-01: missing this made 20% of tengu_prompt_cache_break events false positives
    // → 引出了压缩后缓存基线重置的测试

### 22.4.3 防回归测试

一些测试直接针对历史 Bug：

    // 测试 drift assertion: microCompact 中内联的常量字符串
    // 必须与源文件保持一致
    it('TIME_BASED_MC_CLEARED_MESSAGE must match toolResultStorage.ts', () => {
      // Drift is caught by a test asserting equality with the source-of-truth.
    })

这种“常量一致性“测试防止了跨文件复制的字符串出现漂移。

## 22.5 Harness Engineering —— AI Agent 的质量保证方法论

### 22.5.1 什么是 Harness Engineering

Harness Engineering 是为 AI Agent 系统构建专用测试基础设施的工程实践。与传统软件测试不同，AI Agent 的核心行为依赖非确定性的模型输出，传统的“输入 → 断言精确输出“模式不再适用。Claude Code 的 Harness Engineering 包含三个支柱：

    graph TD
        HE[Harness Engineering] --> Infra[测试基础设施]
        HE --> Strategy[分层测试策略]
        HE --> Safety[安全回归防线]

        Infra --> TF[测试工厂函数<br/>createTestUserMessage 等]
        Infra --> TP[TestingPermissionTool<br/>权限测试夹具]
        Infra --> MC[模拟 Context<br/>ToolUseContext 构建器]

        Strategy --> Det[确定性层: 精确断言]
        Strategy --> Semi[半确定性层: 结构验证]
        Strategy --> NonDet[非确定性层: 行为特征]

        Safety --> Classifier[分类器回归测试<br/>200+ 命令用例]
        Safety --> Drift[常量漂移检测]
        Safety --> BQ[BQ 数据驱动]

### 22.5.2 TestingPermissionTool —— 权限测试的专用夹具

`TestingPermissionTool` 是一个仅在测试环境中启用的工具，它的唯一目的是验证权限系统的端到端行为：

    // src/tools/testing/TestingPermissionTool.tsx
    export const TestingPermissionTool: Tool<InputSchema, string> = buildTool({
      name: 'TestingPermission',
      isEnabled() {
        return process.env.NODE_ENV === 'test'  // 仅测试环境
      },
      isConcurrencySafe() { return true },
      isReadOnly() { return true },
      async checkPermissions() {
        return {
          behavior: 'ask' as const,   // 始终触发权限对话框
          message: 'Run test?',
        }
      },
      async call() {
        return { data: 'TestingPermission executed successfully' }
      },
    })

注册机制同样受环境门控：

    // src/tools.ts
    ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),

这个工具的设计体现了 Harness Engineering 的核心理念：为测试创建专用的可控组件，而非在生产代码中注入测试逻辑。TestingPermissionTool 的 `checkPermissions` 始终返回 `ask`，使得 E2E 测试可以稳定地验证权限对话框的显示、用户确认和拒绝流程。

### 22.5.3 安全分类器的回归测试

Bash 安全分类器是 Claude Code 最关键的安全组件，其回归测试覆盖了数百个命令用例：

    // 分类器回归测试的结构
    describe('Bash security classifier', () => {
      // 明确安全的命令
      const SAFE_COMMANDS = [
        'ls -la', 'cat README.md', 'git status', 'node --version',
        'echo hello', 'pwd', 'which node', 'head -n 10 file.txt',
      ]

      // 明确危险的命令
      const DANGEROUS_COMMANDS = [
        'rm -rf /', 'rm -rf ~', 'dd if=/dev/zero of=/dev/sda',
        'chmod -R 777 /', 'mkfs.ext4 /dev/sda',
        ':(){ :|:& };:',  // fork bomb
      ]

      // 需要确认但非危险的命令
      const NEEDS_CONFIRMATION = [
        'pip install requests', 'npm install -g',
        'curl https://example.com | bash',
        'docker rm $(docker ps -aq)',
      ]

      // 绕过检测（Zsh 特有）
      const ZSH_BYPASS_ATTEMPTS = [
        '=rm -rf /',           // Zsh equals expansion
        "echo 'safe'$(rm -rf /)",  // 命令替换注入
      ]

      for (const cmd of SAFE_COMMANDS) {
        it(`should allow: ${cmd}`, () => {
          expect(classifyCommand(cmd).behavior).toBe('allow')
        })
      }

      for (const cmd of DANGEROUS_COMMANDS) {
        it(`should deny: ${cmd}`, () => {
          expect(classifyCommand(cmd).behavior).toBe('deny')
        })
      }
    })

这种“正例 + 反例 + 绕过尝试“的测试矩阵确保了分类器在安全关键场景下的可靠性。每当生产环境发现新的绕过方式，都会立即添加对应的回归用例。

### 22.5.4 常量漂移检测（Drift Detection）

跨文件复制的常量容易出现漂移。Claude Code 使用断言测试来防止这种问题（参见第 9 章 ContentReplacementState 的 TIME_BASED_MC_CLEARED_MESSAGE）：

    // 测试内联常量与源文件保持一致
    it('inlined constant must match source of truth', () => {
      const inlined = require('../services/compact/microCompact.js')
        .TIME_BASED_MC_CLEARED_MESSAGE
      const sourceOfTruth = require('../utils/toolResultStorage.js')
        .TIME_BASED_MC_CLEARED_MESSAGE
      expect(inlined).toBe(sourceOfTruth)
    })

这是一种“编译器做不到的验证“ —— TypeScript 无法检测两个不同文件中的字符串常量是否保持同步，但测试可以。

### 22.5.5 可复现的 Agent 测试模式

测试 Agent 行为时，Claude Code 采用“固定输入 + 结构断言“模式：

    // 不断言精确的模型输出
    // BAD: expect(response.text).toBe('I will read the file for you')

    // 断言行为结构
    // GOOD: expect(response.toolCalls).toContainEqual(
    //   expect.objectContaining({ name: 'Read', input: { file_path: '/test' } })
    // )

对于需要更强确定性的场景，测试通过 Mock API 注入固定的 Assistant 响应，绕过模型的非确定性：

    const mockApiResponse = createAssistantMessage({
      content: [
        { type: 'tool_use', id: 'tu_1', name: 'Read',
          input: { file_path: '/tmp/test.txt' } },
      ],
    })
    // 注入 mock → 验证工具执行流程 → 断言结果

## 22.6 端到端测试考量

### 22.5.1 成本与覆盖率的平衡

端到端测试需要真实的 API 调用，每次测试都产生成本。Claude Code 的策略是：

1. 单元测试覆盖所有确定性逻辑
2. 集成测试使用模拟 API 响应覆盖交互逻辑
3. 端到端测试仅覆盖关键用户路径（启动、查询、压缩、恢复）
4. 回归测试仅在发现 Bug 后添加端到端用例

### 22.5.2 快照测试

组件渲染使用快照测试验证 UI 不发生意外变化。终端渲染的快照是 ANSI 字符串，比 HTML 快照更紧凑。

## 本章小结

Claude Code 的测试策略体现了一个核心原则：在确定性层面追求完美覆盖，在非确定性层面追求结构验证。通过将系统分层为确定性/半确定性/非确定性三个层面，团队能够为每个层面选择最适合的测试方法。

数据驱动的测试决策（基于 BQ 生产数据）和防回归测试（基于历史 Bug）确保了测试投资的回报最大化。这不是为了测试覆盖率的数字好看，而是为了系统的实际可靠性。