Stove 0.24.0 — Going Polyglot, and an MCP for AI Triage¶
Stove started as a JVM end-to-end testing framework. Spring Boot, Ktor, Quarkus, Micronaut — spin them up with real PostgreSQL, real Kafka, real WireMock, then assert the whole flow with one Kotlin DSL. That core hasn't changed. What 0.24.0 changes is who gets to play.
This release pushes on five things at once: Go becomes a first-class application under test, any container image can be the AUT, the framework starter is no longer required (test against an already-deployed app), one system type can have many keyed instances (verify across services), and the stove CLI grows an MCP endpoint so AI agents can triage failed runs without scraping logs.
Why polyglot, why now¶
Microservice fleets are not monolingual. The order service might be Spring Boot, the inventory service Go, the recommender Python, the edge router Rust. If your e2e framework only covers the JVM, every non-JVM service either gets its own bespoke harness or doesn't get end-to-end tested at all. Both are bad outcomes.
The interesting question isn't "how do we add a Go runner?" It's "what's actually language-specific about an end-to-end test?" The answer turns out to be: very little. The Stove DSL — http {}, postgresql {}, kafka {}, tracing {}, dashboard {} — is about the contract: what went over the wire, what's in the database, what spans appeared. The language of the application under test is an implementation detail.
So 0.24.0 splits AUT lifecycle from test logic. Two new starters, the test surface unchanged:
stove-processruns your app as a host binary. Fast iteration, easy debugging.stove-containerruns your app as a Docker image. CI parity with the artifact you ship.
Both work for any language. Both pass infrastructure config the same way (envMapper / argsMapper). Both ride the same readiness model. The Kotlin tests don't care which one is in play.
A tour: Go on Stove¶
Go gets the deepest treatment because it's the showcase language. The go-showcase recipe is an HTTP + PostgreSQL + Kafka service. Same StoveConfig.kt runs the binary directly or runs it inside a Docker container, branched on a single system property.
The Go side¶
The Go application stays small. All tracing is in the infrastructure layer — otelhttp wraps the mux, otelsql wraps the DB driver. Business handlers stay clean:
func handleCreateProduct(db *sql.DB, producer KafkaProducer) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req createProductRequest
json.NewDecoder(r.Body).Decode(&req)
product := Product{ID: uuid.New().String(), Name: req.Name, Price: req.Price}
insertProduct(r.Context(), db, product) // otelsql traces this automatically
if producer != nil {
event := ProductCreatedEvent{ID: product.ID, Name: product.Name, Price: product.Price}
eventBytes, _ := json.Marshal(event)
producer.SendMessage("product.created", product.ID, eventBytes)
}
writeJSON(w, http.StatusCreated, product)
}
}
The Stove HTTP client sends a traceparent header. otelhttp extracts it. Spans created in the Go app share the originating test's trace ID. No glue code, no manual correlation.
The Kotlin side¶
test("create product, verify HTTP, DB, Kafka, traces") {
stove {
var productId: String? = null
http {
postAndExpectBody<ProductResponse>(
uri = "/api/products",
body = CreateProductRequest(name = "Test", price = 42.99).some()
) { actual ->
actual.status shouldBe 201
productId = actual.body().id
}
}
postgresql {
shouldQuery<ProductRow>(
query = "SELECT id, name, price FROM products WHERE id = '$productId'",
mapper = productRowMapper
) { rows -> rows.size shouldBe 1 }
}
kafka {
shouldBePublished<ProductCreatedEvent>(10.seconds) {
actual.name == "Test"
}
}
tracing {
shouldContainSpan("http.request")
shouldNotHaveFailedSpans()
}
}
}
If you removed the file path, you couldn't tell from this test that the AUT is in Go. That's the point.
Kafka, in three flavors¶
shouldBePublished<T> and shouldBeConsumed<T> need an observation point on the broker side. For JVM apps, Stove uses Kafka client interceptors. For Go, 0.24.0 ships stove-kafka, a small Go library that forwards produced/consumed/committed messages over gRPC to Stove's observer.
The bridge is library-agnostic at its core. First-party integrations exist for the three Go Kafka clients people actually use:
- IBM/sarama via
ProducerInterceptor/ConsumerInterceptor - twmb/franz-go via
kgo.WithHooks(...) - segmentio/kafka-go via tiny
ReportWritten/ReportReadhelpers
Want confluent-kafka-go or something else? Skip the subpackages, use the core API:
bridge.ReportPublished(ctx, &stovekafka.PublishedMessage{...})
bridge.ReportConsumed(ctx, &stovekafka.ConsumedMessage{...})
bridge.ReportCommitted(ctx, topic, partition, offset+1)
In production, STOVE_KAFKA_BRIDGE_PORT is unset, NewBridgeFromEnv() returns nil, and every method becomes a no-op. Zero overhead in prod, full assertion fidelity in tests.
Coverage from black-box tests¶
A nice side-effect of standardizing on stove-process and stove-container: Go 1.20+ integration coverage just works. Build with go build -cover, set GOCOVERDIR, and Go writes coverage data on graceful shutdown. Stove already sends SIGTERM and waits for clean exit — exactly the lifecycle Go's coverage tooling expects.
./gradlew e2eTestWithCoverage -Pgo.coverage=true
./gradlew e2eTest-containerWithCoverage -Pgo.coverage=true
Per-function summary, HTML report. One catch worth a paragraph: when Go runs under Java's ProcessBuilder, the stdout pipe can close before the process exits. Log writes to that closed pipe trigger SIGPIPE — Go dies before flushing coverage. The fix is one line in main():
That's it. No framework changes were needed for coverage; it's a Gradle concern, an env var, and an existing graceful-shutdown signal.
Container mode is not just for Go¶
stove-container is language-agnostic. Anything that ships in an image works — Go, Python, Node.js, Rust, .NET, even your existing JVM artifact when you want to test the actual deployed binary instead of the in-process bean graph.
One thing worth being explicit about: building the image is not Stove's job. containerApp(...) only needs an image reference. Where it comes from is your call:
- A tag your CI just produced (
-Papp.image=ghcr.io/acme/app:sha-abc) - A pull from a registry, lazy on first use
- An optional Gradle
Exectask that runsdocker buildfor local iteration
Most teams already have a perfectly good image-build pipeline. Stove doesn't try to own it.
containerApp(
image = System.getProperty("app.container.image"),
target = ContainerTarget.Server(
hostPort = 8090, internalPort = 8090,
portEnvVar = "APP_PORT", bindHostPort = false
),
envProvider = envMapper {
"database.host" to "DB_HOST"
"kafka.bootstrapServers" to "KAFKA_BROKERS"
env("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4317")
},
configureContainer = { withNetworkMode("host") }
)
configureContainer { ... } exposes the underlying Testcontainers GenericContainer, so anything Testcontainers can do — bind mounts, network mode, log consumers, capabilities — is available without bespoke API surface.
A common pattern: e2eTest runs process mode for daily local development; e2eTest-container runs container mode in CI against the image the build job just published. Same StoveConfig, same tests, branched on a system property.
Black-box mode: testing apps Stove didn't start¶
Polyglot AUT is one half of "Stove doesn't have to own the app." The other half is providedApplication() — telling Stove the application is already running somewhere, and you just want to run your tests against it.
Stove().with {
httpClient { HttpClientSystemOptions(baseUrl = "https://staging.myapp.com") }
postgresql {
PostgresqlOptions.provided(
jdbcUrl = "jdbc:postgresql://staging-db:5432/myapp",
host = "staging-db", port = 5432,
configureExposedConfiguration = { emptyList() }
)
}
providedApplication {
ProvidedApplicationOptions(
readiness = ReadinessStrategy.HttpGet(url = "https://staging.myapp.com/health")
)
}
}.run()
Same Stove DSL. Same assertions. No springBoot() / ktor() / goApp() block. Stove waits for the deployed health check, then runs your tests against the live URL — and verifies side effects in the actual database / Kafka / Redis the deployed app uses (via *.provided(...) factories on each system).
The use case is post-deployment smoke testing: the e2e tests you already wrote can double as a CI/CD gate that hits staging immediately after a release. Same code, same intent, different infrastructure.
Multiple instances of the same system, with keys¶
Microservice integration tests usually need to talk to more than one downstream service, or verify state in more than one database. 0.24.0 adds keyed system registration for that:
object OrderService : SystemKey
object PaymentService : SystemKey
object AppDb : SystemKey
object AnalyticsDb : SystemKey
Stove().with {
httpClient { HttpClientSystemOptions(baseUrl = "https://myapp.com") } // default
httpClient(OrderService) { HttpClientSystemOptions(baseUrl = "https://order.internal") }
httpClient(PaymentService) { HttpClientSystemOptions(baseUrl = "https://pay.internal") }
postgresql(AppDb) { /* ... */ }
postgresql(AnalyticsDb) { /* ... */ }
}.run()
In tests, the same key drives the validation DSL:
http(OrderService) { getResponse("/api/orders/$orderId") { /* ... */ } }
postgresql(AnalyticsDb) { shouldQuery<AnalyticsEvent>(/* ... */) { /* ... */ } }
Keys are Kotlin objects — compile-time-safe, IDE-autocompleted, refactor-safe. Default and keyed instances of the same type coexist independently. Reports and traces label keyed calls (HTTP [OrderService] > GET /api/orders/123) so it's clear which service did what.
This pairs naturally with providedApplication(). A single Stove config can wire your app's API, three downstream services, two shared databases, and a Kafka cluster — all already running in staging — and a single Kotlin test asserts behaviour across all of them.
MCP — failure triage for AI agents¶
The other big addition in 0.24.0 has nothing to do with non-JVM apps and everything to do with how people debug failed e2e runs in 2026.
If you're using an AI agent in your editor or CI bot, you've probably watched it try to triage a failed test by reading the entire stdout, then the entire stderr, then tail-ing logs, then guessing at trace IDs. It works, but it burns tokens proportional to log size, and it hallucinates when names are ambiguous.
The Stove dashboard already records every run — timeline, traces, snapshots, Kafka message counts — in a local SQLite database. 0.24.0 adds a Model Context Protocol endpoint on the same stove CLI that exposes that data as structured tools:
$ stove
Stove CLI v0.24.0 running
UI: http://localhost:4040
REST: http://localhost:4040/api/v1
MCP: http://localhost:4040/mcp
gRPC: localhost:4041
Wire any MCP-capable agent at http://localhost:4040/mcp. Then the conversation looks like:
Agent: stove_failures()
→ 2 failed runs across go-showcase and order-service
Agent: stove_failure_detail(run_id="...", test_id="...")
→ compact failure packet: assertion, expected vs actual,
timeline of last 5 actions, exception class
Agent: stove_trace(run_id="...", test_id="...")
→ critical path: 4 spans, exception in PostgresOrderRepository.save
Eight tools total, all read-only, all local-only. Defaults are token-aware: payloads are truncated deterministically with omitted-counts, sensitive keys (authorization, cookie, password, secret, token, apiKey, credential) are redacted before return, and a budget: tiny|compact|full knob lets the agent dial detail when needed.
Two design decisions worth calling out:
-
run_id + test_idis the only authoritative test selector. Apps and runs can contain duplicate test names; an agent inferring "OrderTest::should create order" from a phrase will eventually hit the wrong run. Every tool result includes the next call's exact arguments — agents follow links, they don't construct queries. -
Loopback only. The
/mcpendpoint accepts only localhostHost/Originheaders and rejects anything else. This blocks DNS rebinding from a malicious page in your browser. Safe to leave running on a dev machine; not exposed externally.
If MCP is unavailable, agents fall back to normal test output and logs — it's an optimization, not a dependency.
Putting it together¶
Stove 0.24.0 is one consistent picture, even though the changes touch four different surfaces:
- A test that drives a Go service through HTTP, asserts on PostgreSQL state, validates Kafka messages, and traces the call chain — using the exact same DSL that drives the Spring Boot service next door.
- The same test running against a host binary in your IDE for fast feedback, then against a real Docker image in CI for production parity, with one
-Daut.modeflip. - When something fails, the dashboard shows you what happened. When the agent in your editor wants to help, it asks the dashboard via MCP instead of inhaling logs.
Three integrations, one feedback loop. That's the release.
Getting started¶
Upgrade the CLI:
Add the modules you need to your test classpath:
testImplementation(platform("com.trendyol:stove-bom:0.24.0"))
testImplementation("com.trendyol:stove-process") // host binary
testImplementation("com.trendyol:stove-container") // Docker image
testImplementation("com.trendyol:stove-dashboard") // dashboard streaming
testImplementation("com.trendyol:stove-tracing") // distributed tracing
testImplementation("com.trendyol:stove-kafka") // Kafka assertions
For Go Kafka assertions:
Links¶
- Full 0.24.0 release notes
- Other Languages & Stacks overview
- Go Process Mode
- Go Container Mode
- MCP component docs
- Dashboard component docs
go-showcaserecipe — process and container modes in one repostove-kafkaGo bridge