测试策略¶
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 测试服务:
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 转录。
契约测试级别¶
建议的级别:
- 针对 V2 契约的 Schema 测试。
- 针对硬不变量的纯投影测试。
- Provider 适配器回放测试:从原始转录到 V2 领域事件。
- 完整编排集成测试:从命令经过回放运行时到最终投影。
第四级是最重要的一级。它能够捕获生命周期不匹配,例如子轮次关闭父运行,或检查点被过早捕获。
首批十个集成测试¶
V2 应该从十个左右扎实的测试开始:
simple:发送一条消息会创建一个运行、一个根节点、一个 provider 轮次、一个助手响应和一个根检查点。multi_turn:后续消息会在同一应用线程上创建单调有序的应用运行。message_steering:steering 会附着到当前活动运行的意图上,而不是变成无关的运行。turn_interrupt:中断确认不会完成该运行,直到 provider 终态事件到达。steering_restart_fallback:不支持原生 steering 的 provider 会中断当前活动尝试,并在同一运行下创建一个替代尝试。subagent:子 provider 轮次会创建嵌套的执行节点,并且永远不会完成父运行。subagent_checkpoint:子节点/subagent 节点创建嵌套的检查点作用域,而不会推进应用运行计数。thread_rollback:回滚以检查点作用域为目标,并协调 provider 回滚快照。approval_request:provider 审批回调会变成持久化运行时请求,并通过真实适配器路径得到解决。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 回调参数。
实现顺序¶
测试基础设施应在生产重写之前构建:
- V2 契约 schema。
- Effect 服务定义。
- provider 运行时传输抽象。
- 通用回放运行时。
- 用于应用自有 provider 回放 fixtures 的 Codex、Claude、Cursor、OpenCode 和 ACP 转录加载器。
- 针对核心不变量的投影 reducer 测试。
- 完整的命令到投影集成测试。
- 生产层。
这样可以在构建过程中始终保持架构可执行,并防止测试只验证简化的 mock。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 t3/t3code