How bide is verified
bide makes a small number of strong promises: a side effect runs at most once, even across crashes and competing drivers; a resumed run continues exactly where it stopped; and the journal is a complete, tamper-evident record of what happened. This page describes the discipline that keeps those promises true as the code changes. The individual test suites are described in Testing; the promises themselves are stated precisely in the Guarantee and bounded in Known limitations.
The rule: no fix without a failing test
Every bug fix follows the same three steps, and the pull request shows all three.
- Reproduce it. A suspicion from reading code is not a bug yet. It becomes one when a test fails because of it, and the pull request quotes that failure: for example,
charged 2 times, want 1, orthe losing driver was told it won. - Fix it, and show the same test passing.
- Mutation-check the fix. Disable each part of the fix in turn and confirm a test fails. A part whose removal no test notices is either unnecessary, and removed, or untested, and gets a test. This is how a fix that passes by accident is caught.
A test that can fail only by chance is not accepted. When a bug depends on timing, the test forces the interleaving instead of hoping for it (see below), so it fails every time on the unfixed code.
Adversarial audits
Beyond fixing what turns up, each subsystem is audited on purpose: the SQL stores, the plan runtime, halts and approvals and sagas, streaming, middleware, sessions, and concurrency throughout. An audit looks for ways to break a guarantee, and every finding goes through the rule above before anything changes. Findings that cannot be reproduced are recorded as suspicions, not fixed.
Techniques
Crash injection. Test stores fail at a chosen write, after the step's side effect has run and before its record commits, which is the window at-most-once exists for. The deterministic simulation tests (agent/dst_test.go, agent/saga_dst_test.go, agent/mofn_dst_test.go) sweep the crash point across every write of a run and check that each resume either matches the clean run or halts; a side effect never runs twice. The reference-model tests (agent/refmodel_test.go) go further: random scenarios of parallel calls, sub-agents, approvals, failures and sagas, under random crash schedules, must settle at exactly the outcome, side effects, compensations and model-visible conversation that an independent crash-free interpreter of the same scenario computes. The chaos benchmark applies the same injection to bide and to other SDKs.
Cancellation sweeps. A context that reports itself cancelled after a chosen number of checks lands a cancellation at every point of an operation. This is how a store that told a losing driver it had won was found and pinned.
Forced interleavings. Races are made deterministic rather than left to the scheduler: a rendezvous that holds two writers until both arrive, a model that blocks until a sibling is mid-call, a server that keeps streaming until the client leaves. Each fails on the unfixed code on every run.
Overlapping drivers. Two drivers of one run, in one process or two, race for the same step. The exclusive attempt claim must let exactly one of them run a side effect, whatever their leases say. A multi-process harness on Postgres (store/postgres/ha_multiproc_test.go) runs worker processes through agent.RecoverLoop against one database while the test kills, stalls and restarts them, and the database counts how often each side effect fired.
Cross-process end to end. examples/approval and examples/plan build real binaries, kill and resume them across processes, and verify the resulting evidence with the bide-audit CLI, including tampered and incomplete evidence that must be rejected.
Model checking. The claim protocol is also modelled in TLA+ (spec/tla) and checked with TLC: every interleaving of two drivers, in one process or two, with every placement of up to two ambiguous store replies (an error that did or did not commit, and, in separate configurations, an error that commits later), a crash and a cancellation. The invariants are the guarantees: at most one fire per call, no not-started record for a claim that fired, a recorded result never replaced, a resolution never overriding a live driver; a liveness property states that an effect that provably never started does not halt for ever. Each rule an earlier review found wrong is kept as a configuration that must still produce its counterexample, so the model cannot quietly lose the power to find it. Nightly, the Apalache model checker also checks an inductive invariant of the claim model, which proves, for two drivers over two processes, AtMostOnce and NotStartedExclusive on one call (attempts 0..3, 8 claim ids) and all four of AtMostOnce, NotStartedExclusive, NoLiveOverride and AtMostOncePerIntent with halt resolution and the caller's second call (attempts 0..3, 6 claim ids), without the approval gate, and, under the lease check, assuming no plain run holds the live attempt at the check (PlainRunIdleAtCheck), at any depth and for any number and mix of faults within the run's 8 (6) claim ids and attempts 0..3 (claim ids are never reused, so this bounds the number of claims). Further models cover the approval gate with 1-of-1 and m-of-n tallies and approvers' key sets (model 1b), flow semantics (model 7), spend accounting of model calls (model 8) and the bide protocol's claim rules (model 2).
Conformance suites. A port is held to its contract by a reusable suite that any implementation, bide's or yours, can run:
agent/storetestchecks a store (agent.Store) against every requirement the journal builds on (A1 to A8: a single winner among 64 goroutines racing one name through three handles, and a side effect behind a Step that runs exactly once under that race; reads that are always a prefix of the run's commit order; byte fidelity; context; iterators that hold nothing across a yield), the journal format header (first in every run; one header under concurrent first writers; a run in another format, or with no header, refused on reads and writes; a read racing a run's first write never sees a headerless record), in-flight steps shared by Journals over one store, the reuse of a claim whose insert failed but committed, and record fidelity: the record returned on the live path is exactly the record a replay reads back, in the journal's canonical form, for content whose encoding is easy to get wrong (HTML-significant characters, U+2028, NUL, invalid UTF-8, unusual number forms, key order), with a fresh salt on every record, and journaled even when the caller's context was cancelled while the step ran.MemStore,store/sqlite, andstore/postgresrun it; see extension points.storetest.CheckWrapperchecks a store wrapper's use ofUnwrap, and, given two contexts that differ in what the wrapper reads from a context, that its keys do not depend on the context.govern/eventlogtestchecks a governed event log: dense, unique positions under concurrent appends from separate handles, and appends idempotent by id, so a repeated append (a retry, even concurrent with the original) is recorded once.model/modeltestchecks a model adapter: an abandoned stream releases its response, and a response that ends before the turn finishes is an error, not an answer. ItsReadSSEandCheckSSEPrefixhold an adapter's SSE reader to oneFinish, sent last, and to never turning a response cut short into a different complete answer.CheckFinishchecks theFinishcarries a neutral finish reason, andToolNameschecks the adapter's tool-name rule.- RFC 6962 reference vectors check the Merkle tree and proofs (Pillar 4).
Fuzzing. The parsers and verifiers that read untrusted bytes have Go fuzz targets, each checking a property, not only the absence of panics:
agent:FuzzRecordRoundTrip,FuzzDecodeRecordandFuzzEncodeRecord_FixedPoint(a journal record decodes back to what was encoded, and re-encoding is a fixed point).model/provider:FuzzSSEScanner(the shared SSE framing).model/anthropic,model/openai,model/gemini:FuzzStreamSSE(oneFinish, last; every failure anErrModel; a response cut at any byte never succeeds with a different message).audit:FuzzUnmarshalStrict(an accepted proof file reads exactly as it decodes, and agrees withencoding/json),FuzzArtifactVerify(only a genuine artifact verifies),FuzzInclusionProof,FuzzConsistencyProof,FuzzInclusionRaw,FuzzConsistencyRaw(a mutated proof is rejected, andauditagrees with the standaloneaudit/verify).plan:FuzzLoadConfig(a config that loads validates, has a stable digest, and runs).codec/gcf(its own module):FuzzEncodeToolResult(GCF output reads back as exactly the tool's JSON value).
Each target's seed corpus holds the inputs of the bugs fuzzing has found, so go test ./..., which runs the seeds only, keeps them fixed in CI. To fuzz one target:
go test -run '^$' -fuzz=FuzzStreamSSE -fuzztime=5m ./model/openaiRace detection and stress. The CI test run on Linux uses -race; macOS and Windows run the plain suite, which also runs the scale tests at full size (they run smaller under -race). The concurrency-heavy packages are also run repeatedly under -race -count=N -cpu=1,2,8, so scheduling differences get many chances to surface.
Continuous integration
Every pull request must pass, before it can merge:
- Lint:
gofmt,go vetandgovulncheckacross every module, including the example modules;doccheck(internal/tools/doccheck), which requires a doc comment on every exported identifier and a package comment on every package; checks that every module is built, tested and ingo.work, and is classified as published or repo-only for releases; and the docs snippet check (internal/tools/docsnip): every Go block in the README anddocs/compiles against the current code, and every API listing matches the package's declarations. The snippet check also runs on documentation-only changes. - Tests on Linux, macOS, and Windows, with
-raceon Linux. On Linux the core,governandintegrationmodules are also tested withGOEXPERIMENT=nojsonv2, so the journal encoding does not depend onencoding/json/v2. The Linux tests run in parallel jobs (the core'sagentpackage, the rest of the core, the other modules, and thenojsonv2run), and the required Test (ubuntu-latest) check fails unless each of them succeeded. Every module, and every package of the split core, is in exactly one of theagent,coreandrestshards, or CI fails; thenojsonv2job runs the core,governandintegrationagain, as before. - Integration against real Postgres 16 and Redis 7. Each suite runs twice against the same services, so a test that passes only on a fresh database fails, and a skipped test fails the job, since a skip would mean nothing was tested.
- DCO sign-off on every commit.
- Models: the TLA+ models under
spec/tla/are checked with TLC (.github/workflows/models.yml): the committed PlusCal translation must be current, every configuration must pass, and every regression configuration must still fail with its named property. Larger bounds run nightly. The Lint job keeps the models and the code in step: the code they describe is wrapped in region markers, andinternal/tools/modelsyncfails a change to a marked region that changes no model (unless the pull request says why with aProtocol-Impactline) and any action name the markers, the model-to-code maps and the specs disagree on;TestProtocolVocabularychecks the claim code's journal keys against the record kinds the claim model declares (see keeping the code and the models in step).
Pull requests merge through a merge queue, which runs the required checks again on the change combined with main and any changes queued ahead of it, so every merge is tested against the code it lands on. A pull request that changes only documentation (or only the models under spec/tla/) skips the Go lint, build and tests; the required checks still report, so it can merge.
Three jobs run nightly and on demand (workflow_dispatch), not on pull requests, and are not required checks:
- Models (nightly) checks the
nightlyTLA+ configurations, with larger bounds (.github/workflows/models.yml). - Apalache (nightly) runs the Apalache checks (
spec/tla/check.sh apalache): the claim model's inductive invariant, a bounded symbolic regression check, TLC checks that the invariant holds in every reachable state of three configurations, and a type check of model 9's wrapper (.github/workflows/models.yml). - Explore (full bound) runs the fault-schedule explorations of the claim protocol (
agent) and of flow lowering (plan) withBIDE_EXPLORE=1(.github/workflows/explore.yml). Every pull request runs them at a smaller default bound, under-raceon Linux; the full bound explores three faulted drives, every process plan, and two preemptions with more faults in the concurrent explorers, which takes minutes, so it runs without-race. The job uploads the test log and the schedule signatures (BIDE_EXPLORE_SIGS, one line per explored schedule) as an artifact kept 30 days (about 170 MB a night), and its summary gives the schedule count per test and a digest of the signatures, so coverage can be compared between runs.
What this does not prove
- Mutation checks are per fix, not exhaustive. Each fix is checked by hand against its own mutants; the codebase as a whole is not put through automated mutation testing.
- CI runs fuzz seeds, not fuzzing. New inputs are searched for when someone runs the fuzzer; CI replays the seed corpus.
- Tests cover the scenarios they model. The deterministic sweeps and forced interleavings cover the windows each guarantee depends on, but a scenario nobody has modelled is not covered until someone does.
- Model checking is bounded, and checks the design, not the code. TLC explores every interleaving within the bounds each configuration states (drivers, faults, attempts); a bug that needs more is outside it. The inductive invariant Apalache checks for the claim model lifts the depth bound for its properties, not the number of drivers, processes, calls, attempts or claim ids, and the id pool bounds the number of claims a covered run can make. Until trace validation lands, nothing checks mechanically that the Go code implements the model; the map from model steps to Go functions in
spec/tla/README.mdis reviewed by hand. - Model behaviour is measured, not proven. Whether a model decides well is evaluated statistically with
eval, and an eval pass rate is not one of the guarantees above (see Evaluation).
Reproduce it yourself
export GOWORK=off
go test -race ./... # core module
(cd store/sqlite && go test -race ./...) # any other module the same way
(cd store/postgres && PG_DSN='postgres://user:pass@localhost:5432/db?sslmode=disable' \
go test -race -count=2 ./...) # integration, against your Postgres
go test -race -count=20 -cpu=1,2,8 ./agent ./plan # stress
BIDE_EXPLORE=1 go test -count=1 -timeout 85m -run Explore ./agent ./plan # full-bound explorations
go test -run '^$' -fuzz=FuzzUnmarshalStrict -fuzztime=5m ./audit # fuzz one targetA report that comes with a failing test is the fastest kind to fix; see CONTRIBUTING.