| D-M2-31-1 |
2026-10-24 |
Architecture: the conductor is Go (services/server/test/agents), the Fake-Agent is Python (bothy_sdk.testing.fake_agent), one process per agent and consumer, started with uv run --project sdk/python --locked --quiet python -m bothy_sdk.testing.fake_agent after one uv sync per test binary. The sdkserver of M2-29 runs the other way round (the Python tests drive a Go server process) and is not reused. |
Only the Go side can own the server in the test process with its fake clock, pgtest, the matrix world, the jobs and the checks; 09 16.1 asks for an adapter on the SDK. |
| D-M2-31-2 |
2026-10-24 |
Scenario format: YAML decoded strictly into the Go types of scenario_test.go, checked by Validate; expectations are literal data written out from 09 16.2 and the chapters it names; rows expand the steps of a case (AG-02, AG-12, and the token variants of AG-08); no expectation is computed with server code. |
The plan row demands data-driven scenarios with literal outcomes; the types are the documentation and the check. |
| D-M2-31-3 |
2026-10-24 |
State checks read through AdminDB with one read-only query per check (every row as a JSON object; the key table only kid, purpose, state and times); every human answer is validated against docs/api/openapi.yaml (M1-31) and must hold no JSON null; the jti columns are compared by their hash prefix. |
The suite proves stored state, not only answers, and never prints a token. |
| D-M2-31-4 |
2026-10-24 |
Fake-Agent ops and verdicts as in the README of the suite; the well-behaved default of 09 4.9 finishes a refused run with the matching reason unless skip_own_checks or accept: false; requests of a run carry its token in Invocation-Token (07 SR9) unless bind: false. The SDK's own clock injection (Clock, M2-29) is the hook: ControlledClock takes the harness clock of each request as wall time and adds the real time since the start to the monotonic time, so the JWKS max-age follows the harness clock; no SDK change was needed. |
The SDK binds every request of a run, so a patch after the end of a run is refused by the binding check (403 invocation_token_invalid, run_inactive) before the run document answers 409 conflict run_not_active; AG-14 asserts both. |
| D-M2-31-5 |
2026-10-24 |
Credentials of a test double travel only over its stdin (hello); argv and environment hold none, the reply of export_token is the only reply with a token, stderr is at level WARNING. |
04 2.2 rule 3, SEC-LOG-2, T-TK-05. |
| D-M2-31-6 |
2026-10-24 |
Jobs run only on request: harness.Options.ManualJobs gives River no-ops for runs.queued_expiry, runs.cancel_timeout, runs.stall_check and instance.key_rotation, and Server.RunDueJobs runs the real handlers of the jobs due on the harness clock. |
River schedules by the wall clock while the server follows the fake clock. |
| D-M2-31-7 |
2026-10-24 |
Crash emulation by cursor snapshot: Server.Restart stops the wire graph gracefully and starts it again on the same database; RestoreCursors writes a snapshot of agent_consumer_cursors back between stop and start. |
An in-process server cannot be killed without killing the test; the snapshot leaves the state of a process that died before it stored later acknowledgements (AG-28). |
| D-M2-31-8 |
2026-10-24 |
Every client of a case reaches the server through one fault proxy on 127.0.0.1 port 0, whose origin is server.public_url (harness.Options.PublicURL of M2-47 serves as the brief's PublicOrigin); it keeps that address across a restart, closes open WebSockets when it switches its upstream, and drops the answer of the next request of an operation after the server answered, keeping the answer for the conductor (AG-32). Its upstream client is egress.NewLoopback. |
The DPoP htu is built from server.public_url, so every proof stays valid across a restart; the lost answer of AG-32 needs a proxy. |
| D-M2-31-9 |
2026-10-24 |
Fixtures only for state no operation can write: imported session rows (agentstest.InsertImportedSession, AG-18) and a message of origin import in a thread of an existing channel (messagingtest.InsertImportedMessage, new, AG-20). The import interface of M8-03 writes imported messages only into the channels of its own source. |
The acceptance rule of the brief; the world's channels are no import source. |
| D-M2-31-10 |
2026-10-24 |
REST is the default transport, because an empty poll up to the head that the conductor read from the event log proves that nothing was delivered without a timeout; every expectation none uses REST. The gateway is used in AG-12 (both transports) and AG-23 (command.autocomplete_requested reaches only a live gateway connection). |
Deterministic "nothing arrived" checks. |
| D-M2-31-11 |
2026-10-24 |
AG-02 has 32 rows (three switches in eight combinations, main thread and thread, with and without a mention of the operator), each in its own channel of Support with auto_thread = false, the thread opened by Lotse's own messages. AG-08 "missing" is the empty token (403 invocation_token_invalid, invalid); the absent member is the schema error 400 validation_failed. AG-22 emergency follows 09 4.5 "Keys and rotation" point 3: one delivery held, emergency rotation, the clock past agent.jwks_max_age, the held token unknown_key, then the redelivery with a token of the new key starts. |
09 3.4, 09 4.5; the case of an adapter whose still-valid cache accepted a token of the withdrawn key is a spec gap (first found by M2-06) and not asserted. |
| D-M2-31-12 |
2026-10-24 |
Deferred parts: testdata/deferred.golden lists AG-01 origin=import M8-03, AG-20 attachment M3-07, AG-20 transcript M3-09, AG-25 voice M3-09; the ticket of a line adds the part and removes the line. M8-03 and M3-07 landed before this suite; their parts stay deferred here and are named as a follow-up in the report. |
The brief's list; the import path of M8-03 writes only into its own channels, and the attachment path needs the file pipeline with a scan. |
| D-M2-31-13 |
2026-10-24 |
Red-run proof: each scenario file failed before the conductor and the Fake-Agent supported its steps; TestExpectations_Bite proves that every kind of expectation and every check kind fails on a wrong value with a message that names the step and holds no token; the report lists per AG ID the ticket and test that recorded the red run of the behaviour. |
Plan row M2-31, second acceptance test. |
| D-M2-31-14 |
2026-10-24 |
CI path filter: GATE_COMPONENTS["server"] owns sdk/python/ in addition to services/. |
The suite runs inside the server gates and executes the SDK. |
| D-M2-31-15 |
2026-10-24 |
AG-31 and AG-32 are part of the suite, although the plan row names AG-01 to AG-30. |
Chapter 09 16.2 lists them, and the chapter wins (PLAN 1 point 3). |
| D-M2-31-16 |
2026-10-24 |
harness.Options.ProofsOnClock: the clients of a server on a fake clock take the iat of their proofs from that clock. AG-23 waits for command.autocomplete_timeout in real time with a test value, because the server bounds the wait with its own timer (M2-17); the scenario sets http.request_timeout above it as the start check demands. |
A server whose fake clock moved minutes ahead refuses a proof with the real time (auth.dpop_proof_max_age). |