测试策略
V2 应当用少量高价值的集成测试来验证,而不是靠一大套把被测行为模拟掉的单元测试。
目标并不是「永远不许有测试替身」。目标是:测试替身只出现在真正的进程、网络、时钟、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)、规范化器、命令(command)/事件(event)基础设施、投影(projection)的 reducer / projector、检查点(checkpoint)策略或业务逻辑。
重放通信记录中的原始提供方帧是真实的传输证据。在生产环境中,与之对应的原始帧是保留期限有限的诊断日志数据。集成测试仍应使用真实的规范化编排持久化与投影,这样重放输入会走上与实时提供方输出相同的适配器路径和编排路径。
允许的测试替代
允许的替代:
- 由确定性重放通信记录支撑的提供方(provider)运行时传输。
- Effect 运行时时间,在测试中用
TestClock控制,它来自effect/testing。 - Effect 的
Random,配有确定性的测试实现,以得到稳定的 UUID 和数字。 - 临时文件系统 / 工作树(worktree)。
- 使用相同仓库接口的临时数据库或内存数据库。
- 仅在直接测试进程失败行为时使用的伪造进程监管器。
集成测试中不允许:
- 模拟的编排器(orchestrator)。
- 模拟的提供方适配器(adapter)。
- 模拟的提供方事件(event)规范化器。
- 模拟的命令(command)/ 事件基础设施或 V2 事件汇。
- 模拟的投影 reducer。
- 模拟的检查点(checkpoint)服务行为。
- 模拟的提供方能力策略。
- 把预先规范化的领域事件用作适配器测试的输入。
- 另起一套时钟 / 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 之上,这样测试层就能产生确定性的值,而不必去模拟编排逻辑。
通用重放运行时
重放必须提供方(provider)中立。Codex 的 NDJSON 夹具只是某一个提供方的通信记录格式,并不是框架本身。
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;
};重放运行时负责这些确定性的传输语义:
- 按序发出入站事件(event)。
- 出站命令(command)断言。
- 暂停 / 恢复与时序控制。
- 运行时退出 / 错误模拟。
- 恢复游标 / 提供方会话(session)还原。
- 通信记录元数据校验。
重放运行时不得知道回合(turn)、计划、审批、子智能体或检查点(checkpoint)各自意味着什么。提供方适配器(adapter)负责解释提供方专有的帧。
恢复测试只在提供方传输边界使用重放。应用重启的测试方式,是拆掉并重建最外层的编排器(orchestrator)/ 服务器层,让它面对持久化存储。空闲清理和崩溃恢复通过生产环境的生命周期服务来测试,例如提供方会话回收器或运行时恢复策略。测试不得为了断言,给适配器或编排器增加仅仅用来重启会话的方法。
提供方通信记录格式
每个提供方(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)夹具应尽可能原样保留原始提供方证据。预期的 V2 事件(event)或投影(projection)是断言,不是夹具输入。
对 Claude 而言,最初的重放边界是 Agent SDK 的 query() 调用所返回的异步可迭代对象。一份通信记录应包含我们发出的 query 提示词与选项,然后按提供方顺序重放原始的 SDKMessage 分块。这是有意用真实的 Claude SDK 输出去测试 V2 适配器(adapter);它并不测试 SDK 自己的子进程或传输解析器。
ACP 夹具对着一个子进程重放对端运行。该对端会校验每一个出站帧,并送出已录制的入站帧。这里替换的只是外部 ACP 驱动(driver)传输;共享运行时、适配器规范化、编排、持久化和投影仍然是生产代码。通用的 ACP Registry 驾驭程序会把符合协议标准的 ACP 通信记录重新定向;提供方扩展通信记录仍限定在拥有该扩展的变体范围内。
OpenCode 夹具在 HTTP/SSE 边界替换 SDK 客户端(client)。它们必须保留请求响应与 SSE 事件之间的竞态,因为用户消息和终止态的 session.status 事件可能先于对应的 promptAsync 或 abort 响应到达。子智能体夹具必须同时保留父提供方会话(session)与子提供方会话的 id,这样仅根会话才有的终止行为才仍然可测。
提供方通信记录的录制器放在服务器编排测试工具包里,不放在提供方客户端包里。Codex app-server 通信记录使用 bun run record:codex-replay -- --scenario <name>,Claude Agent SDK 通信记录使用 bun run record:claude-replay -- --scenario <name>。Cursor Agent SDK 通信记录使用 pnpm --filter t3 record:cursor-replay --scenario <name>。
契约测试层级
建议的层级:
- V2 契约的 Schema 测试。
- 针对硬性不变量的纯投影(projection)测试。
- 从原始通信记录到 V2 领域事件(event)的提供方(provider)适配器(adapter)重放测试。
- 从命令(command)经重放运行时直到最终投影的完整编排集成测试。
第四层最为重要。正是这一层测试能抓住生命周期错位,例如子回合(turn)关掉了父运行,或检查点(checkpoint)捕取得过早。
首批十项集成测试
V2 一开始应当具备大约十项扎实的测试:
simple:发送一条消息,会创建一次运行、一个根节点、一个提供方(provider)回合(turn)、一条助手回复,以及一个根检查点(checkpoint)。multi_turn:后续消息在同一个应用对话线程(thread)上创建单调有序的应用运行。message_steering:引导(steering)附着在活动运行的意图上,而不会变成一次无关的运行。turn_interrupt:中断确认并不会完成这次运行,直到提供方的终止事件到达。steering_restart_fallback:不具备原生引导能力的提供方会中断当前尝试,并在同一次运行下创建一次替代尝试。subagent:子提供方回合会创建嵌套的执行节点,并且永远不会完成父运行。subagent_checkpoint:子节点 / 子智能体节点会创建嵌套的检查点作用域,且不会推进应用运行计数。thread_rollback:回滚以检查点作用域为目标,并调和提供方的回滚快照。approval_request:提供方的审批回调会变成持久化的运行时请求,并经由真实的适配器(adapter)路径得到解决。provider_switch_return:从某个提供方切换离开会创建一次上下文交接,切换回来时则以增量交接恢复先前的提供方对话线程。
只有在保护一项新的不变量,或复现一种真实的失败模式时,才应再增加测试。
夹具规则
- 夹具是原始的提供方(provider)通信记录,不是被模拟的领域事件(event)。
- 夹具应包含足够的出站预期,以证明应用发出了正确的提供方命令(command)。
- 夹具应包含协议元数据、提供方版本、模型、工作目录(cwd)策略和捕获时间戳。
- 在 Effect 的
TestClock与确定性的Random层下,夹具重放必须是确定的。 - 提供方通信记录若由一次真实运行生成,应保留原始的提供方帧顺序。
- 脱敏时应保留 id、方法名、生命周期顺序和关联结构。
- 夹具通信记录是测试输入,也是诊断证据。它们并不表示生产环境会把全部原始提供方帧存进 SQLite。
断言
断言应优先检查最终投影(projection)和持久化的规范化编排事件(event),而不是附带的实现调用。
好的断言:
- 重复分发命令(command)时返回原来的命令回执序列,并且不重放提供方传输。
- 已存储事件的序号单调性。
- 快照序号,以及从该序号之后继续流式传输的行为。
- 运行状态与序号。
- 活动中的运行尝试与最终运行尝试。
- 执行节点的父子结构。
- 提供方对话线程(thread)与提供方回合(turn)的关联。
- 检查点(checkpoint)作用域层级。
- 待处理的运行时请求与已解决的运行时请求。
- 交接的覆盖范围与策略。
- 在相关时,重放通信记录的帧数与规范化事件数。
弱断言:
- 内部函数的确切调用次数。
- 私有辅助函数的调用顺序。
- 适配器(adapter)/ 运行时边界之下被模拟回调的参数。
实现顺序
测试基础设施应在重写生产代码之前建好:
- V2 契约 Schema。
- Effect 服务定义。
- 提供方(provider)运行时传输抽象。
- 通用重放运行时。
- 面向应用自有提供方重放夹具的 Codex、Claude、Cursor、OpenCode 和 ACP 通信记录加载器。
- 针对核心不变量的投影(projection)reducer 测试。
- 从命令(command)到投影的完整集成测试。
- 生产层。
这样,架构在搭建过程中始终可以执行,也避免测试仅仅去验证简化后的模拟对象。