Skip to content

M2-31 Conformance suite with the Fake-Agent

No Date Ruling Reason
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).