Problem
renderRoute (agent-bundle/test) mounts one fresh state owner per render (packages/agent-bundle/src/test/render.ts:592-640, mountState: mkdtemp + sqlite driver for workspace-durable, memory driver otherwise, createGeneratedRuntimeState, requestBindings, closed after the render). That is the right default for isolated route-unit tests, but every example whose routes accumulate state across events has to prove a multi-render journey — record on event 1, read on event 2 — and the harness gives them no way to keep one mounted state across renders. Each example therefore re-implements the harness's own mountState in test code, in a different shape, with @agent-bundle/runtime/state* and @agent-bundle/runtime/mount imports that the harness was meant to hide.
Evidence
Three independent re-implementations on main:
examples/worktree-proximity/tests/route-unit/routes.test.ts
:8-11 imports createGeneratedRuntimeState (@agent-bundle/runtime/mount) and createSqliteStateDriver (@agent-bundle/runtime/state/sqlite)
:183-186 beforeEach: mkdtemp + createGeneratedRuntimeState({ definition: topologyStateDefinition, driver: createSqliteStateDriver({ root }) }); :192 afterEach close + rm
:71-91 per render: runtimeState.requestBindings() → context: { noticeLedger: bindings.noticeLedger, state: bindings.state } → bindings.close() in finally
:236-246, :275-283, :315-320, :343-353, :376-390 five more requestBindings()/close() pairs just to read the snapshot after a journey
examples/host-test/tests/route-unit/routes.test.ts — the same shape, byte-for-byte in structure: :8-11 imports, :74-79 beforeEach mount, :86 close, :50-63 per-render bindings.
examples/audiobook-curator/tests/route-unit/state.test.ts — a third shape: :7-8 imports createAgentStateHandle, createMemoryStateDriver, defineState; :47-54 re-wraps the app's definition with defineState({ ...shelfStateDefinition, id: 'audiobook-curator/test-shelf', lifetime: 'process' }), opens a memory driver, wraps the store with createAgentStateHandle, and passes the same state to two renderRoute calls (:60, :75). This variant mounts no notice ledger, so it works only because the routes it renders never touch notices.
The harness already contains the exact code all three copy (render.ts:600-640); it just closes it at the end of one render.
Proposed shape
Either of these (or both; the first is the smaller change):
A. A renderRoute option that reuses a mounted state.
import { mountTestState, renderRoute } from 'agent-bundle/test';
const state = await mountTestState(); // manifest state + notice ledger, same driver rules as mountState
try {
await renderRoute('event:session/start', { context: { ...state.context(), host, workspace }, input });
await renderRoute('event:tool/before', { context: { ...state.context(), host, workspace }, input });
const snapshot = await state.read(); // typed by the manifest state definition
} finally {
await state.close();
}
mountTestState(options?: { lifetime?: 'process' | 'workspace-durable'; driver?: AgentStateDriver }) returns { context(): Pick<RenderRouteContext, 'state' | 'noticeLedger'>; read(); notices(); close() }, honours manifest.state.lifetime by default, and reuses mountState's disposable-sqlite / memory choice. renderRoute already respects a caller-supplied state/noticeLedger (render.ts:627-628), so no render-path change is needed.
B. A scoped helper — withSharedState(async (state) => { ... }) — that does the try/finally for the caller.
Either way the examples drop their @agent-bundle/runtime/mount and /state/sqlite imports and the per-test mkdtemp/rm.
Acceptance
agent-bundle/test exports a documented helper that mounts the manifest's state definition and notice ledger once and hands the same state/noticeLedger handles to any number of renderRoute / renderRouteEvents calls, with a typed snapshot read and a single close().
workspace-durable definitions use a disposable sqlite root that the helper removes on close(); other lifetimes use the memory driver (same rules as mountState).
examples/worktree-proximity/tests/route-unit/routes.test.ts, examples/host-test/tests/route-unit/routes.test.ts, and examples/audiobook-curator/tests/route-unit/state.test.ts are rewritten on top of it and no longer import @agent-bundle/runtime/mount or @agent-bundle/runtime/state/sqlite.
website/docs/en/guide/development/testing.mdx (and the zh twin) document the multi-render journey pattern.
Found while auditing the examples against the public surface in #473.
Problem
renderRoute(agent-bundle/test) mounts one fresh state owner per render (packages/agent-bundle/src/test/render.ts:592-640,mountState:mkdtemp+ sqlite driver forworkspace-durable, memory driver otherwise,createGeneratedRuntimeState,requestBindings, closed after the render). That is the right default for isolated route-unit tests, but every example whose routes accumulate state across events has to prove a multi-render journey — record on event 1, read on event 2 — and the harness gives them no way to keep one mounted state across renders. Each example therefore re-implements the harness's ownmountStatein test code, in a different shape, with@agent-bundle/runtime/state*and@agent-bundle/runtime/mountimports that the harness was meant to hide.Evidence
Three independent re-implementations on
main:examples/worktree-proximity/tests/route-unit/routes.test.ts:8-11importscreateGeneratedRuntimeState(@agent-bundle/runtime/mount) andcreateSqliteStateDriver(@agent-bundle/runtime/state/sqlite):183-186beforeEach:mkdtemp+createGeneratedRuntimeState({ definition: topologyStateDefinition, driver: createSqliteStateDriver({ root }) });:192afterEachclose +rm:71-91per render:runtimeState.requestBindings()→context: { noticeLedger: bindings.noticeLedger, state: bindings.state }→bindings.close()infinally:236-246, :275-283, :315-320, :343-353, :376-390five morerequestBindings()/close()pairs just to read the snapshot after a journeyexamples/host-test/tests/route-unit/routes.test.ts— the same shape, byte-for-byte in structure::8-11imports,:74-79beforeEachmount,:86close,:50-63per-render bindings.examples/audiobook-curator/tests/route-unit/state.test.ts— a third shape::7-8importscreateAgentStateHandle,createMemoryStateDriver,defineState;:47-54re-wraps the app's definition withdefineState({ ...shelfStateDefinition, id: 'audiobook-curator/test-shelf', lifetime: 'process' }), opens a memory driver, wraps the store withcreateAgentStateHandle, and passes the samestateto tworenderRoutecalls (:60,:75). This variant mounts no notice ledger, so it works only because the routes it renders never touchnotices.The harness already contains the exact code all three copy (
render.ts:600-640); it just closes it at the end of one render.Proposed shape
Either of these (or both; the first is the smaller change):
A. A
renderRouteoption that reuses a mounted state.mountTestState(options?: { lifetime?: 'process' | 'workspace-durable'; driver?: AgentStateDriver })returns{ context(): Pick<RenderRouteContext, 'state' | 'noticeLedger'>; read(); notices(); close() }, honoursmanifest.state.lifetimeby default, and reusesmountState's disposable-sqlite / memory choice.renderRoutealready respects a caller-suppliedstate/noticeLedger(render.ts:627-628), so no render-path change is needed.B. A scoped helper —
withSharedState(async (state) => { ... })— that does the try/finally for the caller.Either way the examples drop their
@agent-bundle/runtime/mountand/state/sqliteimports and the per-testmkdtemp/rm.Acceptance
agent-bundle/testexports a documented helper that mounts the manifest's state definition and notice ledger once and hands the samestate/noticeLedgerhandles to any number ofrenderRoute/renderRouteEventscalls, with a typed snapshot read and a singleclose().workspace-durabledefinitions use a disposable sqlite root that the helper removes onclose(); other lifetimes use the memory driver (same rules asmountState).examples/worktree-proximity/tests/route-unit/routes.test.ts,examples/host-test/tests/route-unit/routes.test.ts, andexamples/audiobook-curator/tests/route-unit/state.test.tsare rewritten on top of it and no longer import@agent-bundle/runtime/mountor@agent-bundle/runtime/state/sqlite.website/docs/en/guide/development/testing.mdx(and thezhtwin) document the multi-render journey pattern.Found while auditing the examples against the public surface in #473.