vike
DeveloperTesting

The exec contract

One scenario table folded through the real order FSM against every covered venue's real event mapper — the machine-checked version of emitter-split, no-silent-vanish and exactly-one-terminal.

The venue-adapter execution contract spans every bridge crate and lives in none of them, so its harness is hosted one layer down, in crates/vike-bridge-core/tests/bridge_conformance.rs. Before it existed the contract was verified ad hoc, per venue. Now it is one scenario table run against every covered venue's real, fixture-tested event_mapper, folded through the real vike_exec::ManagedOrder FSM — the same code the live core folds through. Only the venue-native JSON frames and the mapper are venue-specific; the scenarios, the folds and the assertions are shared.

The three clauses under test

The file's header states them as the contract:

  • Emitter split — Rust emits OrderSubmitted synchronously at submit; the venue emits OrderAccepted, OrderRejected, OrderCanceled and the fill events.
  • No silent vanish — a dead venue path must synthesize a terminal OrderRejected.
  • Exactly one terminal — the FSM must reach exactly one of Filled, Canceled, Rejected, Denied or Expired. Illegal or out-of-order events are dropped idempotently, never corrupting the single terminal.

The oracle is not a re-implementation. ContractOrder wraps the real FSM and mirrors ExecutionEngine::on_event's thin per-order policy: the bare-fill versus wrap-fill split, trade-id dedup so a reconnect replay never double-counts, and a counter for the one genuinely bad case — a terminal event that failed to apply while the order was still live. That counter must end at zero in every healthy scenario, and the terminal count must end at exactly one.

The four scenarios

  1. Lifecycle — submit, accept, partial fill, full fill reaches Filled with one terminal.
  2. TransportDeath — a dead venue path still synthesizes a terminal, driven through the exact shared seam each venue really uses: ExecActor for command venues, resolve_ambiguous_submit for REST-poll venues.
  3. CancelAfterClose — a late venue OrderCanceled replay and a late OrderCancelRejected advisory arriving after the order already closed are both dropped without corrupting the single terminal.
  4. ReconnectMidOrder — driven through the real shared pump run_user_data_forever with a scripted drop and reconnect that replays the mid-order partial: no duplicate terminal, no double-counted fill, no lost terminal.

The two axes

ExecKind selects which no-vanish seam TransportDeath exercises — CommandActor (a dead thread synthesizes OrderRejected) or RestPoll (an ambiguous or timed-out submit resolves through the shared resolve_ambiguous_submit).

FillShape is the orthogonal axis, and it exists because fill granularity genuinely differs. Crypto venues stream Cumulative partials — each piece a distinct OrderPartiallyFilled before the terminal OrderFilled. The FX, CFD and equity venues fill Whole: one execution reports the full quantity, and their real mappers have no partial-fill path at all. Alpaca even folds a "partial_fill" SSE event into OrderFilled. Lifecycle and ReconnectMidOrder branch on this so a whole-fill venue is exercised faithfully instead of being asserted into a state it can never emit; TransportDeath and CancelAfterClose are already shape-agnostic.

A doc comment is not a declaration

ExecKind's own comment records why it names no venue list: the list that used to live there named four of the seven CommandActor venues, was written when the table held six, and never grew when three more joined. FillShape is the counter-example it points at — its variants name venues, but each name carries the reason (that venue's real mapper has no partial-fill path), so it reads as evidence rather than as a roster somebody must keep in sync.

Covered and deferred

covered_bridges() returns the venues in the table; DEFERRED records the rest with a reason each. No coverage fraction is written anywhere — the two lists partition the roster, so their own lengths are the count, and coverage_matrix prints the whole grid under --nocapture together with the deferral reasons, so what did not run is explicit. The roster gate then holds the partition exact against vike_model::VENUES.

Deferring a venue: name the shape, never a feature gate

The FXCM row is the worked example, and its removal is recorded in DEFERRED's own doc comment. The old reason read "behind the fxcm feature; default build is an Unavailable stub — no real event_mapper is compiled to drive". Every clause of that was false: the crate declares its module tree with no cfg at all, map_fxcm_event was always compiled in every build, and the feature gates nothing but build.rs. The obstacle was one keyword, mod instead of pub mod, behind a reason nobody re-read for as long as it sounded plausible. A feature is a claim about a build, and a build is exactly the thing a stale reason stops describing.

FXCM is now covered, and its row is instructive for the opposite reason: its "frames" are JSON envelopes the C++ shim builds, its accept is the synchronous return of a placement FFI call, and its comment states plainly what a green row does not say — nothing about the shim, the FFI, or any ForexConnect behaviour behind it, because no CI machine has ever linked that SDK. The row covers the venue's pure layer, which is where its last four recorded defects lived.

Frames from real wire

A venue's frame methods may source their template from that venue's committed sanitized capture under crates/bridges/<venue>/tests/fixtures/captured/, written by the venue's opt-in capture smoke and consumed here through captured_template. The harness loads the first captured frame as a structure template — the real envelope, field set and value types — and patches only the scenario-driven leaves through the type-preserving patch_f64 and patch_str. Absent a fixture it falls back to the hand-authored frame, byte-identical to before. The coverage printout names which venues sourced captured templates, so what ran against real wire is never a guess.

Related: the order lifecycle, the ExecutionClient seam, test doubles.

On this page