V2 should be validated with a small number of high-value integration tests rather than a large suite of unit tests that mock away the behavior being tested.
The goal is not "no test doubles ever." The goal is that test doubles exist only at true process, network, clock, id, and filesystem boundaries. Core orchestration behavior must run for real.
The default V2 test shape is:
| 1 | command dispatch |
| 2 | -> real Orchestrator |
| 3 | -> real ProviderAdapter |
| 4 | -> replayed ProviderRuntime transport |
| 5 | -> real adapter normalizer |
| 6 | -> real V2 event store/sink using production persistence semantics |
| 7 | -> real V2 projection/projector |
| 8 | -> real Checkpoint policy |
| 9 | -> assertions |
The replay framework replaces the external provider process or network stream. It does not replace the adapter, normalizer, command/event infrastructure, projection reducers/projectors, checkpoint policy, or business logic.
Raw provider frames in replay transcripts are realistic transport evidence. In production, equivalent raw frames are diagnostic log data with bounded retention. Integration tests should still use real normalized orchestration persistence/projections so replay input exercises the same adapter and orchestration paths as live provider output.
Allowed substitutes:
TestClock from effect/testing.Random, provided with a deterministic test implementation for stable UUIDs/numbers.Not allowed in integration tests:
Clock, DateTime, or Random services.Pure reducer tests are still valid, but they should be few and targeted. They should test projection invariants directly, not replace integration coverage.
Production code should read time through Effect runtime APIs, such as DateTime.now and Clock.currentTimeMillis, not through Date.now or ad hoc wrappers. Production code should allocate random values through effect/Random, not through direct crypto.randomUUID, Math.random, or a custom global id generator.
Tests should provide Effect test services:
| 1 | import { TestClock } from "effect/testing"; |
The id allocator can still expose domain-specific helpers such as newRunId or newNodeId, but those helpers should be implemented on top of Random so test layers can produce deterministic values without mocking orchestration logic.
Replay must be provider-neutral. Codex NDJSON fixtures are one provider's transcript format, not the framework itself.
| 1 | type ProviderReplayTranscript = { |
| 2 | provider: ProviderKind; |
| 3 | protocol: string; |
| 4 | version: string; |
| 5 | scenario: string; |
| 6 | entries: ProviderReplayEntry[]; |
| 7 | }; |
| 8 | |
| 9 | type ProviderReplayEntry = |
| 10 | | { |
| 11 | type: "expect_outbound"; |
| 12 | label?: string; |
| 13 | frame: unknown; |
| 14 | } |
| 15 | | { |
| 16 | type: "emit_inbound"; |
| 17 | label?: string; |
| 18 | frame: unknown; |
| 19 | afterMs?: number; |
| 20 | } |
| 21 | | { |
| 22 | type: "runtime_exit"; |
| 23 | status: "success" | "error" | "cancelled"; |
| 24 | error?: unknown; |
| 25 | }; |
The replay runtime owns deterministic transport semantics:
The replay runtime must not know what a turn, plan, approval, subagent, or checkpoint means. Provider adapters interpret provider-specific frames.
Recovery tests use replay only at the provider transport boundary. App restart is tested by tearing down and recreating the outermost orchestrator/server layer against durable persistence. Idle cleanup and crash recovery are tested through production lifecycle services such as the session reaper or runtime recovery policy. Tests must not add adapter or orchestrator methods whose only purpose is to restart sessions for assertions.
Each provider can have its own raw frame format inside the generic replay envelope.
Examples:
| 1 | Codex replay transcript |
| 2 | -> JSON-RPC app-server requests/responses/notifications |
| 3 | -> consumed by CodexAdapter |
| 4 | |
| 5 | Claude replay transcript |
| 6 | -> Claude Agent SDK query() outbound options and yielded SDKMessage chunks |
| 7 | -> consumed by ClaudeAdapter |
| 8 | |
| 9 | Cursor replay transcript |
| 10 | -> Cursor Agent SDK open/send calls and ordered onDelta/run results |
| 11 | -> consumed by CursorAdapter |
| 12 | |
| 13 | OpenCode replay transcript |
| 14 | -> OpenCode SDK requests/responses plus ordered SSE events |
| 15 | -> consumed by OpenCodeAdapter |
| 16 | |
| 17 | ACP replay transcript |
| 18 | -> logical JSON-RPC requests/responses/notifications over a strict NDJSON peer |
| 19 | -> consumed by AcpAdapter and a provider flavor (Grok or ACP Registry) |
Fixtures should preserve raw provider evidence as closely as possible. Expected V2 events or projections are assertions, not fixture input.
For Claude, the initial replay boundary is the async iterable returned by the Agent SDK query()
call. A transcript should include the query prompt/options we sent and then replay the raw
SDKMessage chunks in provider order. This intentionally tests the V2 adapter against real Claude
SDK output; it does not test the SDK's own subprocess or transport parser.
ACP fixtures run against a child-process replay peer that validates every outbound frame and serves recorded inbound frames. This replaces only the external ACP driver transport; the shared runtime, adapter normalization, orchestration, persistence, and projections remain production code. The generic ACP Registry harness retargets protocol-standard ACP transcripts; provider-extension transcripts remain scoped to the flavor that owns the extension.
OpenCode fixtures replace the SDK client at its HTTP/SSE boundary. They must preserve races between
request responses and SSE events, because user messages and terminal session.status events can
arrive before the corresponding promptAsync or abort response. Subagent fixtures must retain
both parent and child session ids so root-only terminal behavior remains testable.
Provider transcript recorders live with the server orchestration testkit, not with provider client
packages. Use bun run record:codex-replay -- --scenario <name> for Codex app-server transcripts
and bun run record:claude-replay -- --scenario <name> for Claude Agent SDK transcripts. Use
pnpm --filter t3 record:cursor-replay --scenario <name> for Cursor Agent SDK transcripts.
Recommended levels:
The fourth level is the most important one. It is the test that catches lifecycle mismatches such as child turns closing parent runs or checkpoints being captured too early.
V2 should start with roughly ten strong tests:
simple: sending one message creates one run, one root node, one provider turn, one assistant response, and one root checkpoint.multi_turn: follow-up messages create monotonically ordered app runs on the same app thread.message_steering: steering attaches to the active run intent instead of becoming an unrelated run.turn_interrupt: interrupt acknowledgement does not complete the run until the provider terminal event arrives.steering_restart_fallback: a provider without native steering interrupts the active attempt and creates a replacement attempt under the same run.subagent: child provider turns create nested execution nodes and never complete the parent run.subagent_checkpoint: child/subagent nodes create nested checkpoint scopes without advancing the app run count.thread_rollback: rollback targets checkpoint scopes and reconciles provider rollback snapshots.approval_request: provider approval callbacks become durable runtime requests and are resolved through the real adapter path.provider_switch_return: switching away from a provider creates a context handoff, and switching back resumes the prior provider thread with a delta handoff.Additional tests should be added only when they protect a new invariant or reproduce a real failure mode.
TestClock and deterministic Random layers.Assertions should prefer final projections and durable normalized orchestration events over incidental implementation calls.
Good assertions:
Weak assertions:
Testing infrastructure should be built before production rewrites:
This keeps the architecture executable while it is being built and prevents tests from validating only simplified mocks.