This document is the source-backed testing doctrine for Minimmit. It preserves
research lessons that are useful for the project without turning AGENTS.md,
skills, or assurance ledgers into long research notes.
crates/core/assurance.yaml remains the claim and evidence ledger. It should
not carry testing roadmaps, source surveys, or confidence essays.
The preferred Minimmit testing loop is:
paper claim -> explicit input -> core step -> semantic effect -> trace/replay -> evidence
Use the smallest deterministic test that gives useful confidence. For current
minimmit-core work, this usually means pure Rust tests over typed protocol
values. Once the deterministic core exposes an input boundary, protocol
scenario tests should drive that boundary directly.
When a core output depends on shell work becoming durable before release, tests should assert the protocol-visible ready outputs and the shell ordering contract. Add a core-visible shell completion input only when completion must change later core behavior. See Core And Shell Boundary for the core/shell boundary.
When sync, store, simulation, or shell work models overload, tests should assert bounded degradation through deterministic observations. See Bounded Degradation for overload vocabulary and testing implications.
Each test should protect behavior that Minimmit owns: protocol obligations, project policy, validation, state transitions, or observable semantic output. Do not add, move, or preserve tests whose failure would primarily report a change in language, library, dependency, derived, or otherwise mechanical behavior rather than a change in Minimmit behavior.
Tests should read as concrete protocol stories. Keep the relevant setup, transition, and assertion visible when they are short.
Structure non-trivial input-step and scenario tests as Given / When /
Then, the behavior-focused form of Arrange / Act / Assert. Use section
comments when the phases are not obvious from blank lines alone. Given should
set up concrete protocol state, When should drive the event or transition
under test, and Then should assert semantic output or selected post-state.
Use helpers for meaningful domain fixtures that would otherwise distract from
the behavior under test. Do not wrap scalar construction or simple assertions
in test-only helpers; use direct public constructors such as BlockId::new,
ViewNumber::new, ValidatorId::new, and TransactionId::new at the call
site.
Names should spell out the protocol relation that matters. Avoid vague relative or policy-heavy names when a simpler name can identify membership, view, parent, signer, persistence, or ordering relationships directly. Literal expected values should be understandable from the test name, local variables, or a short intent comment; unexplained lists of numbers are not useful evidence.
Comments in tests should explain protocol intent, fixture relationships, deliberate ordering, or regression risk that names and assertions do not make obvious. Prefer clearer names or structure first, and do not add comments that only restate assertions or obvious control flow.
Protocol PRs that change processor state, events, validation, or Ready
outputs should name the protocol state facts they touch before implementation.
For each fact, include a small state-fact transition matrix in the test,
nearby protocol documentation, or PR text:
- fact name in protocol vocabulary
- paper claim or explicit evidence gap
- events that establish the fact
- events that must not establish the fact
- later events that depend on the fact
- event that resets the fact, or the explicit reset evidence gap
- executable evidence that covers the cross-event path
Name facts by their protocol meaning, not by private storage. For example,
proposed_current_view is an implementation detail for the fact that the local
leader's current-view proposal slot is consumed. Matrix evidence should drive
real Event -> Processor -> Ready transitions and assert public semantic
effects such as ready output, observed artifacts, view, or selected post-state.
- Small claim tests are the default. They cover thresholds, typed construction, duplicate rejection, distinct sender counting, deterministic ordering, and precise errors.
- Input-step tests should drive one explicit protocol event through the core and inspect the returned protocol-visible output.
- Scenario tests should feed a short ordered input trace, record step outcomes, and assert a named protocol story.
- Overload scenario tests should assert public deterministic observations such as admission, rejection, expiry, queue age, or tracked-state pressure, not private queue internals.
- Replay fixtures should be added only after scenario shape stabilizes. A replayed failure must include enough metadata to reproduce it exactly.
- Model conformance should map named model actions into Rust events and compare a small semantic projection, not every transient implementation field.
- Bounded event search should come after local scenario tests are useful. Start with seeded shuffle or small exhaustive event bags before richer scheduling.
Only input-step and scenario testing are active Codex skill guidance for now. Replay fixtures, property packs, model conformance, and bounded search should get separate skills when the implementation reaches those layers.
Use two property classes:
always: safety properties. A violation is a candidate bug when the property is high-level, protocol-meaningful, and independently confirmable.sometimes: reachability or coverage signals. Reaching one shows the harness exercised useful protocol territory, but does not prove correctness.
Good always properties for Minimmit should be stated over protocol facts such
as notarization, nullification, finalization, view progression, signer
membership, and distinct sender counting. Avoid primary bug oracles based on
private buffers, queue depths, scheduler internals, or implementation-specific
interpretation.
Good sometimes properties include observing an M-notarization,
L-notarization, nullification, higher view, non-genesis finalization, timeout
path, or rejected conflicting evidence.
When the implementation reaches property-pack testing, keep the first pack
small, high-level, and confirmable. always properties should be the only bug
threshold. sometimes properties should stay coverage signals.
Any randomized, scheduled, or searched run that reports a candidate must record:
- scenario family or test name
- ordered input trace, or enough scheduler data to reconstruct it
- seed when randomness is used
- scheduler name and version when scheduling is used
- triggered
alwaysproperties - reached
sometimesproperties - implementation commit when the result leaves a local test failure report
Unreplayable failures are not acceptable evidence.
Start replay and search work with short explicit traces. Move next to seeded
shuffle or small exhaustive event bags. Richer scheduling, broad simulation,
chaos, Antithesis integration, Jepsen-style external testing, and VOPR-style
infrastructure should wait until local Event -> Processor -> Ready
scenarios prove the need.
For Quint or model conformance, map named model actions into Rust events at a protocol-local boundary. Compare semantic projected state, not every transient implementation field. Do not count model-only checks as Rust implementation evidence unless the trace drives Rust behavior.
- Keep
minimmit-coredeterministic and free of hidden IO, wall-clock time, randomness, async scheduling, networking, storage engines, and production shell behavior. - Represent shell ordering through explicit ready outputs. Add core-visible shell completion input only when completion must change later core behavior.
- Assert persist-before-broadcast behavior through protocol-visible outputs, not DB mechanics or shell queues.
- Assert bounded degradation through explicit observations when overload policy belongs to the crate under test.
- Prefer behavior tests through public APIs and protocol-facing outputs.
- Keep tests readable and actionable from the test name plus assertion output.
- Remove mechanical coverage instead of relocating it when cleanup scope permits.
- Let
crates/core/assurance.yamldrive protocol test priority. - Add or update a state-fact transition matrix when protocol state, events, validation, or ready outputs change.
- Treat model-only checks as design evidence unless they are connected to Rust behavior.
- Use explicit evidence gaps rather than implying coverage from partial tests.
Do not add these until the local Event -> Processor -> Ready shape and
scenario tests justify them:
- broad simulation or chaos framework
- Antithesis integration
- Jepsen-style external system testing
- full TigerBeetle VOPR-style infrastructure
- generic deterministic runtime
- coverage gates or flake dashboards
- broad property-test or fuzz dependencies in
minimmit-core
Google testing:
- Software Engineering at Google, Testing Overview: small deterministic tests by default, risk-driven investment, and a bias toward fast actionable feedback.
- Unit Testing: test behavior through public APIs and keep tests maintainable.
- Test Doubles: prefer realistic behavior and avoid brittle interaction tests when state tests will do.
- Larger Testing: larger tests are useful but expensive, so reserve them for risks small tests cannot cover.
- Continuous Integration: preserve signal quality and keep failures actionable.
- Just Say No to More End-to-End Tests: broad end-to-end tests should not be the primary correctness strategy.
- Flaky Tests at Google: flaky tests damage trust in the suite.
- Test Failures Should Be Actionable: a failing test should point directly at the broken behavior.
Private protocol-event research:
- protocol-event-lab README: protocol-visible events, quiescence, semantic properties, and replayable traces are the useful middle layer between pure models and whole-system fault platforms.
- protocol-event-lab thesis: schedule protocol-visible events rather than low-level runtime tasks.
- protocol-event-lab integration challenges: adapters should expose narrow event injection, quiescence, state snapshots, and invariant evaluation.
- protocol-event-lab Antithesis-style properties: keep first property packs small, high-level, and confirmable.
- proto-core-lab Minimmit scope: start with a small synchronous core and import only deterministic harness ideas, not a generic framework.
- proto-core-lab protocol-core testing direction: test through explicit events, explicit transitions, and semantic effects.
- proto-core-lab composed submachines: split complex protocol cores into deterministic submachines with typed internal event flow only when the concern is real.
- proto-core-lab explicit output lifecycle: represent shell work completion explicitly instead of hiding output lifecycle state inside the core.
- proto-core-lab deterministic cores and shells: keep core semantics deterministic while shells execute storage and network outputs.
- proto-core-lab Quint boundary: keep protocol-specific Quint replay mappings local until more than one protocol proves the abstraction.
Model-based and system testing:
- Quint model-based testing: distinguish model validation from implementation conformance, and use traces to connect the two.
- Informal Systems trace generation: generated model traces can seed implementation tests when mapped through a clear boundary.
- Antithesis properties:
alwaysandsometimesproperties are a useful vocabulary for safety and reachability. - Antithesis deterministic simulation testing: deterministic replay matters more than one-off fault discovery.
- TigerBeetle VOPR: record enough seed, scheduler, and trace metadata to reproduce simulation failures exactly.
- TigerBeetle simulation testing for liveness: separate safety exploration from liveness scenarios that heal a correct core.
- FoundationDB testing: deterministic simulation and fault injection are powerful after the system has a suitable controlled execution boundary.
- Stateright: bounded model checking is useful for small protocol models and state spaces.
- Jepsen: test named correctness claims through histories and checkers, not vague system behavior.
- P: event-oriented state machines are a useful way to model distributed protocol behavior.
- Molly / Lineage-Driven Fault Injection: fault exploration should be guided by the dependencies needed to violate a meaningful outcome.