Skip to content

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 shouldHaveBeenCalled counts of 0 and validate() 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:

requestMatcher = RequestMatcher.message<GetUserRequest> { it.userId == "123" }

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 called validate().

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/Retried and the peek* 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:

  1. Kafka publish default partition (behavior): publish no 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 explicit partition or a message key.
  2. 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.
  3. 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. Error stubs are type-agnostic and never conflict.
  4. Bidi requestMatcher is rejected (behavior): it was silently ignored before; passing a non-Any matcher now fails fast. Use metadataMatcher, or inspect requests inside the handler.
  5. validate() sees more (behavior, intentional): unmatched requests without test tags now fail every test's validate() instead of being invisible (WireMock), and gRPC Mock journals unknown-method and bidi calls it previously dropped. If validate() newly fails, it found real unmatched traffic.
  6. Stubs registered outside a test context are no longer tagged "default" — they're untagged and visible to every test's journal view.
  7. Binary compatibility (recompile if you compiled against Stove types): ReceivedRequest gained a testId field and StubDefinition subclasses gained delay/thenFailWith/trailers fields (data-class constructor/copy changes); WireMock mock* methods gained a trailing delay: Duration? parameter. All source-compatible.
  8. 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.