0.26.0¶
Status: Upcoming — not released yet
This release is a ground-up rework of Stove's two mocking systems — stove-wiremock and stove-grpc-mock — plus a new evidence pipeline that turns the dashboard into a network tab for your application's egress. It also ships the stove-kafka internals rework: event-driven waits instead of polling, and test-scoped assertions that make parallel suites safe.
The guiding principle throughout: evidence is either proven or explicitly unattributed — never inferred. A mock exchange is linked to a test only through provable signals (the X-Stove-Test-Id header, W3C baggage, or the matched stub's registration tag); everything else is shown as unattributed rather than guessed. In tests we want certainty, not blur.
Mock correctness: test-scoped, fail-open journals¶
Both mocks now record every exchange into a test-scoped journal with fail-open semantics: an entry is excluded from a test's view only when it is provably tagged with a different test id. Untagged traffic (apps without propagation) stays visible to every test — behavior identical to before scoping existed — so scoping can only add precision, never hide evidence.
What this fixes:
- gRPC Mock
validate()was not test-scoped — one test's unmatched request failed every other test in the suite. It is now scoped and parallel-safe. - WireMock's journal silently dropped untagged traffic — apps without header propagation got
shouldHaveBeenCalledcounts of 0 andvalidate()could never see their unmatched requests. Both are fixed. - Calls to gRPC methods with no stubs at all were invisible — a typo'd method name short-circuited inside gRPC before Stove saw it. These are now journaled with a "no stubs registered for this method" diagnosis and still answer
UNIMPLEMENTED. - Bidi streaming calls are journaled like every other RPC type, matched and unmatched.
Attribution needs no setup for matched traffic: the stub you registered inside the test carries the test's identity, so even a black-box container app is scoped correctly. For unmatched traffic, enabling stove-tracing propagates the test id via W3C baggage and makes attribution exact everywhere.
Near-miss diagnostics: failures explain why nothing matched¶
Instead of "unmatched request" or a bare count mismatch:
- WireMock
validate()failures diff each unmatched request against the test's closest stubs (ranked by WireMock's own match distance), and zero-match verifications diff the expected pattern against the requests the test actually received. A stub that matches exactly but wasn't active is called out as consumed (removeStubAfterRequestMatched) or registered too late. - gRPC Mock captures per-candidate rejection reasons at request time (so they survive stub removal): which matcher rejected — request or metadata — and for
RequestMatcher.ExactMessage, the expected vs. received payloads rendered as proto text.
gRPC Mock grows a verification API¶
The module previously had no way to assert "the app called this method". Now:
grpcMock {
// Typed, point-in-time, test-scoped; exact-count semantics
shouldHaveBeenCalled<GetUserRequest>("users.UserService", "GetUser") { it.userId == "123" }
shouldNotHaveBeenCalled<GetUserRequest>("users.UserService", "GetUser") { it.userId == "999" }
}
There is deliberately no timeout parameter: mock assertions run after your anchor assertion (Kafka's atLeastIn, an awaited HTTP call) has already absorbed any async wait — by then the call either happened or it didn't.
Stubbing and verification also accept generated MethodDescriptors, ending string-typo UNIMPLEMENTED debugging:
grpcMock {
mockUnary(GreeterGrpc.getSayHelloMethod(), response = reply)
shouldHaveBeenCalled<HelloRequest>(GreeterGrpc.getSayHelloMethod())
}
And request matching is typed:
Resilience testing becomes one-liners¶
WireMock:
wiremock {
// Network-level faults
mockFault(RequestMethod.GET, "/payments/status", Fault.CONNECTION_RESET_BY_PEER)
// Latency on any stub — client-timeout tests
mockGet(url = "/slow", statusCode = 200, responseBody = body.some(), delay = 2.seconds)
// Retry journeys
behaviourFor("/payments", ::post) {
failsTimes(2, withStatus = 503)
thenSucceeds { aResponse().withStatus(200).withBody("""{"recovered":true}""") }
}
// Responses computed from the actual request
mockDynamic(RequestMethod.POST, "/orders") { request, serde ->
aResponse().withStatus(201).withBody("""{"echo":${request.bodyAsString}}""")
}
}
gRPC Mock:
grpcMock {
// Deadline testing — DEADLINE_EXCEEDED is finally reproducible
mockUnary(service, method, response = reply, delay = 2.seconds)
// Streams that fail mid-flight after emitting items
mockServerStream(service, method, responses = items, thenFailWith = Status.UNAVAILABLE)
// Structured errors with trailers, like real gRPC APIs return
mockError(service, method, status = Status.Code.FAILED_PRECONDITION, trailers = metadata)
}
GrpcMockSystemOptions gains opt-in enableHealthService (the standard grpc.health.v1.Health, for apps that gate startup on a healthy channel) and enableReflectionService (so grpcurl works against the mock).
Dashboard: a network tab for your application's egress¶
Every request that reaches either mock — matched or not — is emitted as a mock interaction event and streamed to the stove CLI: method/URL or gRPC method, final status (including CANCELLED/DEADLINE_EXCEEDED outcomes the handler never sees), latency, truncated request/response bodies with sensitive-field redaction (authorization, tokens, passwords, cookies), near-miss diagnoses for unmatched exchanges, scenario state transitions, injected faults and configured delays, and the trace id when the request carried traceparent.
Each interaction carries its attribution: PROVEN_HEADER, PROVEN_BAGGAGE, PROVEN_STUB, or UNATTRIBUTED. Unattributed exchanges land in a run-level lane — never guessed into a test — and each one is actionable: fix the stub matcher, or enable stove-tracing for propagation.
The mocks also raise warnings — diagnostics that never fail tests, computed strictly from provable evidence:
UNUSED_STUB— a stub the test registered but nothing matched: dead fixture, or a test passing without the interaction it thinks it proves.CROSS_TEST_MATCH— a request provably sent by one test served by a stub registered in another.UNVALIDATED_UNMATCHED— a test ended with its own unmatched requests and never calledvalidate().
Snapshots now carry a timestamp and a trigger, and the dashboard captures the failing system's snapshot at the moment of the first failure (FAILURE) in addition to test end (TEST_END) — the two genuinely differ once cleanup and stub consumption run.
The stove CLI persists all of it (schema migration runs automatically) and serves it over new endpoints: /runs/{run}/interactions, /runs/{run}/warnings, their per-test variants, and /interactions|warnings/ambient for the unattributed lane. Older CLI versions simply ignore the new events — the protocol change is additive.
Agents triaging over MCP get the same evidence first-class: stove_failure_detail now includes the failed test's unmatched exchanges (with their near-miss diagnoses) and mock warnings, stove_timeline interleaves mock exchanges with report entries chronologically, the new stove_interactions tool queries exchanges and warnings per test or per run (including the unattributed lane), and stove_raw_evidence accepts interaction and warning kinds. Bodies pass through the same sensitive-field redaction as every other MCP surface.
stove-kafka: event-driven internals and test scoping¶
Shipped in the same window (see lib/stove-kafka/ROADMAP.md for the full engineering log):
- Signal-driven waits.
waitUntilConsumed/Published/Failed/Retriedand thepeek*loops no longer poll with fixed delays; they suspend on the message store's version signal. Assertions complete as soon as the condition holds. - Test-scoped assertions and failure dumps, fail-open — the same scoping model as the mocks. A latent bug is fixed where another test's failed message with matching content could fail the current test.
- Per-system bridge runtime. Keyed Kafka systems own their observer scope, port, and serde; closing one no longer affects others. The observer wire contract is unchanged — existing applications and the published Go bridge keep working with
STOVE_KAFKA_BRIDGE_PORT, no regeneration or lockstep release. - Exception-honest assertions: an exception thrown by your condition propagates as itself instead of being mislabeled a timeout.
Behavior changes & migration guide¶
From 0.25.x to 0.26.0¶
Most suites upgrade drop-in. Review these:
- Kafka
publishdefault partition (behavior):publishno longer pins records to partition 0 by default — Kafka's configured partitioner decides, so null-key publishes to multi-partition topics spread via the sticky partitioner. Tests asserting partition metadata or cross-message ordering across partitions may need pinning an explicitpartitionor a message key. - gRPC Mock stub precedence (behavior): among matching stubs, the last registered wins (test-local stubs override fixture defaults, matching WireMock's own semantics). Previously the first match won.
- gRPC Mock method-type conflicts fail fast (behavior): registering stubs of different RPC types (e.g. unary + server-streaming) for one method now throws at registration instead of silently letting registration order pick the handler type.
Errorstubs are type-agnostic and never conflict. - Bidi
requestMatcheris rejected (behavior): it was silently ignored before; passing a non-Anymatcher now fails fast. UsemetadataMatcher, or inspect requests inside the handler. validate()sees more (behavior, intentional): unmatched requests without test tags now fail every test'svalidate()instead of being invisible (WireMock), and gRPC Mock journals unknown-method and bidi calls it previously dropped. Ifvalidate()newly fails, it found real unmatched traffic.- Stubs registered outside a test context are no longer tagged
"default"— they're untagged and visible to every test's journal view. - Binary compatibility (recompile if you compiled against Stove types):
ReceivedRequestgained atestIdfield andStubDefinitionsubclasses gaineddelay/thenFailWith/trailersfields (data-class constructor/copychanges); WireMockmock*methods gained a trailingdelay: Duration?parameter. All source-compatible. - stove CLI: upgrade the CLI to see interactions and warnings; the database migrates automatically on first start. An old CLI keeps working with new tests (new events are ignored); a new CLI keeps working with old tests.