跳转至

测试策略

V2 应通过少量高价值的集成测试来验证,而不是通过大量把被测行为 mock 掉的单元测试。

目标不是“永远不使用测试替身”。目标是在真正的进程、网络、时钟、ID 和文件系统边界之外,不引入测试替身。核心编排行为必须真实运行。

测试原则

V2 默认的测试形态是:

command dispatch
  -> real Orchestrator
  -> real ProviderAdapter
  -> replayed ProviderRuntime transport
  -> real adapter normalizer
  -> real V2 event store/sink using production persistence semantics
  -> real V2 projection/projector
  -> real Checkpoint policy
  -> assertions

重放框架替换的是外部 provider 进程或网络流。它不会替换 adapter、normalizer、命令/事件基础设施、投影 reducer/projector、checkpoint 策略或业务逻辑。

重放记录中的原始 provider 帧是真实的传输层证据。在生产环境中,等价的原始帧是带有有界保留策略的日志数据。集成测试仍应使用真实的规范化编排持久化/投影,这样重放输入与实时 provider 输出会走相同的 adapter 和编排路径。

允许的测试替身

允许的替身:

  • provider 运行时传输,由确定性重放记录支撑。
  • Effect 运行时时间,在测试中通过 effect/testing 的 TestClock 控制。
  • Effect Random,通过确定性测试实现提供稳定的 UUID/数字。
  • 临时文件系统/worktree。
  • 临时数据库或使用相同 repository 接口的内存数据库。
  • 仅在直接测试进程故障行为时使用假进程监督器。

集成测试中不允许出现:

  • mock orchestrator。
  • mock provider adapter。
  • mock provider 事件 normalizer。
  • mock 命令/事件基础设施或 V2 event sink。
  • mock projection reducer。
  • mock checkpoint 服务行为。
  • mock provider 能力策略。
  • 使用预规范化的领域事件作为 adapter 测试输入。
  • 自定义 clock/id 服务,复制 Effect 的 Clock、DateTime 或 Random 服务。

纯 reducer 测试仍然有效,但应该少而精。它们应该直接测试投影不变量,而不是取代集成测试覆盖。

生产代码应通过 Effect 运行时 API 读取时间,例如 DateTime.now 和 Clock.currentTimeMillis,而不是通过 Date.now 或临时包装器。生产代码应通过 effect/Random 获取随机值,而不是直接使用 crypto.randomUUID、Math.random 或自定义全局 id 生成器。

测试应提供 Effect 测试服务:

import { TestClock } from "effect/testing";

id 分配器仍然可以暴露领域特定的辅助函数,例如 newRunId 或 newNodeId,但这些辅助函数应基于 Random 实现,以便测试层可以在不 mock 编排逻辑的情况下产生确定性值。

通用重放运行时

重放必须是 provider 中立的。Codex NDJSON fixtures 只是某一个 provider 的记录格式,而不是框架本身。

type ProviderReplayTranscript = {
  provider: ProviderKind;
  protocol: string;
  version: string;
  scenario: string;
  entries: ProviderReplayEntry[];
};

type ProviderReplayEntry =
  | {
      type: "expect_outbound";
      label?: string;
      frame: unknown;
    }
  | {
      type: "emit_inbound";
      label?: string;
      frame: unknown;
      afterMs?: number;
    }
  | {
      type: "runtime_exit";
      status: "success" | "error" | "cancelled";
      error?: unknown;
    };

重放运行时负责确定性的传输语义:

  • 按顺序的入站事件发射。
  • 出站命令断言。
  • 暂停/恢复与时序控制。
  • 运行时退出/错误模拟。
  • 恢复游标/会话恢复。
  • 记录元数据校验。

重放运行时不需要理解 turn、plan、approval、subagent 或 checkpoint 的含义。Provider adapter 负责解释 provider 特定的帧。

恢复测试只在 provider 传输边界使用重放。应用重启的测试方式是拆除最外层 orchestrator/server 层,并基于持久化存储重新创建它。空闲清理和崩溃恢复通过生产生命周期服务(如 session reaper 或 runtime recovery policy)进行测试。测试不得添加仅为了断言而重启会话的 adapter 或 orchestrator 方法。

Provider 记录格式

每个 provider 都可以在通用重放封装内拥有自己的原始帧格式。

示例:

Codex replay transcript
  -> JSON-RPC app-server requests/responses/notifications
  -> consumed by CodexAdapter

Claude replay transcript
  -> Claude Agent SDK query() outbound options and yielded SDKMessage chunks
  -> consumed by ClaudeAdapter

Cursor replay transcript
  -> Cursor Agent SDK open/send calls and ordered onDelta/run results
  -> consumed by CursorAdapter

OpenCode replay transcript
  -> OpenCode SDK requests/responses plus ordered SSE events
  -> consumed by OpenCodeAdapter

ACP replay transcript
  -> logical JSON-RPC requests/responses/notifications over a strict NDJSON peer
  -> consumed by AcpAdapter and a provider flavor (Grok or ACP Registry)

Fixtures 应尽可能保留原始的 provider 证据。期望的 V2 事件或投影是断言,而不是 fixture 输入。

对于 Claude,初始重放边界是 Agent SDK query() 调用返回的异步可迭代对象。记录应包含我们发送的 query prompt/options,然后按 provider 顺序重放原始 SDKMessage 块。这有意让 V2 adapter 针对真实的 Claude SDK 输出进行测试;它不测试 SDK 自身的子进程或传输解析器。

ACP fixtures 针对一个子进程重放对等端运行,该对等端校验每个出站帧并提供记录的入站帧。这仅替换外部 ACP 驱动传输;共享运行时、adapter 规范化、编排、持久化和投影仍然是生产代码。通用 ACP Registry 测试框架会重新定向协议标准的 ACP 记录;provider 扩展记录仍限定在拥有该扩展的 flavor 范围内。

OpenCode fixtures 在 HTTP/SSE 边界上替换 SDK 客户端。它们必须保留请求响应与 SSE 事件之间的竞态,因为用户消息和终态 session.status 事件可能会在对应的 promptAsync 或 abort 响应之前到达。Subagent fixtures 必须同时保留父会话 ID 和子会话 ID,以便仅根会话才有的终态行为仍然可测试。

Provider 转录记录器与服务器编排测试工具包放在一起,而不是放在 provider 客户端包中。使用 bun run record:codex-replay -- --scenario <name> 录制 Codex 应用服务器转录,使用 bun run record:claude-replay -- --scenario <name> 录制 Claude Agent SDK 转录。使用 pnpm --filter t3 record:cursor-replay --scenario <name> 录制 Cursor Agent SDK 转录。

契约测试级别

建议的级别:

  1. 针对 V2 契约的 Schema 测试。
  2. 针对硬不变量的纯投影测试。
  3. Provider 适配器回放测试:从原始转录到 V2 领域事件。
  4. 完整编排集成测试:从命令经过回放运行时到最终投影。

第四级是最重要的一级。它能够捕获生命周期不匹配,例如子轮次关闭父运行,或检查点被过早捕获。

首批十个集成测试

V2 应该从十个左右扎实的测试开始:

  1. simple:发送一条消息会创建一个运行、一个根节点、一个 provider 轮次、一个助手响应和一个根检查点。
  2. multi_turn:后续消息会在同一应用线程上创建单调有序的应用运行。
  3. message_steering:steering 会附着到当前活动运行的意图上,而不是变成无关的运行。
  4. turn_interrupt:中断确认不会完成该运行,直到 provider 终态事件到达。
  5. steering_restart_fallback:不支持原生 steering 的 provider 会中断当前活动尝试,并在同一运行下创建一个替代尝试。
  6. subagent:子 provider 轮次会创建嵌套的执行节点,并且永远不会完成父运行。
  7. subagent_checkpoint:子节点/subagent 节点创建嵌套的检查点作用域,而不会推进应用运行计数。
  8. thread_rollback:回滚以检查点作用域为目标,并协调 provider 回滚快照。
  9. approval_request:provider 审批回调会变成持久化运行时请求,并通过真实适配器路径得到解决。
  10. provider_switch_return:切换到其他 provider 会创建上下文交接;切换回来时,会通过增量交接恢复之前的 provider 线程。

只有在测试保护新的不变量或复现真实故障模式时,才应添加额外的测试。

Fixture 规则

  • Fixtures 是原始 provider 转录,而不是 mock 的领域事件。
  • Fixtures 应包含足够的出站期望,以证明应用发送了正确的 provider 命令。
  • Fixtures 应包含协议元数据、provider 版本、模型、cwd 策略和捕获时间戳。
  • Fixture 回放必须在 Effect TestClock 和确定性 Random 层下保持确定性。
  • 当 provider 转录来自真实运行时,请保持原始 provider 帧的顺序。
  • 脱敏应保留 id、方法名、生命周期顺序和关联结构。
  • Fixture 转录既是测试输入,也是诊断证据。它们并不意味着生产环境会将所有原始 provider 帧存储在 SQLite 中。

断言

断言应优先使用最终投影和持久化的规范化编排事件,而不是实现中的附带调用。

好的断言:

  • 重复的命令分发返回原始回执序列,而不会重放 provider 传输层。
  • 存储事件序列的单调性。
  • 快照序列及序列之后的流式行为。
  • 运行状态和序号。
  • 活动/最终运行尝试。
  • 执行节点的父/子结构。
  • provider 线程与 provider 轮次的关联。
  • 检查点作用域的层级。
  • 待处理/已解决的运行时请求。
  • 交接的覆盖范围和策略。
  • 在相关时,回放转录的帧数和规范化事件数。

弱的断言:

  • 内部函数调用的精确次数。
  • 私有辅助函数的调用顺序。
  • 适配器/运行时边界之下的 mock 回调参数。

实现顺序

测试基础设施应在生产重写之前构建:

  1. V2 契约 schema。
  2. Effect 服务定义。
  3. provider 运行时传输抽象。
  4. 通用回放运行时。
  5. 用于应用自有 provider 回放 fixtures 的 Codex、Claude、Cursor、OpenCode 和 ACP 转录加载器。
  6. 针对核心不变量的投影 reducer 测试。
  7. 完整的命令到投影集成测试。
  8. 生产层。

这样可以在构建过程中始终保持架构可执行,并防止测试只验证简化的 mock。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code