Skip to content

agent-bundle/test: mount one state (and notice ledger) across several renderRoute calls — every state-bearing example re-implements mountState #484

Description

@ScriptedAlchemy

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:

  1. 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
  2. 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.
  3. 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 helperwithSharedState(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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttestingFramework test harnesses and integration evidence

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions