Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error codes

Every failure this harness can raise carries a stable machine-readable code alongside its human-readable message. The reporting layer classifies outcomes by code, the JUnit and Markdown renderers group by it, and scenario assertions branch on it, so the code — not the message text — is the interface.

Two properties this document exists to guarantee:

  • Every code the harness can raise is listed here, so a consumer that observes one in a report can look up what it means without reading the source.
  • Every code listed here exists, because a stale table is worse than no table: consumers trust it.

Both are enforced by scripts/check-error-codes.py, which discovers every crate’s code() method, extracts the codes it can return, and compares them against the tables below. It runs in CI, so drift is a build failure.

Naming, and how it differs from the simulator

Codes here are UPPER_SNAKE_CASE (ASSERTION_TYPE_MISMATCH). The crucible-simulator domain codes are snake_case (insufficient_balance), and those codes pass through this harness unchanged — an adapter never re-codes a simulator failure, so one condition always reports one code.

The two conventions are currently a family inconsistency rather than a deliberate distinction; harmonising them is tracked separately. Until then, treat an UPPER_SNAKE_CASE code as a harness failure (this repository’s own machinery: definitions, fixtures, adapters, assertions, the runner) and a snake_case code as a domain failure reported by the system under test.

The stability contract

  • Never rename a code. Reporting, classification and consumer match statements depend on the string.
  • Never reuse a retired code for a different condition.
  • An unrecognised code means “a failure I do not specifically classify” and must be reported as such, not treated as a panic or an unreachable branch.

Harness definitions and registry

scenario-core

Definition-time validation: a scenario, its metadata, or its environment is structurally wrong, so it is rejected before anything executes.

CodeRetryableRaised when
INVALID_IDnoA scenario id is malformed or does not match the required shape.
DUPLICATE_IDnoA scenario id is registered twice.
INVALID_METADATAnoScenario metadata is missing a required field or is inconsistent.
UNKNOWN_ACTORnoA step references an actor the scenario never declares.
UNKNOWN_TOKENnoA step references a token the scenario never declares.
UNKNOWN_STATEnoA step references a named state that was never captured.
UNKNOWN_REFERENCEnoA step references a value, snapshot, or fixture that does not exist.
UNAVAILABLE_CAPABILITYnoThe scenario requires a capability the harness does not provide.
UNSUPPORTED_ENVIRONMENTnoThe scenario targets an environment this harness cannot build.
INVALID_SEEDnoA seed is malformed or out of range.
INVALID_TIMEOUTnoA timeout value is malformed or not a positive duration.
CONFIDENTIAL_PUBLIC_INPUTnoPrivacy guard: a proof-generation step’s public inputs name a confidential field. Rejected at definition time so private material cannot be published by construction, not merely by convention.
SERIALIZATIONnoEncoding or decoding a scenario, vector, or result failed.

scenario-registry

CodeRetryableRaised when
DUPLICATE_SCENARIOnoA scenario with that id is already registered.
UNKNOWN_SCENARIOnoA lookup, filter, or replay names a scenario that is not registered.
INVALID_FILTERnoA registry filter (category, tag, capability, id prefix) is malformed.

Runner

scenario-runner

Execution, lifecycle, and isolation failures. These describe the harness’s own behaviour around a scenario, not a verdict about the system under test.

CodeRetryableRaised when
CONFIGURATIONnoThe runner was configured inconsistently (for example, parallel workers with serial-only scenarios).
SCENARIO_DEFINITIONnoThe definition is valid per scenario-core but unusable as written.
ENVIRONMENTnoBuilding the scenario environment failed.
TIMEOUTyesThe scenario exceeded its timeout. Retryable only because a timing-dependent failure may not recur; a deterministic failure never will, and is classified as such.
CANCELLEDyesThe run was cancelled before the scenario completed. Classified distinctly from a failure, so an abort is never reported as a defect.
RETRY_EXHAUSTEDnoThe declared retry limit was reached.
NON_RETRYABLEnoA failure occurred that the retry policy refuses to retry, by design: deterministic failures are never retried.
ISOLATIONnoA scenario’s state leaked into another, or an isolation boundary was violated.
CLEANUPnoA cleanup hook failed after the scenario ran.
HOOKnoA lifecycle hook failed.
INFRASTRUCTUREyesA transport or harness-level dependency failed. Distinct from NON_RETRYABLE precisely so the retry policy can treat the two differently.

Assertions and fixtures

assertions

CodeRetryableRaised when
ASSERTION_MISSING_OBSERVATIONnoAn assertion expected an observation the run never produced. Usually a scenario bug rather than a system failure.
ASSERTION_TYPE_MISMATCHnoThe observed value’s type does not match what the assertion compares against.
ASSERTION_MISSING_FIXTUREnoAn assertion references fixture data that does not exist.
ASSERTION_INTERNALnoAn internal inconsistency in the assertion engine — a bug here, not a finding.

fixtures

CodeRetryableRaised when
FIXTURE_MISSINGnoA referenced fixture is absent from the catalog.
FIXTURE_MALFORMEDnoA fixture failed to parse or violates its schema.
FIXTURE_VERSION_MISMATCHnoA fixture’s declared version is not one this harness supports.
FIXTURE_INTERNALnoAn internal inconsistency while loading fixtures.

Adapters

Adapters translate the harness’s operation vocabulary into a backing surface. A translation failure is an adapter fault: it means the operation could not be expressed, which is a different finding from the surface rejecting it.

adapters/simulator

CodeRetryableRaised when
INVALID_OPERATIONnoThe operation could not be translated into a simulator call.

adapters/prover

CodeRetryableRaised when
NO_PROOF_FIXTUREnoA proof was required but the pack provides no fixture posture for it.
GENERATIONnoProof generation through the prover surface failed.
UNSUPPORTED_OPERATIONnoThe prover surface has no handling for this operation.

adapters/soroban

CodeRetryableRaised when
UNKNOWN_CONTRACTnoThe contract surface names a function on a contract it does not know.
UNKNOWN_FUNCTIONnoThe operation has no contract-function translation.
MISSING_ARGUMENTnoA call was translated without a required argument.
INVALID_ARGUMENTnoAn argument does not match the call’s expected type.
CONFIDENTIAL_INVOCATIONnoPrivacy guard: an invocation would place confidential material in publicly visible contract arguments.

adapters/testnet

CodeRetryableRaised when
TESTNET_NOT_CONFIGUREDnoThe live-network path was used without explicit opt-in configuration. The adapter never silently falls back to a network.
INVALID_CONFIGURATIONnoThe network configuration is malformed (bad RPC URL, unsupported network).
INVALID_EXECUTIONnoA network execution attempt is structurally invalid.
POLL_TIMEOUTyesPolling for a transaction result exceeded its budget. The network may simply be slow.
MALFORMED_STATUSnoA network status response did not match the expected shape.

Codes shared across crates

Two codes appear in more than one module on purpose: the same condition has the same name wherever it surfaces, so a consumer never has to special-case which crate produced it.

CodeRetryableRaised when
FIXTURE_INCONSISTENCYnoA fixture contradicts another fixture or a declared expectation. Raised by the simulator and prover adapters as well as the Soroban adapter.
INTERNALnoAn internal inconsistency indicating a bug in this repository rather than a finding about the system under test. Raised by every module that defines codes.

Using codes

#![allow(unused)]
fn main() {
if outcome.code() == "CONFIDENTIAL_PUBLIC_INPUT" {
    // A privacy guard tripped: this is a finding about the scenario, not a
    // flaky run, so it must never be retried or classified as infrastructure.
    report(Classification::PrivacyViolation);
}
}

Prefer the typed error variant inside Rust, where match is exhaustive and the compiler enforces it. Codes exist for the surfaces a type cannot reach: the JSON report, the JUnit XML a CI server ingests, the Markdown summary, and the exit code of the CLI.