The agent-loop testkit is test tooling in the DataMind Oda source. It gives a test the standard services an agent loop needs, mounts the production loop, and provides inbox stubs, so tests that target the loop or a durable inbox do not rebuild that wiring. To choose between the test benches, see Testing tiers.
mountAgentLoopTestDependencies mounts the six services an agent loop depends on, in this fixed order:
mountAgentLoopTestHarness mounts the production loop with an empty agent list, so no agents exist until you create them, and returns two helpers:
| Helper | What it does |
|---|---|
create | Creates a production agent with its fresh session; the promise resolves to that agent. |
claim | Admits pending messages through the production loop driver's own claim operation. |
The context owns the loop and every agent created by the testkit. A test sets it up in this order (imports omitted):
const ctx = new Context()
await mountAgentLoopTestDependencies(ctx)
// Mount the model adapter and any load-order-sensitive plugins here.
const helpers = await mountAgentLoopTestHarness(ctx)
const agent = await helpers.create(SessionId('test-agent'))
agent.inbox.append('next-turn', message)
const admitted = helpers.claim(agent, 'next-turn', 1)agent/ inbox/ claimed with the turn that owns it.Two stubs stand in for an inbox when the test does not need real inbox behaviour, and one real inbox comes with the testkit:
createInboxStub implements the public queue operations over two process-local arrays, one per inbox target ('next-turn' and 'next-step'), and never writes to a session:
append and prepend add a message to the back or the front of one queue.replace and remove find a message by its id and report false when it is not there.splice edits one queue in place, and clear empties both queues, the step queue first.unsupportedInbox exposes empty queues and refuses every mutation; the first attempt throws this test Agent does not support Inbox mutations. Use it when the test subject must not touch pending input at all.message "<id>" is already pending. Durable events, projection validation and live inbox notifications all need this real agent; the stubs carry none of them.Model, session store, projection registry, system prompt, tool runtime, then agent registry. The helper stops before the agent loop, so the test controls the loop's load order.
No. The dependency helper mounts no adapter and stops before the loop, so the test decides which adapter runs. Register the one the test exercises after the prerequisites, before the loop starts.
The production loop with an empty agent list. No agent exists until you create one with create, and the context owns it.
The next step's messages first, then one next-turn message when one is requested. The claim runs through the production loop driver's own operation, and each admitted message fires agent/ with the turn that owns it.
It uses the fail-fast stub, unsupportedInbox, which refuses every mutation by design and throws this test Agent does not support Inbox mutations at the first attempt. Use the mutable stub when the test edits pending input, or a real agent from the testkit.
A message with that id is already pending in a real inbox. The refusal names it: message "<id>" is already pending. Use a new message, or edit or remove the pending one first.
No. The mutable stub is process-local and never writes to a session. Durable events, projection validation and live inbox notifications all require a real agent created by the testkit.
The helper call rejects. Services activated before the failure stay owned by the context and unwind when the context is disposed.
The owning context. Dispose it after every test so its agents reach quiescence and their scoped registrations unwind.
No. It provides the loop's prerequisite services and a driver for the real loop; the other benches cover the interface, model traffic and recorded sessions. Testing tiers compares them.