[
  {
    "path": ".claude/skills/stove/SKILL.md",
    "content": "---\nname: stove\ndescription: Use when adding Stove e2e tests to a project, configuring Stove systems (HTTP, PostgreSQL, MySQL, MSSQL, Cassandra, MongoDB, Redis, Elasticsearch, Couchbase, Kafka, WireMock, gRPC, Dashboard), writing tests with the stove {} DSL, enabling OpenTelemetry tracing, writing AbstractProjectConfig, extending Stove with custom systems, setting up smoke tests against remote/deployed applications (providedApplication), registering multiple instances of the same system type (keyed systems with SystemKey), testing non-JVM applications (Go, Python, Rust, Node.js) with processApp()/goApp() from stove-process or containerApp() from stove-container (target/readiness, env/args mapping, image-based AUT), the Stove Kafka bridge library (stove-kafka for Go with IBM/sarama, twmb/franz-go, or segmentio/kafka-go), or collecting Go code coverage from Stove black-box tests (go build -cover, GOCOVERDIR, SIGPIPE handling).\n---\n\n# Setting Up Stove E2E Tests\n\nCopy this checklist and track progress:\n\n```\nSetup Progress:\n- [ ] Step 1: Create test-e2e source set layout\n- [ ] Step 2: Configure Gradle (BOM, source set, e2eTest task)\n- [ ] Step 3: Extract run() function from application entry point\n- [ ] Step 4: Create StoveConfig (AbstractProjectConfig)\n- [ ] Step 5: Create kotest.properties (Kotest only)\n- [ ] Step 6: Configure systems inside Stove().with { }\n- [ ] Step 7: Configure tracing (optional)\n- [ ] Step 8: Write tests using stove {} DSL\n```\n\nImportant: Stove e2e tests are Kotlin-first. Even if your application is Java/Scala, keep e2e tests under `src/test-e2e/kotlin` and write Stove setup/tests in Kotlin.\n\n## Step 1: Project structure\n\n```\nyour-module/src/\n  main/(kotlin|java)/\n  test/(kotlin|java)/\n  test-e2e/\n    kotlin/com/yourcompany/yourapp/e2e/\n      setup/\n        StoveConfig.kt\n        InitialMigration.kt\n      tests/\n        OrderE2ETest.kt\n    resources/\n      kotest.properties\n```\n\n## Step 2: Gradle configuration\n\nFor source set registration, e2eTest task, and IDE integration details, see [gradle-config.md](gradle-config.md).\n\nAdd dependencies using the BOM:\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$stoveVersion\"))\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-spring\")\n    testImplementation(\"com.trendyol:stove-extensions-kotest\")\n\n    // Add only what you need:\n    testImplementation(\"com.trendyol:stove-http\")\n    testImplementation(\"com.trendyol:stove-postgres\")\n    testImplementation(\"com.trendyol:stove-mysql\")\n    testImplementation(\"com.trendyol:stove-mssql\")\n    testImplementation(\"com.trendyol:stove-cassandra\")\n    testImplementation(\"com.trendyol:stove-mongodb\")\n    testImplementation(\"com.trendyol:stove-redis\")\n    testImplementation(\"com.trendyol:stove-elasticsearch\")\n    testImplementation(\"com.trendyol:stove-couchbase\")\n    testImplementation(\"com.trendyol:stove-kafka\")\n    testImplementation(\"com.trendyol:stove-spring-kafka\")\n    testImplementation(\"com.trendyol:stove-wiremock\")\n    testImplementation(\"com.trendyol:stove-grpc\")\n    testImplementation(\"com.trendyol:stove-grpc-mock\")\n    testImplementation(\"com.trendyol:stove-tracing\")\n    testImplementation(\"com.trendyol:stove-dashboard\")\n    testImplementation(\"com.trendyol:stove-process\")  // non-JVM apps (Go, Python, etc.)\n    testImplementation(\"com.trendyol:stove-container\")  // non-JVM apps as Docker images\n}\n```\n\nFor Ktor, replace `stove-spring` with `stove-ktor`. For Quarkus, use `stove-quarkus`. For Micronaut, use `stove-micronaut`. For JUnit, replace `stove-extensions-kotest` with `stove-extensions-junit` and skip Step 5.\n\nIf you are unsure about Stove API names/signatures, verify from local downloaded artifacts (Gradle cache or Maven local repo) before writing code. See [gradle-config.md](gradle-config.md#resolve-api-ambiguity-from-local-artifacts).\n\n## Step 3: Extract run()\n\nStove starts your application from tests. Extract the entry point:\n\n```kotlin\n// src/main/kotlin/.../MyApp.kt\n@SpringBootApplication\nclass MyApp\n\nfun main(args: Array<String>) = run(args)\n\nfun run(\n    args: Array<String>,\n    init: SpringApplication.() -> Unit = {}\n): ConfigurableApplicationContext =\n    runApplication<MyApp>(*args) { init() }\n```\n\n## Step 4: StoveConfig\n\n```kotlin\nclass StoveConfig : AbstractProjectConfig() {\n    override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n    override suspend fun beforeProject() {\n        Stove()\n            .with {\n                // Systems go here — see Step 6\n            }.run()\n    }\n\n    override suspend fun afterProject() {\n        Stove.stop()\n    }\n}\n```\n\nFor JUnit, see [gradle-config.md](gradle-config.md) for the `BaseE2ETest` pattern.\n\n## Step 5: kotest.properties (Kotest only)\n\nCreate `src/test-e2e/resources/kotest.properties`:\n\n```properties\nkotest.framework.config.fqn=com.yourcompany.yourapp.e2e.setup.StoveConfig\n```\n\n## Step 6: Configure systems\n\nConfigure inside `Stove().with { }`. For all options per system, see [system-setup.md](system-setup.md).\n\n```kotlin\nStove()\n    .with {\n        httpClient {\n            HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n        }\n\n        bridge()\n\n        // Optional (requires com.trendyol:stove-tracing)\n        tracing { enableSpanReceiver() }\n\n        wiremock {\n            WireMockSystemOptions(\n                configureExposedConfiguration = { cfg ->\n                    listOf(\"payment.url=${cfg.baseUrl}\")\n                }\n            )\n        }\n\n        postgresql {\n            PostgresqlOptions(\n                databaseName = \"testdb\",\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"spring.datasource.url=${cfg.jdbcUrl}\",\n                        \"spring.datasource.username=${cfg.username}\",\n                        \"spring.datasource.password=${cfg.password}\"\n                    )\n                }\n            ).migrations { register<InitialMigration>() }\n        }\n\n        kafka {\n            KafkaSystemOptions(\n                serde = StoveSerde.jackson.anyByteArraySerde(),\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"spring.kafka.bootstrap-servers=${cfg.bootstrapServers}\",\n                        \"spring.kafka.producer.properties.interceptor.classes=${cfg.interceptorClass}\",\n                        \"spring.kafka.consumer.properties.interceptor.classes=${cfg.interceptorClass}\"\n                    )\n                }\n            )\n        }\n\n        mongodb {\n            MongodbSystemOptions(\n                databaseOptions = DatabaseOptions(\n                    default = DefaultDatabase(name = \"testdb\", collection = \"orders\")\n                ),\n                configureExposedConfiguration = { cfg ->\n                    listOf(\"spring.data.mongodb.uri=${cfg.connectionString}\")\n                }\n            )\n        }\n\n        // Optional: streams test events to stove CLI dashboard\n        dashboard {\n            DashboardSystemOptions(appName = \"my-service\")\n        }\n\n        // Application runner goes last\n        springBoot(\n            runner = { params ->\n                com.yourcompany.yourapp.run(params) {\n                    addTestDependencies {\n                        bean<TestSystemKafkaInterceptor<*, *>>(isPrimary = true)\n                        bean { StoveSerde.jackson.anyByteArraySerde() }\n                    }\n                }\n            },\n            withParameters = listOf(\"server.port=8080\")\n        )\n    }.run()\n```\n\nFor Spring Boot 4.x, use:\n\n```kotlin\naddTestDependencies4x {\n    registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)\n    registerBean { StoveSerde.jackson.anyByteArraySerde() }\n}\n```\n\nFor Ktor:\n\n```kotlin\nktor(\n    runner = { params -> com.yourcompany.yourapp.run(params, wait = false) },\n    withParameters = listOf(\"server.port=8080\")\n)\n```\n\nFor Quarkus:\n\n```kotlin\nquarkus(\n    runner = { params -> com.yourcompany.yourapp.main(params) },\n    withParameters = listOf(\"quarkus.http.port=8080\")\n)\n```\n\nFor Micronaut:\n\n```kotlin\nmicronaut(\n    runner = { params -> com.yourcompany.yourapp.run(params) },\n    withParameters = listOf(\"micronaut.server.port=8080\")\n)\n```\n\n## Step 7: Tracing (optional)\n\nFor full plugin options, buildSrc alternative, and trace validation DSL, see [tracing.md](tracing.md).\n\n```kotlin\nplugins { id(\"com.trendyol.stove.tracing\") version \"$stoveVersion\" }\n\nstoveTracing {\n    serviceName.set(\"my-service\")\n    testTaskNames.set(listOf(\"e2eTest\"))\n}\n```\n\n## Step 8: Write tests\n\nFor the complete DSL reference (HTTP, PostgreSQL, MySQL, MSSQL, Cassandra, MongoDB, Redis, Elasticsearch, Couchbase, Kafka, WireMock, gRPC Mock, gRPC Client, Bridge, multi-system examples), see [writing-tests.md](writing-tests.md).\n\n```kotlin\nclass OrderE2ETest : FunSpec({\n    test(\"should create order and publish event\") {\n        stove {\n            val userId = \"user-${UUID.randomUUID()}\"\n\n            wiremock {\n                mockGet(\"/inventory/item-1\", 200, InventoryResponse(true).some())\n            }\n\n            http {\n                postAndExpectBody<OrderResponse>(\n                    uri = \"/orders\",\n                    body = CreateOrderRequest(userId, 99.99).some()\n                ) { response ->\n                    response.status shouldBe 201\n                }\n            }\n\n            postgresql {\n                shouldQuery<OrderRow>(\n                    query = \"SELECT * FROM orders WHERE user_id = '$userId'\",\n                    mapper = { row -> OrderRow(row.string(\"id\"), row.string(\"status\")) }\n                ) { it.size shouldBe 1 }\n            }\n\n            kafka {\n                shouldBePublished<OrderCreatedEvent>(10.seconds) {\n                    actual.userId == userId\n                }\n            }\n        }\n    }\n})\n```\n\n## Smoke testing with providedApplication\n\nStove can test against **already-deployed** applications — any language, any framework. Use `providedApplication()` instead of a JVM runner. See [system-setup.md](system-setup.md#provided-application-smoke-testing) for full details.\n\n```kotlin\nStove().with {\n    httpClient { HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\") }\n    postgresql(AppDb) {\n        PostgresqlOptions.provided(\n            jdbcUrl = \"jdbc:postgresql://staging-db:5432/myapp\",\n            cleanup = { ops -> ops.execute(\"DELETE FROM orders WHERE test = true\") },\n            configureExposedConfiguration = { listOf() }  // no AUT to configure\n        )\n    }\n    providedApplication {\n        ProvidedApplicationOptions(\n            readiness = ReadinessStrategy.HttpGet(url = \"https://staging.myapp.com/health\")\n        )\n    }\n}.run()\n```\n\nKey points:\n- No JVM runner needed — the application is already running\n- Works with any language (Go, Python, .NET, Rust, Node.js, etc.)\n- `Bridge` (DI access) is **not available** — there's no local DI container\n- Use `cleanup` lambdas to manage test data on external infrastructure\n- Optional health check waits for the app to be ready before running tests\n\n## Keyed systems (multiple instances)\n\nRegister multiple instances of the same system type using `SystemKey`. See [system-setup.md](system-setup.md#keyed-systems-multiple-instances) and [writing-tests.md](writing-tests.md#keyed-system-tests) for examples.\n\n```kotlin\n// Define keys as singleton objects\nobject AppDb : SystemKey\nobject AnalyticsDb : SystemKey\nobject PaymentService : SystemKey\nobject InventoryService : SystemKey\n\nStove().with {\n    postgresql(AppDb) {\n        PostgresqlOptions(configureExposedConfiguration = { cfg ->\n            listOf(\"app.datasource.url=${cfg.jdbcUrl}\")\n        })\n    }\n    postgresql(AnalyticsDb) {\n        PostgresqlOptions(configureExposedConfiguration = { cfg ->\n            listOf(\"analytics.datasource.url=${cfg.jdbcUrl}\")\n        })\n    }\n    httpClient(PaymentService) {\n        HttpClientSystemOptions(baseUrl = \"https://pay.internal\")\n    }\n    springBoot(runner = { params -> run(params) })\n}.run()\n```\n\nAll systems support keyed registration: PostgreSQL, MySQL, MSSQL, Cassandra, MongoDB, Redis, Elasticsearch, Couchbase, Kafka (core), WireMock, gRPC, gRPC Mock, HTTP. Each keyed instance gets its own container, port, state storage, and configuration.\n\n## Writing custom Stove systems\n\nStove is extensible. For the complete pattern with a working db-scheduler example, see [custom-systems.md](custom-systems.md).\n\n## Best practices\n\n- Generate unique IDs per test: `UUID.randomUUID()`\n- Configure Stove once in `AbstractProjectConfig`, never per-test\n- Keep e2e tests in `src/test-e2e/kotlin` (also for Java/Scala applications)\n- If API is ambiguous, inspect local `stove-*.jar` / `stove-*-sources.jar` in Gradle/Maven caches and confirm class/method names before coding\n- Use `port = 0` for WireMock and gRPC Mock (dynamic ports, CI-safe)\n- Test through HTTP endpoints; verify DB state and events as side effects\n- Use `shouldBePublished<Event>(atLeastIn = 10.seconds) { ... }` — never `Thread.sleep`\n- Use `cleanup` lambdas in system options to wipe test data on teardown — essential for provided (external) instances\n- Use `Stove { keepDependenciesRunning() }` locally for faster iteration; disable in CI\n- **AI agent feedback loop**: Enable tracing + reporting. When tests fail, the execution report contains the full call chain, system snapshots, and timeline. AI agents can parse this structured output to understand exactly what went wrong inside the application and iterate on fixes with precise feedback.\n- **Kafka test-friendly settings**: Default Kafka producer/consumer settings are tuned for production throughput, not test speed. Configure `linger.ms=0`, `batch.size=1`, `auto.commit.interval.ms=100`, `auto-offset-reset=earliest`, and enable broker-level auto-topic creation. Without these, `shouldBePublished`/`shouldBeConsumed` assertions will timeout or flake. See [system-setup.md](system-setup.md#test-friendly-kafka-settings) for JVM details and [go-setup.md](go-setup.md) for Go libraries.\n\n## Running tests\n\n```bash\n./gradlew e2eTest\n./gradlew e2eTest --tests \"com.myapp.e2e.OrderE2ETest\"\n```\n\n## Additional resources\n\n- [gradle-config.md](gradle-config.md) — Source set, e2eTest task, IDE integration, artifact list\n- [system-setup.md](system-setup.md) — All system configuration options\n- [writing-tests.md](writing-tests.md) — Complete test DSL reference with examples\n- [tracing.md](tracing.md) — Tracing plugin options and validation DSL\n- [custom-systems.md](custom-systems.md) — Writing your own Stove system\n- [other-languages.md](other-languages.md) — Testing non-JVM apps (Go, Python, Rust, Node.js)\n- [go-setup.md](go-setup.md) — Go-specific setup (process mode focus): HTTP, PostgreSQL, Kafka bridge, OTel tracing, code coverage\n- [container.md](container.md) — Language-agnostic `containerApp(...)` AUT (image source is the user's responsibility, not Stove's)\n- [mcp.md](mcp.md) — Stove CLI MCP endpoint for agent-driven failed-test triage\n"
  },
  {
    "path": ".claude/skills/stove/container.md",
    "content": "# Container AUT — `stove-container`\n\nUse `stove-container` when the application under test should run as a Docker image. Works for **any language** — Go, Python, Node.js, Rust, .NET, JVM, anything that can ship in a container. Same Stove DSL, same systems, same envMapper / argsMapper model — only the runner changes.\n\nIf you want fast iteration without an image, use `stove-process` (`processApp` / `goApp`). See [other-languages.md](other-languages.md).\n\n## Image source: not Stove's job\n\n`containerApp(...)` only needs an **image reference**. Where that image comes from is up to the user / CI:\n\n- **Pre-built in CI** — most common. CI publishes an image tag (e.g. `ghcr.io/acme/app:sha-abc123`); the test reads it from a system property or env var.\n- **Pulled from a registry** — Testcontainers handles the pull lazily.\n- **Locally built** — optionally wire a Gradle `Exec` task (`docker build`) and `dependsOn` it from your test task. This is one valid path, not a requirement.\n\nLead with the pre-built path. Show local-build as an optional convenience. Never frame \"Stove builds your image\" — Stove launches images, it does not own the build pipeline.\n\n## When to recommend container mode\n\n| Need | Use |\n|------|-----|\n| Fastest local iteration loop | `stove-process` |\n| CI parity with the production image | `stove-container` |\n| Catch glibc/musl, base image, locale, CA cert regressions | `stove-container` |\n| Inner debug loop, breakpoints in IDE | `stove-process` |\n| One repo runs both modes, branched on a system property | Both — single StoveConfig |\n\nA common pattern: `e2eTest` uses process mode for local development; `e2eTest-container` runs container mode in CI before merge using the image CI just built and tagged.\n\n## Setup checklist\n\n```\n- [ ] Step 1: Add stove-container dependency\n- [ ] Step 2: Decide image source (CI artifact, registry pull, or optional local build)\n- [ ] Step 3: Add an e2eTest-container Test task; pass the image tag as a system property\n- [ ] Step 4: Wire containerApp(...) into StoveConfig\n- [ ] Step 5: Pick a networking model (host network or port binding)\n- [ ] Step 6: (Optional) Bind-mount data / coverage directories\n```\n\n## Step 1: Dependency\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$stoveVersion\"))\n    testImplementation(\"com.trendyol:stove-container\")\n    // ... other stove dependencies as needed\n}\n```\n\n## Step 2 + 3: Image source and Gradle wiring\n\nThe default and recommended pattern: CI (or another build step) produces an image tag, and the test task receives it via a system property.\n\n```kotlin title=\"build.gradle.kts\"\nval containerImage = providers.environmentVariable(\"APP_IMAGE\")\n    .orElse(providers.gradleProperty(\"app.image\"))\n    .orElse(\"my-app:local\")           // local fallback only\n\ntasks.register<Test>(\"e2eTest-container\") {\n    useJUnitPlatform()\n    systemProperty(\"app.container.image\", containerImage.get())\n}\n```\n\nIf you also want a Gradle-driven local build (optional), add an `Exec` task and depend on it explicitly:\n\n```kotlin\nval dockerExecutable = providers.environmentVariable(\"DOCKER_EXECUTABLE\").getOrElse(\"docker\")\n\ntasks.register<Exec>(\"buildContainerImage\") {\n    description = \"Optional convenience: builds the AUT image locally.\"\n    commandLine(\n        dockerExecutable, \"build\",\n        \"--file\", projectDir.resolve(\"Dockerfile\").absolutePath,\n        \"--tag\", \"my-app:local\",\n        projectDir.absolutePath\n    )\n    outputs.upToDateWhen { false }\n}\n\n// Only depend on it for the local-build path:\ntasks.named<Test>(\"e2eTest-container-local\") {\n    dependsOn(\"buildContainerImage\")\n    systemProperty(\"app.container.image\", \"my-app:local\")\n}\n```\n\nThe CI path uses the image already produced by the upstream build; the local path opts into building. The Stove test code does not change.\n\n## Step 4: StoveConfig\n\n```kotlin\nimport com.trendyol.stove.container.ContainerTarget\nimport com.trendyol.stove.container.containerApp\nimport com.trendyol.stove.system.application.envMapper\n\ncontainerApp(\n    image = System.getProperty(\"app.container.image\")\n        ?: error(\"app.container.image system property not set\"),\n    target = ContainerTarget.Server(\n        hostPort = 8090,\n        internalPort = 8090,\n        portEnvVar = \"APP_PORT\",\n        bindHostPort = false      // host network → no need to bind\n    ),\n    envProvider = envMapper {\n        \"database.host\" to \"DB_HOST\"\n        \"database.port\" to \"DB_PORT\"\n        \"database.name\" to \"DB_NAME\"\n        \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n        env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:4317\")\n    },\n    configureContainer = {\n        withNetworkMode(\"host\")\n        // bind mounts, log consumers, capabilities — anything Testcontainers exposes\n    },\n    beforeStarted = { configurations ->\n        // optional pre-start hook with resolved configs\n    }\n)\n```\n\n### `containerApp` parameters\n\n| Parameter | Purpose |\n|-----------|---------|\n| `image` | Image reference, e.g. `ghcr.io/acme/app:sha-abc` or `my-app:local` |\n| `target` | `ContainerTarget.Server` or `ContainerTarget.Worker` (carries readiness) |\n| `registry` | Image registry override (defaults to `DEFAULT_REGISTRY`) |\n| `compatibleSubstitute` | Substitute image for arch/OS compatibility |\n| `command` | Override container command (appended with argsMapper output) |\n| `envProvider` | `envMapper { ... }` mapping Stove configs to env vars |\n| `argsProvider` | `argsMapper(prefix, separator) { ... }` for CLI-flag-driven apps |\n| `beforeStarted` | Async hook with resolved configs, runs before container start |\n| `configureContainer` | `GenericContainer<*>.()` — bind mounts, network mode, etc. |\n| `gracefulShutdownTimeout` | Defaults to 5 seconds |\n\n### `ContainerTarget` variants\n\n| Variant | Use case | Default readiness |\n|---------|----------|-------------------|\n| `ContainerTarget.Server(hostPort, internalPort, portEnvVar, bindHostPort)` | HTTP / gRPC / TCP servers | HTTP GET `http://localhost:$hostPort/health` |\n| `ContainerTarget.Worker()` | Kafka consumers, batch jobs | 2-second fixed delay |\n\n## Step 5: Networking strategies\n\n**Host network (Linux only)** — container shares the host network namespace. Reach Postgres / Kafka on `localhost`. Set `bindHostPort = false`:\n\n```kotlin\ntarget = ContainerTarget.Server(hostPort = 8090, internalPort = 8090, portEnvVar = \"APP_PORT\", bindHostPort = false),\nconfigureContainer = { withNetworkMode(\"host\") }\n```\n\n**Port binding (cross-platform)** — Stove binds `hostPort → internalPort`. The app must reach databases / brokers via shared network aliases or `host.docker.internal`:\n\n```kotlin\ntarget = ContainerTarget.Server(hostPort = 8090, internalPort = 8090, portEnvVar = \"APP_PORT\", bindHostPort = true),\nconfigureContainer = { withNetwork(Network.SHARED) }\n```\n\nDocker Desktop on macOS / Windows does **not** support host networking — use port binding there.\n\n## Step 6: Bind mounts (optional)\n\nUse for any data the container or the test needs to share with the host: coverage directories, fixture seeds, read-only configs, etc. Anything Testcontainers exposes is available inside `configureContainer`.\n\n```kotlin\nconfigureContainer = {\n    withFileSystemBind(hostDir, \"/inside/container\")\n    withLogConsumer(Slf4jLogConsumer(LoggerFactory.getLogger(\"app\")))\n}\n```\n\nFor Go integration coverage specifically, see [go-setup.md](go-setup.md#code-coverage).\n\n## Running\n\n```bash\n# CI/registry image — image tag passed in\n./gradlew e2eTest-container -Papp.image=ghcr.io/acme/app:sha-abc123\n# or\nAPP_IMAGE=ghcr.io/acme/app:sha-abc123 ./gradlew e2eTest-container\n\n# Optional local-build path (only if you wired buildContainerImage)\n./gradlew e2eTest-container-local\n```\n\n## Single StoveConfig, both modes\n\nThe recipe pattern: branch on a system property to switch between starters within one config file.\n\n```kotlin\nwhen (resolveAutMode()) {\n    AutMode.Process -> processApp { /* ... */ }\n    AutMode.Container -> containerApp(/* ... */)\n}\n\nprivate fun resolveAutMode(): AutMode =\n    when ((System.getProperty(\"aut.mode\") ?: \"process\").lowercase()) {\n        \"process\" -> AutMode.Process\n        \"container\" -> AutMode.Container\n        else -> error(\"Unsupported aut.mode\")\n    }\n```\n\nDrive the choice from Gradle:\n\n```kotlin\ntasks.register<Test>(\"e2eTest\") { systemProperty(\"aut.mode\", \"process\") /* ... */ }\ntasks.register<Test>(\"e2eTest-container\") { systemProperty(\"aut.mode\", \"container\") /* ... */ }\n```\n\n## Common pitfalls\n\n| Symptom | Cause | Fix |\n|---------|-------|-----|\n| `connection refused` to Postgres / Kafka inside container | Container can't reach Testcontainers on `localhost` | `withNetworkMode(\"host\")` (Linux) or shared network + aliases |\n| Stove never sees `/health` | Wrong port / binding | Confirm `bindHostPort` matches network mode; verify app listens on `internalPort` |\n| `Failed to start container application` | Image missing or unauthorized pull | Verify the image exists locally / in the registry; check `docker images` and registry credentials |\n| Slow inner loop | Image build dominates | Use `stove-process` for daily dev; container mode in CI |\n\n## Reference\n\n- Module source: `starters/container/stove-container/`\n- DSL: `starters/container/stove-container/src/main/kotlin/com/trendyol/stove/container/ContainerDsl.kt`\n- Showcase (process + container in one repo): `recipes/process/golang/go-showcase/`\n- Docs: `docs/other-languages/go-container.md` (Go-specific walkthrough)\n"
  },
  {
    "path": ".claude/skills/stove/custom-systems.md",
    "content": "# Writing Your Own Stove System\n\n## Contents\n- [Implement PluggedSystem](#1-implement-pluggedsystem)\n- [Create a listener](#2-create-a-listener)\n- [Write DSL extensions](#3-write-dsl-extensions)\n- [Register the listener](#4-register-the-listener)\n- [Use in tests](#5-use-in-tests)\n\nComplete working example: `recipes/kotlin-recipes/spring-showcase/src/test-e2e/kotlin/.../setup/DbSchedulerSystem.kt`\n\n## 1. Implement PluggedSystem\n\n```kotlin\nclass DbSchedulerSystem(\n    override val stove: Stove\n) : PluggedSystem,\n    AfterRunAwareWithContext<ApplicationContext>,\n    Reports {\n\n    lateinit var listener: StoveDbSchedulerListener\n    override val reportSystemName: String = \"DbScheduler\"\n\n    override suspend fun afterRun(context: ApplicationContext) {\n        listener = context.getBean()\n    }\n\n    override fun snapshot(): SystemSnapshot = SystemSnapshot(\n        system = reportSystemName,\n        state = mapOf(\"completedExecutions\" to listener.getCompletedExecutionsSnapshot()),\n        summary = \"Completed: ${listener.getCompletedExecutionsSnapshot().size} task(s)\"\n    )\n\n    suspend inline fun <reified T : Any> shouldBeExecuted(\n        atLeastIn: Duration = 5.seconds,\n        noinline condition: T.() -> Boolean\n    ): DbSchedulerSystem = report(\n        action = \"Assert task executed: ${T::class.simpleName}\",\n        expected = \"Task with ${T::class.simpleName} payload executed\".some(),\n        metadata = mapOf(\"timeout\" to atLeastIn.toString())\n    ) {\n        listener.waitUntilObservedSuccessfully(atLeastIn, T::class, condition)\n    }.let { this }\n\n    override fun close() = Unit\n}\n```\n\n### Lifecycle interfaces\n\n| Interface | When Called | Use Case |\n|---|---|---|\n| `PluggedSystem` | Always (required) | Base interface, provides `close()` |\n| `RunAware` | Before app starts | System needs to do setup before the app |\n| `AfterRunAware<T>` | After app starts | Receives the test system instance |\n| `AfterRunAwareWithContext<T>` | After app starts | Receives app DI container (e.g., `ApplicationContext`) |\n| `ExposesConfiguration` | During setup | System exposes config to the application (like containers) |\n| `Reports` | On test failure | Contributes to failure reports via `snapshot()` |\n\n## 2. Create a listener\n\nObserves what happens inside the running application:\n\n```kotlin\nclass StoveDbSchedulerListener : AbstractSchedulerListener() {\n    private val completedExecutions: ConcurrentMap<String, ExecutionComplete> = ConcurrentHashMap()\n\n    override fun onExecutionComplete(executionComplete: ExecutionComplete) {\n        completedExecutions[executionComplete.execution.taskInstance.id] = executionComplete\n    }\n\n    suspend fun <T : Any> waitUntilObservedSuccessfully(\n        atLeastIn: Duration, clazz: KClass<T>, condition: (T) -> Boolean\n    ): Collection<ExecutionComplete> { /* poll until match or timeout */ }\n}\n```\n\n## 3. Write DSL extensions\n\n```kotlin\n// Registration — used in Stove().with { }\n@StoveDsl\nfun WithDsl.dbScheduler(): Stove =\n    this.stove.getOrRegister(DbSchedulerSystem(this.stove)).let { this.stove }\n\n// Validation — used in stove { }\n@StoveDsl\nsuspend fun ValidationDsl.tasks(validation: suspend DbSchedulerSystem.() -> Unit): Unit =\n    validation(this.stove.getOrNone<DbSchedulerSystem>().getOrElse {\n        throw SystemNotRegisteredException(DbSchedulerSystem::class)\n    })\n```\n\n## 4. Register the listener\n\n```kotlin\nStove().with {\n    dbScheduler()\n\n    springBoot(\n        runner = { params ->\n            com.myapp.run(params) {\n                addTestDependencies {\n                    bean<StoveDbSchedulerListener>(isPrimary = true)\n                }\n            }\n        }\n    )\n}.run()\n```\n\n## 5. Use in tests\n\n```kotlin\nstove {\n    http { postAndExpectBody<OrderResponse>(\"/api/orders\", body = request.some()) { /* ... */ } }\n\n    tasks {\n        shouldBeExecuted<OrderEmailPayload> {\n            this.orderId == orderId && this.userId == userId\n        }\n    }\n}\n```\n\n**Pattern**: listener captures events -> system exposes assertions -> DSL extensions make it ergonomic -> `report()` integrates with reporting.\n\n## 6. Extending built-in systems\n\nAdd domain-specific helpers to existing systems without creating new ones:\n\n```kotlin\n@StoveDsl\nsuspend fun KafkaSystem.publishWithCorrelationId(\n    topic: String,\n    message: Any,\n    correlationId: String = UUID.randomUUID().toString()\n) {\n    publish(topic, message, headers = mapOf(\"X-Correlation-ID\" to correlationId))\n}\n\n// Usage\nkafka { publishWithCorrelationId(\"orders\", event, \"corr-123\") }\n```\n"
  },
  {
    "path": ".claude/skills/stove/go-setup.md",
    "content": "# Go Application Setup with Stove\n\nComplete guide for testing Go applications with Stove. Covers HTTP, PostgreSQL, Kafka (with bridge), OpenTelemetry tracing, dashboard, MCP triage, and integration coverage.\n\nThis skill focuses on **process mode** (`stove-process` / `goApp`) — fastest local iteration. For container-image AUT (`stove-container` / `containerApp`) — language-agnostic, image source is your responsibility — see [container.md](container.md). For agent-driven failure triage via the `stove` CLI MCP endpoint, see [mcp.md](mcp.md).\n\nThe same `StoveConfig.kt` can serve both modes by branching on a system property like `-Daut.mode=process|container` (see the showcase recipe).\n\n## Setup Checklist\n\n```\n- [ ] Step 1: Create Go app with env var config + health endpoint + SIGTERM handling\n- [ ] Step 2: Add OpenTelemetry instrumentation (otelhttp, otelsql)\n- [ ] Step 3: Add Kafka with Stove bridge interceptors (optional)\n- [ ] Step 4: Add stove-process dependency (provides goApp() DSL)\n- [ ] Step 5: Create test-e2e source set + StoveConfig\n- [ ] Step 6: Configure Gradle build (go build + e2eTest)\n- [ ] Step 7: Write tests\n```\n\n## Step 1: Go app requirements\n\nThe Go app must:\n- Read config from **environment variables**\n- Expose **GET /health** returning 200\n- Handle **SIGTERM** for graceful shutdown\n\nKey env vars:\n\n| Variable | Purpose |\n|----------|---------|\n| `APP_PORT` | HTTP listen port |\n| `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS` | PostgreSQL |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP gRPC endpoint |\n| `KAFKA_BROKERS` | Kafka broker addresses |\n| `KAFKA_LIBRARY` | Kafka client: `sarama`, `franz`, or `segmentio` (default: `sarama`) |\n| `STOVE_KAFKA_BRIDGE_PORT` | Stove bridge gRPC port (test-only) |\n| `GOCOVERDIR` | Directory for Go integration test coverage data (test-only) |\n\n## Step 2: OpenTelemetry\n\n```go\n// HTTP: wrap mux with otelhttp\nhandler := otelhttp.NewHandler(mux, \"http.request\")\n\n// DB: use otelsql instead of database/sql\ndb, _ := otelsql.Open(\"postgres\", connStr, otelsql.WithAttributes(semconv.DBSystemPostgreSQL))\n\n// Tracing: use WithSyncer for tests (not WithBatcher)\ntp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter), ...)\n\n// Propagation: must set W3C TraceContext for Stove trace correlation\notel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(\n    propagation.TraceContext{}, propagation.Baggage{},\n))\n```\n\n## Step 3: Kafka bridge\n\nThe Stove Kafka bridge library lives at `go/stove-kafka/`. It has a library-agnostic core and client-specific subpackages.\n\n### Architecture\n\n```\ngo/stove-kafka/\n  bridge.go           # Core: Bridge, PublishedMessage, ConsumedMessage (library-agnostic)\n  sarama/             # IBM/sarama interceptors\n    interceptors.go\n  franz/              # twmb/franz-go hooks\n    hooks.go\n  segmentio/            # segmentio/kafka-go helpers\n    bridge.go\n  stoveobserver/      # Generated gRPC code\n```\n\n### Add the dependency\n\n```bash\ngo get github.com/trendyol/stove/go/stove-kafka\n```\n\n### Initialize bridge + wire into your Kafka client\n\n**IBM/sarama:**\n\n```go\nimport (\n    stovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n    stovesarama \"github.com/trendyol/stove/go/stove-kafka/sarama\"\n)\n\nbridge, _ := stovekafka.NewBridgeFromEnv()\ndefer bridge.Close()\n\nconfig := sarama.NewConfig()\nconfig.Producer.Interceptors = []sarama.ProducerInterceptor{\n    &stovesarama.ProducerInterceptor{Bridge: bridge},\n}\nconfig.Consumer.Interceptors = []sarama.ConsumerInterceptor{\n    &stovesarama.ConsumerInterceptor{Bridge: bridge},\n}\n```\n\n**twmb/franz-go:**\n\n```go\nimport (\n    stovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n    \"github.com/trendyol/stove/go/stove-kafka/franz\"\n)\n\nbridge, _ := stovekafka.NewBridgeFromEnv()\ndefer bridge.Close()\n\nclient, _ := kgo.NewClient(\n    kgo.SeedBrokers(\"localhost:9092\"),\n    kgo.WithHooks(&franz.Hook{Bridge: bridge}),\n)\n```\n\n**segmentio/kafka-go:**\n\n```go\nimport (\n    stovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n    \"github.com/trendyol/stove/go/stove-kafka/segmentio\"\n)\n\nbridge, _ := stovekafka.NewBridgeFromEnv()\ndefer bridge.Close()\n\n// After producing\n_ = writer.WriteMessages(ctx, msgs...)\nsegmentio.ReportWritten(ctx, bridge, msgs...)\n\n// After consuming\nmsg, _ := reader.ReadMessage(ctx)\nsegmentio.ReportRead(ctx, bridge, msg)\n```\n\n### Other libraries (e.g. confluent-kafka-go)\n\nThe core bridge has no Kafka client dependency. For any unsupported library, use the core types directly:\n\n```go\nimport stovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n\n_ = bridge.ReportPublished(ctx, &stovekafka.PublishedMessage{\n    Topic: msg.Topic, Key: string(msg.Key), Value: msg.Value, Headers: myHeaders(msg),\n})\n_ = bridge.ReportConsumed(ctx, &stovekafka.ConsumedMessage{\n    Topic: msg.Topic, Key: string(msg.Key), Value: msg.Value,\n    Partition: msg.Partition, Offset: msg.Offset, Headers: myHeaders(msg),\n})\n_ = bridge.ReportCommitted(ctx, msg.Topic, msg.Partition, msg.Offset+1)\n```\n\n### How it works\n\n- All subpackages convert client-specific types to core `PublishedMessage`/`ConsumedMessage` and call bridge methods\n- Consumer interceptors/helpers pre-report commit at `offset+1` (needed for `shouldBeConsumed`)\n- All Bridge methods are nil-safe: `(*Bridge)(nil).ReportPublished(...)` is a no-op\n- All interceptors/hooks/helpers check for nil bridge first — zero overhead in production\n\n### Test-friendly Kafka settings (Go side)\n\nWhen running against Testcontainers (Stove e2e tests), configure Kafka clients for **fast feedback**. Default production settings (large batches, long commit intervals, no auto-topic creation) cause timeouts, missed messages, and flaky tests.\n\n**Key principles:**\n\n1. **Auto-create topics** — test containers may not have topics pre-created; without this, produces fail silently or block\n2. **Small batch size / low batch timeout** — flush produces immediately so `shouldBePublished` sees them\n3. **Short auto-commit interval** — make consumed offsets visible to Stove bridge quickly so `shouldBeConsumed` passes\n4. **Unique consumer groups per test run** — prevent offset carryover between runs (e.g. `\"myapp-\" + library`)\n\n**IBM/sarama:**\n\n```go\nconfig := sarama.NewConfig()\nconfig.Producer.Return.Successes = true\nconfig.Consumer.Offsets.Initial = sarama.OffsetOldest\nconfig.Consumer.Offsets.AutoCommit.Interval = 100 * time.Millisecond\n// sarama relies on broker-side auto.create.topics.enable (no client-side setting)\n```\n\n**twmb/franz-go:**\n\n```go\nclient, _ := kgo.NewClient(\n    kgo.SeedBrokers(brokerList...),\n    kgo.AllowAutoTopicCreation(),                    // client-side topic creation\n    kgo.AutoCommitInterval(100 * time.Millisecond),  // fast offset commits\n    kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),\n    kgo.WithHooks(&franz.Hook{Bridge: bridge}),\n)\n```\n\n**segmentio/kafka-go:**\n\n```go\n// Writer — flush immediately, auto-create topics\nwriter := &kafka.Writer{\n    Addr:                   kafka.TCP(brokerList...),\n    BatchSize:              1,\n    BatchTimeout:           10 * time.Millisecond,\n    RequiredAcks:           kafka.RequireAll,\n    AllowAutoTopicCreation: true,\n}\n\n// Reader — fast commits, low wait\nreader := kafka.NewReader(kafka.ReaderConfig{\n    Brokers:        brokerList,\n    GroupID:         groupID,\n    Topic:           topic,\n    MinBytes:        1,\n    MaxBytes:        10e6,\n    CommitInterval:  100 * time.Millisecond,\n    MaxWait:         500 * time.Millisecond,\n})\n```\n\n**franz-go: separate producer and consumer clients.** Using a single `kgo.Client` for both produce and consume causes consumer group coordination to block `ProduceSync`, leading to 10-30s delays. Always create two clients:\n\n```go\n// Producer — no consumer group overhead\nproducerClient, _ := kgo.NewClient(\n    kgo.SeedBrokers(brokerList...),\n    kgo.AllowAutoTopicCreation(),\n    kgo.WithHooks(hook),\n)\n\n// Consumer — consumer group coordination won't block produces\nconsumerClient, _ := kgo.NewClient(\n    kgo.SeedBrokers(brokerList...),\n    kgo.ConsumeTopics(topic),\n    kgo.ConsumerGroup(groupID),\n    kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),\n    kgo.AutoCommitInterval(100 * time.Millisecond),\n    kgo.AllowAutoTopicCreation(),\n    kgo.WithHooks(hook),\n)\n```\n\n**Common pitfall — consumer group offset carryover:** If running the same tests against multiple Kafka libraries sequentially (e.g. sarama → franz → segmentio), use a unique consumer group per library. Otherwise the second run sees committed offsets from the first and skips messages:\n\n```go\ngroupID := \"myapp-\" + library  // e.g. \"myapp-sarama\", \"myapp-franz\"\n```\n\n## Step 4: Add stove-process dependency\n\nThe `stove-process` module provides `goApp()` out of the box — no custom `ApplicationUnderTest` needed. It supports passing configs as environment variables (`envMapper`) or CLI arguments (`argsMapper`). Go apps typically use env vars.\n\n```kotlin\ndependencies {\n    testImplementation(stoveLibs.stoveProcess) // or \"com.trendyol:stove-process\"\n}\n```\n\nSource: `starters/process/stove-process/`\n\n## Step 5: StoveConfig\n\n```kotlin\nStove().with {\n    httpClient { HttpClientSystemOptions(baseUrl = \"http://localhost:$APP_PORT\") }\n    dashboard { DashboardSystemOptions(appName = \"go-showcase\") }\n    tracing { enableSpanReceiver(port = OTLP_PORT) }\n\n    kafka {\n        KafkaSystemOptions(\n            configureExposedConfiguration = { cfg ->\n                listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n            }\n        )\n    }\n\n    postgresql {\n        PostgresqlOptions(\n            databaseName = \"stove\",\n            configureExposedConfiguration = { cfg ->\n                listOf(\n                    \"database.host=${cfg.host}\", \"database.port=${cfg.port}\",\n                    \"database.name=stove\",\n                    \"database.username=${cfg.username}\", \"database.password=${cfg.password}\"\n                )\n            }\n        ).migrations { register<ProductMigration>() }\n    }\n\n    goApp(\n        target = ProcessTarget.Server(port = APP_PORT, portEnvVar = \"APP_PORT\"),\n        envProvider = envMapper {\n            \"database.host\" to \"DB_HOST\"\n            \"database.port\" to \"DB_PORT\"\n            \"database.name\" to \"DB_NAME\"\n            \"database.username\" to \"DB_USER\"\n            \"database.password\" to \"DB_PASS\"\n            \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n            env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:$OTLP_PORT\")\n            env(\"KAFKA_LIBRARY\") { System.getProperty(\"kafka.library\") ?: \"sarama\" }\n            env(\"STOVE_KAFKA_BRIDGE_PORT\", stoveKafkaBridgePortDefault)\n        }\n    )\n}.run()\n```\n\n## Step 6: Gradle\n\n```kotlin\nval goBinary = layout.buildDirectory.file(\"go-app\").get().asFile\n\ntasks.register<Exec>(\"buildGoApp\") {\n    commandLine(\"go\", \"build\", \"-o\", goBinary.absolutePath, \".\")\n    inputs.files(fileTree(\".\") { include(\"*.go\", \"go.mod\", \"go.sum\") })\n    outputs.file(goBinary)\n}\n\n// Per-library e2e test tasks — each passes KAFKA_LIBRARY to the Go app\nval kafkaLibraries = listOf(\"sarama\", \"franz\", \"segmentio\")\nval kafkaE2eTasks = kafkaLibraries.mapIndexed { index, lib ->\n    tasks.register<Test>(\"e2eTest_$lib\") {\n        dependsOn(\"buildGoApp\")\n        systemProperty(\"go.app.binary\", goBinary.absolutePath)\n        systemProperty(\"kafka.library\", lib)\n        if (index > 0) mustRunAfter(\"e2eTest_${kafkaLibraries[index - 1]}\")\n    }\n}\ntasks.named<Test>(\"e2eTest\") { dependsOn(kafkaE2eTasks); enabled = false }\n\ndependencies {\n    testImplementation(stoveLibs.stove)\n    testImplementation(stoveLibs.stoveProcess)\n    testImplementation(stoveLibs.stovePostgres)\n    testImplementation(stoveLibs.stoveHttp)\n    testImplementation(stoveLibs.stoveTracing)\n    testImplementation(stoveLibs.stoveDashboard)\n    testImplementation(stoveLibs.stoveKafka)\n    testImplementation(stoveLibs.stoveExtensionsKotest)\n}\n```\n\n## Step 7: Write tests\n\n```kotlin\nclass GoShowcaseTest : FunSpec({\n    test(\"create product, verify DB + Kafka + traces\") {\n        stove {\n            var productId: String? = null\n\n            http {\n                postAndExpectBody<ProductResponse>(\n                    uri = \"/api/products\",\n                    body = CreateProductRequest(name = \"Test\", price = 42.99).some()\n                ) { actual ->\n                    actual.status shouldBe 201\n                    productId = actual.body().id\n                }\n            }\n\n            postgresql {\n                shouldQuery<ProductRow>(\n                    query = \"SELECT id, name, price FROM products WHERE id = '$productId'\",\n                    mapper = { row -> ProductRow(row.string(\"id\"), row.string(\"name\"), row.double(\"price\")) }\n                ) { rows -> rows.size shouldBe 1 }\n            }\n\n            kafka {\n                shouldBePublished<ProductCreatedEvent>(10.seconds) {\n                    actual.name == \"Test\"\n                }\n            }\n\n            tracing {\n                waitForSpans(4, 5000)\n                shouldContainSpan(\"http.request\")\n                shouldNotHaveFailedSpans()\n            }\n        }\n    }\n\n    test(\"consume Kafka events\") {\n        stove {\n            var productId: String? = null\n\n            http {\n                postAndExpectBody<ProductResponse>(\n                    uri = \"/api/products\",\n                    body = CreateProductRequest(name = \"Original\", price = 10.0).some()\n                ) { actual -> productId = actual.body().id }\n            }\n\n            kafka {\n                publish(\"product.update\", ProductUpdateEvent(id = productId!!, name = \"Updated\", price = 99.99))\n                shouldBeConsumed<ProductUpdateEvent>(10.seconds) {\n                    actual.id == productId && actual.name == \"Updated\"\n                }\n            }\n\n            postgresql {\n                shouldQuery<ProductRow>(\n                    query = \"SELECT id, name, price FROM products WHERE id = '$productId'\",\n                    mapper = { row -> ProductRow(row.string(\"id\"), row.string(\"name\"), row.double(\"price\")) }\n                ) { rows -> rows.first().name shouldBe \"Updated\" }\n            }\n        }\n    }\n})\n```\n\n## Code Coverage\n\nGo 1.20+ supports integration test coverage for binaries not run via `go test`. Build with `go build -cover`, set `GOCOVERDIR`, and coverage data is written on graceful shutdown — fits perfectly with Stove's lifecycle.\n\n### Gradle setup\n\nEnable with `-Pgo.coverage=true`:\n\n```kotlin\nval coverageEnabled = providers.gradleProperty(\"go.coverage\")\n    .map { it.toBoolean() }.getOrElse(false)\nval goCoverDirPath = layout.buildDirectory.dir(\"go-coverage\").get().asFile.absolutePath\n\n// Build with -cover when enabled\ntasks.register<Exec>(\"buildGoApp\") {\n    val args = mutableListOf(\"go\", \"build\")\n    if (coverageEnabled) args.add(\"-cover\")\n    args.addAll(listOf(\"-o\", goBinary.absolutePath, \".\"))\n    commandLine(args)\n}\n\n// Pass GOCOVERDIR to test JVM, disable build cache for coverage runs\ntasks.register<Test>(\"e2eTest_sarama\") {\n    if (coverageEnabled) {\n        systemProperty(\"go.cover.dir\", goCoverDirPath)\n        outputs.cacheIf { false }  // Coverage data is a side effect\n    }\n}\n\n// Coverage report tasks (register only when coverage is enabled)\nif (coverageEnabled) {\n    tasks.register<Exec>(\"goCoverageReport\") {\n        mustRunAfter(kafkaE2eTasks)\n        commandLine(\"go\", \"tool\", \"covdata\", \"textfmt\", \"-i=$goCoverDirPath\", \"-o=$goCoverOutPath\")\n    }\n    tasks.register<Exec>(\"goCoverageSummary\") { dependsOn(\"goCoverageReport\"); /* go tool cover -func */ }\n    tasks.register<Exec>(\"goCoverageHtml\") { dependsOn(\"goCoverageReport\"); /* go tool cover -html */ }\n    tasks.register(\"e2eTestWithCoverage\") {\n        dependsOn(kafkaE2eTasks)\n        finalizedBy(\"goCoverageSummary\", \"goCoverageHtml\")\n    }\n}\n```\n\n### StoveConfig\n\nPass `GOCOVERDIR` via `envMapper` — empty when disabled, Go ignores it:\n\n```kotlin\nenv(\"GOCOVERDIR\") {\n    System.getProperty(\"go.cover.dir\")?.also { java.io.File(it).mkdirs() } ?: \"\"\n}\n```\n\n### SIGPIPE handling\n\nWhen Go runs under Java's `ProcessBuilder`, stdout pipe can close before process exit. Log writes trigger SIGPIPE (exit 141), killing the process before coverage flush. Fix:\n\n```go\nfunc main() {\n    signal.Ignore(syscall.SIGPIPE) // Ensures clean shutdown + coverage flush\n    // ...\n}\n```\n\n### Running with coverage\n\n```bash\n./gradlew e2eTestWithCoverage -Pgo.coverage=true\n# Output: per-function coverage + HTML report at build/go-coverage/coverage.html\n```\n\n## Running\n\n```bash\n# From the go-showcase directory — runs all three Kafka libraries\ncd recipes/process/golang/go-showcase\n./gradlew e2eTest\n\n# Run a specific library only\n./gradlew e2eTest_sarama\n./gradlew e2eTest_franz\n./gradlew e2eTest_segmentio\n\n# With Go code coverage\n./gradlew e2eTestWithCoverage -Pgo.coverage=true\n```\n\n## Go dependencies\n\n```\ngithub.com/trendyol/stove/go/stove-kafka                        # Stove Kafka bridge (core)\ngithub.com/trendyol/stove/go/stove-kafka/sarama                 # IBM/sarama interceptors\ngithub.com/trendyol/stove/go/stove-kafka/franz                  # twmb/franz-go hooks\ngithub.com/trendyol/stove/go/stove-kafka/segmentio                # segmentio/kafka-go helpers\ngithub.com/XSAM/otelsql                                         # database/sql instrumentation\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp    # HTTP instrumentation\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc # OTLP exporter\ngoogle.golang.org/grpc                                           # gRPC\n```\n\n## Reference\n\n- Process module (goApp DSL): `starters/process/stove-process/`\n- Container module (containerApp DSL): `starters/container/stove-container/`\n- Full working example (process + container in one repo): `recipes/process/golang/go-showcase/`\n- Bridge library source: `go/stove-kafka/`\n- Docs:\n  - `docs/other-languages/go.md` — overview / mode picker\n  - `docs/other-languages/go-process.md` — process mode walkthrough\n  - `docs/other-languages/go-container.md` — container mode walkthrough\n- Sibling skills:\n  - [container.md](container.md) — language-agnostic container AUT\n  - [mcp.md](mcp.md) — MCP triage on failed runs\n  - [other-languages.md](other-languages.md) — non-JVM overview\n"
  },
  {
    "path": ".claude/skills/stove/gradle-config.md",
    "content": "# Gradle Configuration\n\n## Contents\n- [Dependencies (BOM)](#dependencies-bom)\n- [Register test-e2e source set](#register-test-e2e-source-set)\n- [Register e2eTest task](#register-e2etest-task)\n- [IDE integration](#ide-integration)\n- [JUnit base test class](#junit-base-test-class)\n- [Available artifacts](#available-artifacts)\n\n## Dependencies (BOM)\n\nStove e2e tests are Kotlin-first. Even for Java/Scala projects, keep e2e test sources in `src/test-e2e/kotlin`.\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$stoveVersion\"))\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-spring\")             // or stove-ktor / stove-quarkus / stove-micronaut\n    testImplementation(\"com.trendyol:stove-extensions-kotest\")  // or stove-extensions-junit\n\n    // Add only what you need:\n    testImplementation(\"com.trendyol:stove-http\")\n    testImplementation(\"com.trendyol:stove-postgres\")\n    testImplementation(\"com.trendyol:stove-mysql\")\n    testImplementation(\"com.trendyol:stove-mssql\")\n    testImplementation(\"com.trendyol:stove-cassandra\")\n    testImplementation(\"com.trendyol:stove-mongodb\")\n    testImplementation(\"com.trendyol:stove-redis\")\n    testImplementation(\"com.trendyol:stove-elasticsearch\")\n    testImplementation(\"com.trendyol:stove-couchbase\")\n    testImplementation(\"com.trendyol:stove-kafka\")              // standalone Kafka assertions\n    testImplementation(\"com.trendyol:stove-spring-kafka\")       // Spring Kafka assertions + interceptor\n    testImplementation(\"com.trendyol:stove-wiremock\")\n    testImplementation(\"com.trendyol:stove-grpc\")\n    testImplementation(\"com.trendyol:stove-grpc-mock\")\n    testImplementation(\"com.trendyol:stove-tracing\")\n    testImplementation(\"com.trendyol:stove-dashboard\")\n    testImplementation(\"com.trendyol:stove-process\")            // non-JVM process AUT\n    testImplementation(\"com.trendyol:stove-container\")          // non-JVM container AUT\n}\n```\n\n## Register test-e2e source set\n\n```kotlin\nsourceSets {\n    @Suppress(\"LocalVariableName\")\n    val `test-e2e` by creating {\n        compileClasspath += sourceSets.main.get().output\n        runtimeClasspath += sourceSets.main.get().output\n    }\n\n    val testE2eImplementation by configurations.getting {\n        extendsFrom(configurations.testImplementation.get())\n    }\n    configurations[\"testE2eRuntimeOnly\"].extendsFrom(configurations.runtimeOnly.get())\n}\n```\n\n## Register e2eTest task\n\n```kotlin\ntasks.register<Test>(\"e2eTest\") {\n    description = \"Runs e2e tests.\"\n    group = \"verification\"\n    testClassesDirs = sourceSets[\"test-e2e\"].output.classesDirs\n    classpath = sourceSets[\"test-e2e\"].runtimeClasspath\n\n    useJUnitPlatform()\n    reports {\n        junitXml.required.set(true)\n        html.required.set(true)\n    }\n}\n```\n\n## IDE integration\n\n```kotlin\nidea {\n    module {\n        testSources.from(sourceSets[\"test-e2e\"].allSource.sourceDirectories)\n        testResources.from(sourceSets[\"test-e2e\"].resources.sourceDirectories)\n    }\n}\n```\n\n## Resolve API ambiguity from local artifacts\n\nWhen API names/signatures are unclear, inspect locally downloaded Stove artifacts instead of guessing.\n\n```bash\n# Find Stove artifacts in Gradle cache\nfind ~/.gradle/caches/modules-2/files-2.1 -path \"*com.trendyol/stove-*/*/*.jar\" | head -n 20\n\n# Find Stove artifacts in Maven local repo\nfind ~/.m2/repository/com/trendyol -name \"stove-*.jar\" | head -n 20\n\n# List classes to locate exact type names\njar tf ~/.m2/repository/com/trendyol/stove-spring-kafka/<version>/stove-spring-kafka-<version>.jar | rg \"TestSystem|Kafka\"\n\n# Inspect method signatures quickly\njavap -classpath ~/.m2/repository/com/trendyol/stove-spring-kafka/<version>/stove-spring-kafka-<version>.jar \\\n  com.trendyol.stove.kafka.TestSystemKafkaInterceptor\n```\n\nPrefer `*-sources.jar` when available for more accurate reading of function names, generic types, and usage patterns.\n\n## JUnit base test class\n\nUse this instead of `AbstractProjectConfig` when using JUnit:\n\n```kotlin\n@ExtendWith(StoveJUnitExtension::class)\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\nabstract class BaseE2ETest {\n    companion object {\n        @JvmStatic @BeforeAll\n        fun setup() = runBlocking {\n            Stove().with { /* systems */ }.run()\n        }\n\n        @JvmStatic @AfterAll\n        fun teardown() = runBlocking { Stove.stop() }\n    }\n}\n```\n\n## Available artifacts\n\n| Artifact | Description |\n|---|---|\n| `stove` | Core framework |\n| `stove-spring` | Spring Boot starter |\n| `stove-ktor` | Ktor starter |\n| `stove-quarkus` | Quarkus starter |\n| `stove-micronaut` | Micronaut starter |\n| `stove-http` | HTTP client system |\n| `stove-postgres` | PostgreSQL system |\n| `stove-mysql` | MySQL system |\n| `stove-mssql` | MSSQL system |\n| `stove-cassandra` | Cassandra system |\n| `stove-mongodb` | MongoDB system |\n| `stove-redis` | Redis system |\n| `stove-elasticsearch` | Elasticsearch system |\n| `stove-couchbase` | Couchbase system |\n| `stove-kafka` | Standalone Kafka system |\n| `stove-spring-kafka` | Spring Kafka (adds `shouldBeConsumed`, `shouldBeFailed`, `shouldBeRetried`) |\n| `stove-wiremock` | WireMock system |\n| `stove-grpc` | gRPC client system |\n| `stove-grpc-mock` | gRPC mock server system |\n| `stove-tracing` | Tracing system |\n| `stove-dashboard` | Dashboard system (streams events to stove CLI) |\n| `stove-process` | Process-based AUT starter (`processApp`, `goApp`) |\n| `stove-container` | Container-based AUT starter (`containerApp`) |\n| `stove-extensions-kotest` | Kotest reporting integration |\n| `stove-extensions-junit` | JUnit reporting integration |\n"
  },
  {
    "path": ".claude/skills/stove/mcp.md",
    "content": "# Stove MCP — Agent Triage\n\nThe Stove CLI exposes a local **Model Context Protocol** endpoint at `http://localhost:4040/mcp`. Agents use it to inspect failed end-to-end tests through compact, structured tools instead of loading raw logs into context.\n\nUse MCP as an optimization, not a dependency. If MCP is unavailable, fall back to normal test output, Stove failure reports, and logs.\n\n## When to use this skill\n\n- The user is testing with Stove and a recent run has failures\n- The user mentions \"MCP\", \"stove failures\", or asks for triage of a Stove run\n- An agent task instruction says to prefer the local Stove MCP endpoint\n\n## Discovery\n\nWhen `stove` is running, the startup banner prints the endpoint:\n\n```text\nStove CLI v0.24.0 running\nUI:   http://localhost:4040\nREST: http://localhost:4040/api/v1\nMCP:  http://localhost:4040/mcp\ngRPC: localhost:4041\n```\n\nOr query metadata:\n\n```bash\ncurl -s http://localhost:4040/api/v1/meta\n```\n\n```json\n{\n  \"stove_cli_version\": \"0.24.0\",\n  \"mcp\": {\n    \"enabled\": true,\n    \"transport\": \"streamable-http\",\n    \"endpoint\": \"http://localhost:4040/mcp\",\n    \"scope\": \"read-only-test-observability\"\n  }\n}\n```\n\n## MCP client config (generic)\n\n```json\n{\n  \"mcpServers\": {\n    \"stove\": {\n      \"transport\": \"streamable-http\",\n      \"url\": \"http://localhost:4040/mcp\"\n    }\n  }\n}\n```\n\nExact keys vary by agent runtime. The endpoint URL is the load-bearing value.\n\n## Agent Workflow (the only correct order)\n\n1. Call `stove_failures` first.\n2. Pick a specific `run_id` and `test_id` from the result. **Never infer a test selector from names alone** — multiple apps and runs can contain duplicate test names.\n3. Call `stove_failure_detail` with that exact `run_id + test_id` for the compact failure packet.\n4. Drill into `stove_timeline`, `stove_trace`, or `stove_snapshot` only when needed.\n5. Use `stove_raw_evidence` for one specific entry / span / snapshot when the compact view isn't enough.\n6. If MCP is missing data, fall back to normal test output and logs.\n\nEvery failure result includes ready-to-use next tool calls — use them, don't guess.\n\n## Data hierarchy\n\n```\ndatabase\n  -> apps by app_name\n    -> runs by run_id\n      -> tests by test_id\n        -> entries, spans, snapshots\n```\n\n`app_name` is the label set in `DashboardSystemOptions(appName = \"...\")` on the test side. `run_id + test_id` is the only authoritative selector.\n\n## Tools\n\n| Tool | Purpose |\n|------|---------|\n| `stove_apps` | Apps recorded in the dashboard database |\n| `stove_runs` | Runs, filterable by app and status |\n| `stove_failures` | Default entrypoint — failed tests grouped by app and run |\n| `stove_failure_detail` | Compact detail for one exact failed test |\n| `stove_timeline` | Ordered test actions, failure-focused by default |\n| `stove_trace` | Critical path and exception evidence from correlated spans |\n| `stove_snapshot` | System snapshot summaries with targeted JSON drill-down |\n| `stove_raw_evidence` | Capped raw lookup for one entry, span, or snapshot |\n\n## Token Budgeting\n\nTools default to compact output. Large payloads are truncated deterministically and include omitted counts or follow-up tool calls. Sensitive keys (`authorization`, `cookie`, `password`, `secret`, `token`, `apiKey`, `credential`) are redacted before return.\n\nUse `budget` to dial detail:\n\n```json\n{ \"budget\": \"tiny\" }   // tiny | compact | full\n```\n\nTools that expose raw evidence also accept `max_chars`.\n\n## Security\n\n- **Read-only**: no tools to clear data, retry tests, delete runs, or mutate snapshots.\n- **Local-only**: `/mcp` accepts loopback clients and localhost `Host`/`Origin` headers. Non-local hosts are rejected (mitigates DNS rebinding).\n- Safe to run on a dev machine; do not expose externally.\n\n## Troubleshooting\n\nIf MCP is unreachable:\n\n- confirm `stove` is running (`brew install Trendyol/trendyol-tap/stove` then `stove`)\n- check the startup banner for the actual port (some installs use a custom one)\n- open `http://localhost:4040/api/v1/meta` and verify `mcp.enabled` is `true`\n- make sure the agent runtime is configured with `http://localhost:4040/mcp`\n- fall back to normal test output and logs if the endpoint cannot be reached\n\nIf MCP returns no failures:\n\n- the latest recorded runs may have passed\n- the test config may not register `stove-dashboard` (no data is being recorded)\n- the test run may still be in progress\n\n## Recommended agent instruction\n\nAdd to your project's agent rules / system prompt:\n\n```text\nWhen Stove is running, prefer the local Stove MCP endpoint for failed-test triage.\nStart with stove_failures, then use the returned run_id + test_id with\nstove_failure_detail. Drill into stove_timeline, stove_trace, or stove_snapshot\nonly when needed. If MCP is unavailable, ambiguous, or incomplete, fall back to\nnormal test output, Stove reports, and logs.\n```\n\n## Reference\n\n- Component docs: `docs/Components/21-mcp.md`\n- Dashboard component (data source): `docs/Components/18-dashboard.md`\n"
  },
  {
    "path": ".claude/skills/stove/other-languages.md",
    "content": "# Testing Non-JVM Applications with Stove\n\nStove can test any application that speaks HTTP, databases, and messaging --- regardless of the language. Two starters:\n\n- **`stove-process`** — host binary, fastest iteration loop (`processApp` / `goApp`)\n- **`stove-container`** — Docker image, CI parity with the production artifact (`containerApp`). See [container.md](container.md) for the full container guide.\n\nSame Stove DSL, same systems, same env/args mapping. The only difference is *how* the AUT starts.\n\nFor Stove + AI agent triage on failed runs, see [mcp.md](mcp.md).\n\n## Requirements\n\nYour application must:\n\n1. **Accept configuration** --- via environment variables, CLI arguments, or both\n2. **Handle SIGTERM** --- for clean test teardown\n3. **Optional: expose a readiness endpoint** --- HTTP health check, TCP port, or custom probe\n\n## Setup Checklist\n\n```\n- [ ] Step 1: Add `stove-process` or `stove-container` dependency\n- [ ] Step 2: Create test-e2e source set layout\n- [ ] Step 3: Configure Gradle (build app + e2eTest task)\n- [ ] Step 4: Create StoveConfig with systems + processApp/goApp\n- [ ] Step 5: Instrument app with OpenTelemetry (optional)\n- [ ] Step 6: Add Kafka bridge (optional, Go only for now)\n- [ ] Step 7: Write tests using stove {} DSL\n```\n\n## Step 1: Add dependency\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$stoveVersion\"))\n    testImplementation(\"com.trendyol:stove-process\")\n    testImplementation(\"com.trendyol:stove-container\") // if AUT runs as Docker image\n    // ... other stove dependencies as needed\n}\n```\n\n## Step 2-3: Project structure, Gradle\n\nSame as JVM setup (see SKILL.md). Build your app binary before tests:\n\n```kotlin\nval appSourceDir = project.file(\"my-app\")\nval appBinary = project.layout.buildDirectory.file(\"my-app\").get().asFile\n\ntasks.register<Exec>(\"buildApp\") {\n    workingDir = appSourceDir\n    commandLine(\"go\", \"build\", \"-o\", appBinary.absolutePath, \".\")  // or npm, cargo, etc.\n    inputs.files(fileTree(appSourceDir) { include(\"*.go\", \"go.mod\", \"go.sum\") })\n    outputs.file(appBinary)\n}\n\ntasks.named<Test>(\"e2eTest\") {\n    dependsOn(\"buildApp\")\n    systemProperty(\"app.binary\", appBinary.absolutePath)\n}\n```\n\n## Step 4: StoveConfig with processApp / goApp / containerApp\n\nUse `processApp()` for any language binary, `goApp()` as a Go convenience, or `containerApp()` when tests should launch an image directly.\n\n```kotlin\nStove().with {\n    httpClient { HttpClientSystemOptions(baseUrl = \"http://localhost:$APP_PORT\") }\n    tracing { enableSpanReceiver(port = OTLP_PORT) }\n    dashboard { DashboardSystemOptions(appName = \"my-app\") }\n\n    postgresql {\n        PostgresqlOptions(\n            databaseName = \"mydb\",\n            configureExposedConfiguration = { cfg ->\n                listOf(\n                    \"database.host=${cfg.host}\",\n                    \"database.port=${cfg.port}\",\n                    \"database.name=mydb\",\n                    \"database.username=${cfg.username}\",\n                    \"database.password=${cfg.password}\"\n                )\n            }\n        ).migrations { register<SchemaMigration>() }\n    }\n\n    kafka {\n        KafkaSystemOptions(\n            configureExposedConfiguration = { cfg ->\n                listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n            }\n        )\n    }\n\n    // For Go apps — uses go.app.binary system property by default\n    goApp(\n        target = ProcessTarget.Server(port = APP_PORT, portEnvVar = \"APP_PORT\"),\n        envProvider = envMapper {\n            \"database.host\" to \"DB_HOST\"\n            \"database.port\" to \"DB_PORT\"\n            \"database.name\" to \"DB_NAME\"\n            \"database.username\" to \"DB_USER\"\n            \"database.password\" to \"DB_PASS\"\n            \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n            env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:$OTLP_PORT\")\n        }\n    )\n\n    // For any other language — specify the full command\n    // processApp {\n    //     ProcessApplicationOptions(\n    //         command = listOf(\"python3\", \"server.py\"),\n    //         target = ProcessTarget.Server(port = APP_PORT, portEnvVar = \"PORT\"),\n    //         envProvider = envMapper { \"database.host\" to \"DB_HOST\" }\n    //     )\n    // }\n\n    // For apps that prefer CLI arguments instead of env vars\n    // processApp {\n    //     ProcessApplicationOptions(\n    //         command = listOf(\"/path/to/rust-server\"),\n    //         target = ProcessTarget.Server(port = APP_PORT),\n    //         argsProvider = argsMapper(prefix = \"--\", separator = \"=\") {\n    //             \"database.host\" to \"db-host\"   // --db-host=localhost\n    //             \"database.port\" to \"db-port\"   // --db-port=5432\n    //         }\n    //     )\n    // }\n}.run()\n```\n\n### ProcessTarget variants\n\n| Variant | Use case | Default readiness |\n|---------|----------|-------------------|\n| `ProcessTarget.Server(port, portEnvVar)` | HTTP APIs, gRPC servers, TCP servers | HTTP GET `/health` |\n| `ProcessTarget.Worker()` | Kafka consumers, batch jobs, CLI tools | 2-second fixed delay |\n\n### ReadinessStrategy variants\n\n| Strategy | Use case |\n|----------|----------|\n| `ReadinessStrategy.HttpGet(url, timeout, retries, retryDelay, expectedStatusCodes)` | REST APIs with health endpoint |\n| `ReadinessStrategy.TcpPort(port)` | gRPC servers, raw TCP (no HTTP) |\n| `ReadinessStrategy.Probe { ... }` | Custom readiness (file, DB query, etc.) |\n| `ReadinessStrategy.FixedDelay(duration)` | Simple workers with no readiness signal |\n\n### Configuration passing: envMapper and argsMapper\n\nTwo mechanisms to pass Stove configs to the process — use one or both:\n\n**envMapper** — environment variables:\n\n```kotlin\nenvMapper {\n    \"stove.config.key\" to \"ENV_VAR_NAME\"    // map Stove config → env var\n    env(\"STATIC_VAR\", \"value\")              // static env var\n    env(\"COMPUTED_VAR\") { computeValue() }  // computed env var\n}\n```\n\n**argsMapper** — CLI arguments (appended to the command):\n\n```kotlin\n// --db-host=localhost --db-port=5432\nargsMapper(prefix = \"--\", separator = \"=\") {\n    \"database.host\" to \"db-host\"            // map Stove config → CLI flag\n    arg(\"verbose\")                          // boolean flag\n    arg(\"log-level\", \"debug\")               // static flag\n}\n\n// -h localhost -p 5432 (space separator → two args per flag)\nargsMapper(prefix = \"-\", separator = \" \") {\n    \"database.host\" to \"h\"\n    \"database.port\" to \"p\"\n}\n```\n\n## Step 5: OpenTelemetry (optional)\n\nUse your language's OTel SDK. Key points:\n\n- Use **sync exporter** (`WithSyncer`) for tests, not batched\n- Set **W3C Trace Context propagation** so spans share the test's trace ID\n- Stove's HTTP client sends `traceparent` headers automatically\n\n## Step 6: Kafka bridge (Go only)\n\nFor Go apps using IBM/sarama, twmb/franz-go, or segmentio/kafka-go, add the `stove-kafka` bridge library. See [go-setup.md](go-setup.md) for details.\n\nThe bridge intercepts produced/consumed messages and forwards them via gRPC to Stove's observer, enabling `shouldBePublished` and `shouldBeConsumed` assertions.\n\n## Code Coverage (Go)\n\nGo 1.20+ supports integration test coverage: build with `go build -cover`, set `GOCOVERDIR` env var, and coverage data is written on graceful shutdown. This fits Stove's lifecycle (SIGTERM → graceful shutdown → coverage files).\n\nKey pieces:\n- **Gradle**: `-Pgo.coverage=true` adds `-cover` to build, sets `go.cover.dir` system property, disables build cache for coverage runs\n- **StoveConfig**: `env(\"GOCOVERDIR\") { System.getProperty(\"go.cover.dir\")?.also { File(it).mkdirs() } ?: \"\" }`\n- **Go app**: `signal.Ignore(syscall.SIGPIPE)` in `main()` — prevents SIGPIPE (exit 141) from killing the process before coverage flush when stdout pipe closes under `ProcessBuilder`\n- **Report tasks**: `goCoverageReport` (textfmt), `goCoverageSummary` (per-function), `goCoverageHtml` (visual)\n- **Umbrella task**: `e2eTestWithCoverage` runs tests + generates reports\n\n```bash\n./gradlew e2eTestWithCoverage -Pgo.coverage=true\n./gradlew e2eTest-containerWithCoverage -Pgo.coverage=true\n```\n\nNo Stove framework changes needed — uses existing `envMapper`, Gradle tasks, and SIGTERM shutdown.\n\nSee [go-setup.md](go-setup.md#code-coverage) for full details.\n\n## What you can't do\n\n- **No `bridge()` / `using<T> {}`** --- no access to app's DI container\n- Everything else works: HTTP, databases, Kafka, tracing, WireMock, gRPC, dashboard\n\n## Container mode (`containerApp`)\n\nUse `containerApp(...)` from `stove-container` when the AUT should run as a Docker image. Same envMapper/argsMapper model as processApp, plus a `configureContainer { ... }` block for Testcontainers-level customization (network mode, bind mounts, log consumers).\n\n```kotlin\nimport com.trendyol.stove.container.ContainerTarget\nimport com.trendyol.stove.container.containerApp\nimport com.trendyol.stove.system.application.envMapper\n\ncontainerApp(\n    image = \"my-app:local\",\n    target = ContainerTarget.Server(\n        hostPort = 8090, internalPort = 8090,\n        portEnvVar = \"APP_PORT\", bindHostPort = false\n    ),\n    envProvider = envMapper {\n        \"database.host\" to \"DB_HOST\"\n        \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n        env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:4317\")\n    },\n    configureContainer = {\n        withNetworkMode(\"host\")  // Linux only; use port binding + shared network on macOS/Windows\n    }\n)\n```\n\n`ContainerTarget.Server(hostPort, internalPort, portEnvVar, bindHostPort)` for HTTP/gRPC servers, `ContainerTarget.Worker()` for jobs. See [container.md](container.md) for the full guide (Dockerfile, Gradle wiring, networking strategies, coverage volume mounts, common pitfalls).\n\nA common pattern: one `StoveConfig.kt` branches on `-Dgo.aut.mode=process|container` to switch between starters. The infrastructure systems and tests stay identical.\n\n## MCP triage on failures\n\nWhen `stove` (the CLI) is running, agents can triage failed runs through the local MCP endpoint at `http://localhost:4040/mcp` instead of scraping logs. See [mcp.md](mcp.md) for the workflow.\n\n## Reference\n\n- Process module source: `starters/process/stove-process/`\n- Container module source: `starters/container/stove-container/`\n- Container DSL: `starters/container/stove-container/src/main/kotlin/com/trendyol/stove/container/ContainerDsl.kt`\n- Full Go example (process + container in one repo): `recipes/process/golang/go-showcase/`\n- Docs:\n  - `docs/other-languages/go.md` — overview / mode picker\n  - `docs/other-languages/go-process.md` — process mode walkthrough\n  - `docs/other-languages/go-container.md` — container mode walkthrough\n  - `docs/other-languages/index.md`\n  - `docs/Components/21-mcp.md` — MCP triage\n"
  },
  {
    "path": ".claude/skills/stove/system-setup.md",
    "content": "# System Setup Reference\n\n## Contents\n- [Process Application (non-JVM apps)](#process-application-non-jvm-apps)\n- [Provided Application (smoke testing)](#provided-application-smoke-testing)\n- [Keyed systems (multiple instances)](#keyed-systems-multiple-instances)\n- [HTTP Client](#http-client)\n- [PostgreSQL](#postgresql)\n- [MySQL](#mysql)\n- [MSSQL](#mssql)\n- [Cassandra](#cassandra)\n- [MongoDB](#mongodb)\n- [Redis](#redis)\n- [Elasticsearch](#elasticsearch)\n- [Couchbase](#couchbase)\n- [Kafka](#kafka)\n- [WireMock](#wiremock)\n- [gRPC Mock](#grpc-mock)\n- [gRPC Client](#grpc-client)\n- [Bridge](#bridge)\n- [Dashboard](#dashboard)\n- [Reporting](#reporting)\n- [Application runner](#application-runner)\n- [Migrations](#migrations)\n- [Container customization](#container-customization)\n- [Fault injection (pause/unpause)](#fault-injection-pauseunpause)\n- [Serde configuration](#serde-configuration)\n- [Cleanup](#cleanup)\n- [Keep dependencies running](#keep-dependencies-running)\n\nAll systems are configured inside `Stove().with { }`. The application runner goes last.\n\n## Process Application (non-JVM apps)\n\nUse `processApp()` or `goApp()` from the `stove-process` module to test applications written in any language (Go, Python, Rust, Node.js, etc.) as OS processes.\n\n```kotlin\ndependencies {\n    testImplementation(\"com.trendyol:stove-process\")\n}\n```\n\n### ProcessTarget variants\n\n| Variant | Use case | Default readiness |\n|---------|----------|-------------------|\n| `ProcessTarget.Server(port, portEnvVar)` | HTTP/gRPC/TCP servers | HTTP GET `/health` |\n| `ProcessTarget.Worker()` | Kafka consumers, batch jobs | 2s fixed delay |\n\n### ReadinessStrategy variants\n\n| Strategy | Use case |\n|----------|----------|\n| `ReadinessStrategy.HttpGet(url, timeout, retries, retryDelay, expectedStatusCodes)` | REST APIs with health endpoint |\n| `ReadinessStrategy.TcpPort(port)` | gRPC/TCP servers (no HTTP) |\n| `ReadinessStrategy.Probe { ... }` | Custom readiness (file, DB, etc.) |\n| `ReadinessStrategy.FixedDelay(duration)` | Simple workers |\n\n### Configuration passing\n\nStove collects all system configurations (`configureExposedConfiguration` from each system) as `key=value` strings and passes them to the process. Two mechanisms are available — use one or both:\n\n#### envMapper — environment variables\n\nMaps Stove config keys to OS environment variables:\n\n```kotlin\nenvMapper {\n    \"stove.config.key\" to \"ENV_VAR_NAME\"    // map Stove config → env var\n    env(\"STATIC_VAR\", \"value\")              // static env var\n    env(\"COMPUTED_VAR\") { computeValue() }  // computed env var\n}\n```\n\n#### argsMapper — CLI arguments\n\nMaps Stove config keys to command-line arguments, appended to the process command:\n\n```kotlin\n// GNU-style: --db-host=localhost --db-port=5432\nargsMapper(prefix = \"--\", separator = \"=\") {\n    \"database.host\" to \"db-host\"\n    \"database.port\" to \"db-port\"\n    arg(\"verbose\")                          // boolean flag: --verbose\n    arg(\"log-level\", \"debug\")               // static: --log-level=debug\n    arg(\"config-file\") { \"/tmp/test.yaml\" } // computed: --config-file=/tmp/test.yaml\n}\n\n// POSIX-style: -h localhost -p 5432 (space separator → two separate args)\nargsMapper(prefix = \"-\", separator = \" \") {\n    \"database.host\" to \"h\"\n    \"database.port\" to \"p\"\n}\n\n// No prefix: db-host=localhost\nargsMapper(prefix = \"\", separator = \"=\") {\n    \"database.host\" to \"db-host\"\n}\n```\n\n#### Using both together\n\n```kotlin\nprocessApp {\n    ProcessApplicationOptions(\n        command = listOf(\"/path/to/server\"),\n        target = ProcessTarget.Server(port = 8090),\n        envProvider = envMapper {\n            \"database.host\" to \"DB_HOST\"         // passed as env var\n        },\n        argsProvider = argsMapper(prefix = \"--\", separator = \"=\") {\n            \"database.port\" to \"db-port\"         // passed as --db-port=5432\n            arg(\"verbose\")\n        }\n    )\n}\n```\n\n### Examples\n\n```kotlin\n// HTTP API (Go) — env vars\ngoApp(\n    target = ProcessTarget.Server(port = 8090, portEnvVar = \"APP_PORT\"),\n    envProvider = envMapper {\n        \"database.host\" to \"DB_HOST\"\n        \"database.port\" to \"DB_PORT\"\n        env(\"LOG_LEVEL\", \"debug\")\n    }\n)\n\n// Rust CLI server — CLI args\nprocessApp {\n    ProcessApplicationOptions(\n        command = listOf(\"/path/to/rust-server\"),\n        target = ProcessTarget.Server(port = 8090),\n        argsProvider = argsMapper(prefix = \"--\", separator = \"=\") {\n            \"database.host\" to \"db-host\"\n            \"database.port\" to \"db-port\"\n        }\n    )\n}\n\n// gRPC server (any language)\nprocessApp {\n    ProcessApplicationOptions(\n        command = listOf(\"/path/to/grpc-server\"),\n        target = ProcessTarget.Server(\n            port = 50051,\n            portEnvVar = \"GRPC_PORT\",\n            readiness = ReadinessStrategy.TcpPort(port = 50051),\n        ),\n        envProvider = envMapper { \"database.host\" to \"DB_HOST\" }\n    )\n}\n\n// Kafka consumer (no port)\ngoApp(\n    target = ProcessTarget.Worker(readiness = ReadinessStrategy.FixedDelay(3.seconds)),\n    envProvider = envMapper { \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\" }\n)\n```\n\nKey points:\n- `goApp()` defaults binary path from `go.app.binary` system property\n- Port env var is injected automatically for `Server` targets\n- `envProvider` and `argsProvider` can be used independently or together\n- No `bridge()` — the app runs as a separate process, no DI access\n- See [other-languages.md](other-languages.md) and [go-setup.md](go-setup.md) for full setup guides\n\n## Provided Application (smoke testing)\n\nUse `providedApplication()` instead of a JVM runner to test against an already-deployed application. The application can be written in **any language** — Go, Python, .NET, Rust, Node.js, etc.\n\n```kotlin\nStove().with {\n    httpClient {\n        HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\")\n    }\n    providedApplication {\n        ProvidedApplicationOptions(\n            readiness = ReadinessStrategy.HttpGet(\n                url = \"https://staging.myapp.com/health\",\n                retries = 10,\n                retryDelay = 1.seconds,\n                timeout = 30.seconds,\n                expectedStatusCodes = setOf(200)\n            )\n        )\n    }\n}.run()\n```\n\nWithout health check (fire-and-forget):\n\n```kotlin\nprovidedApplication()  // No health check, no options\n```\n\nCombine with `.provided()` system options to connect to existing infrastructure:\n\n```kotlin\nStove().with {\n    httpClient {\n        HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\")\n    }\n    postgresql(AppDb) {\n        PostgresqlOptions.provided(\n            jdbcUrl = \"jdbc:postgresql://staging-db:5432/myapp\",\n            cleanup = { ops -> ops.execute(\"DELETE FROM orders WHERE test_data = true\") },\n            configureExposedConfiguration = { listOf() }\n        )\n    }\n    redis(CacheCluster) {\n        RedisOptions.provided(\n            host = \"staging-redis\", port = 6379,\n            configureExposedConfiguration = { listOf() }\n        )\n    }\n    providedApplication {\n        ProvidedApplicationOptions(\n            readiness = ReadinessStrategy.HttpGet(url = \"https://staging.myapp.com/health\")\n        )\n    }\n}.run()\n```\n\n**Important**: `Bridge` (DI access via `using<T>`) is **not available** with `providedApplication()` — there is no local DI container. Use `cleanup` lambdas to manage test data on external infrastructure.\n\n## Keyed systems (multiple instances)\n\nRegister multiple instances of the same system type using `SystemKey`. Define keys as singleton objects:\n\n```kotlin\nobject AppDb : SystemKey\nobject AnalyticsDb : SystemKey\nobject PaymentService : SystemKey\nobject InventoryService : SystemKey\n```\n\nUse keys in registration:\n\n```kotlin\nStove().with {\n    // Two separate PostgreSQL containers with independent configs\n    postgresql(AppDb) {\n        PostgresqlOptions(\n            databaseName = \"appdb\",\n            configureExposedConfiguration = { cfg ->\n                listOf(\"app.datasource.url=${cfg.jdbcUrl}\")\n            }\n        ).migrations { register<AppSchemaMigration>() }\n    }\n    postgresql(AnalyticsDb) {\n        PostgresqlOptions(\n            databaseName = \"analyticsdb\",\n            configureExposedConfiguration = { cfg ->\n                listOf(\"analytics.datasource.url=${cfg.jdbcUrl}\")\n            }\n        ).migrations { register<AnalyticsSchemaMigration>() }\n    }\n\n    // Two HTTP clients pointing to different services\n    httpClient(PaymentService) {\n        HttpClientSystemOptions(baseUrl = \"https://pay.internal\")\n    }\n    httpClient(InventoryService) {\n        HttpClientSystemOptions(baseUrl = \"https://inventory.internal\")\n    }\n\n    // Two WireMock instances for different external APIs\n    wiremock(PaymentService) {\n        WireMockSystemOptions(port = 0, configureExposedConfiguration = { cfg ->\n            listOf(\"payment.url=${cfg.baseUrl}\")\n        })\n    }\n    wiremock(InventoryService) {\n        WireMockSystemOptions(port = 0, configureExposedConfiguration = { cfg ->\n            listOf(\"inventory.url=${cfg.baseUrl}\")\n        })\n    }\n\n    springBoot(runner = { params -> run(params) })\n}.run()\n```\n\nAll systems support keyed registration: PostgreSQL, MySQL, MSSQL, Cassandra, MongoDB, Redis, Elasticsearch, Couchbase, Kafka (core), WireMock, gRPC, gRPC Mock, HTTP.\n\nEach keyed instance gets:\n- Its own container with a unique dynamic port (no conflicts)\n- Independent `configureExposedConfiguration` — all configs are aggregated and passed to the AUT\n- Isolated state storage (separate lock files per key)\n- Its own `cleanup` lambda\n- Distinct reporting name (e.g., `\"PostgreSQL [AppDb]\"`)\n\nA single key can be shared across protocol types:\n\n```kotlin\nobject PaymentService : SystemKey\n\nhttpClient(PaymentService) { HttpClientSystemOptions(baseUrl = \"...\") }\ngrpc(PaymentService) { GrpcSystemOptions(host = \"...\", port = 50051) }\nwiremock(PaymentService) { WireMockSystemOptions(...) }\n```\n\nKeyed systems work with both Testcontainers and `.provided()` (external) instances:\n\n```kotlin\npostgresql(AppDb) {\n    PostgresqlOptions.provided(\n        jdbcUrl = \"jdbc:postgresql://staging-db:5432/app\",\n        configureExposedConfiguration = { listOf() }\n    )\n}\n```\n\n## HTTP Client\n\n```kotlin\nhttpClient {\n    HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n}\n```\n\nAdvanced options:\n\n```kotlin\nhttpClient {\n    HttpClientSystemOptions(\n        baseUrl = \"http://localhost:8080\",\n        timeout = 60.seconds,                                          // Request timeout (default: 30s)\n        contentConverter = JacksonConverter(myObjectMapper),            // Custom JSON converter\n        configureClient = {                                            // Ktor HttpClient config\n            install(Logging) { level = LogLevel.ALL }\n        },\n        configureWebSocket = {                                         // WebSocket config\n            pingIntervalMillis = 10_000\n        },\n        createClient = { url -> myCustomHttpClient(url) }             // Full client factory override\n    )\n}\n```\n\n## PostgreSQL\n\n```kotlin\npostgresql {\n    PostgresqlOptions(\n        databaseName = \"testdb\",\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"spring.datasource.url=${cfg.jdbcUrl}\",\n                \"spring.datasource.username=${cfg.username}\",\n                \"spring.datasource.password=${cfg.password}\"\n            )\n        }\n    ).migrations {\n        register<InitialMigration>()\n    }\n}\n```\n\nMigration class:\n\n```kotlin\nclass InitialMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n    override val order: Int = 1\n\n    override suspend fun execute(connection: PostgresSqlMigrationContext) {\n        connection.operations.execute(\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS orders (\n                id VARCHAR(255) PRIMARY KEY,\n                user_id VARCHAR(255) NOT NULL,\n                amount DECIMAL(10, 2) NOT NULL,\n                status VARCHAR(50) NOT NULL,\n                created_at TIMESTAMP DEFAULT NOW()\n            );\n            \"\"\".trimIndent()\n        )\n    }\n}\n```\n\nFor R2DBC:\n\n```kotlin\nconfigureExposedConfiguration = { cfg ->\n    listOf(\n        \"spring.r2dbc.url=r2dbc:postgresql://${cfg.host}:${cfg.port}/testdb\",\n        \"spring.r2dbc.username=${cfg.username}\",\n        \"spring.r2dbc.password=${cfg.password}\"\n    )\n}\n```\n\n## MySQL\n\n```kotlin\nmysql {\n    MySqlSystemOptions(\n        databaseName = \"testdb\",\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"spring.datasource.url=${cfg.jdbcUrl}\",\n                \"spring.datasource.username=${cfg.username}\",\n                \"spring.datasource.password=${cfg.password}\"\n            )\n        }\n    ).migrations {\n        register<InitialMigration>()\n    }\n}\n```\n\nSame migration pattern as PostgreSQL, using `MySqlMigrationContext`.\n\n## MSSQL\n\n```kotlin\nmssql {\n    MsSqlSystemOptions(\n        databaseName = \"testdb\",\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"spring.datasource.url=${cfg.jdbcUrl}\",\n                \"spring.datasource.username=${cfg.username}\",\n                \"spring.datasource.password=${cfg.password}\"\n            )\n        }\n    ).migrations {\n        register<InitialMigration>()\n    }\n}\n```\n\nSame migration pattern, using `MsSqlMigrationContext`.\n\n## Cassandra\n\n```kotlin\ncassandra {\n    CassandraSystemOptions(\n        keyspace = \"my_keyspace\",\n        datacenter = \"datacenter1\",\n        container = CassandraContainerOptions(tag = \"4.1\"),\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"cassandra.host=${cfg.host}\",\n                \"cassandra.port=${cfg.port}\",\n                \"cassandra.keyspace=${cfg.keyspace}\",\n                \"cassandra.datacenter=${cfg.datacenter}\"\n            )\n        }\n    ).migrations {\n        register<CreateTableMigration>()\n    }\n}\n```\n\nMigration class:\n\n```kotlin\nclass CreateTableMigration : CassandraMigration {\n    override val order: Int = 1\n\n    override suspend fun execute(connection: CassandraMigrationContext) {\n        connection.session.execute(\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS orders (\n                id text PRIMARY KEY,\n                user_id text,\n                amount double,\n                status text\n            );\n            \"\"\".trimIndent()\n        )\n    }\n}\n```\n\nFor an externally managed Cassandra:\n\n```kotlin\ncassandra {\n    CassandraSystemOptions.provided(\n        host = \"localhost\",\n        port = 9042,\n        datacenter = \"dc1\",\n        keyspace = \"my_keyspace\",\n        configureExposedConfiguration = { cfg ->\n            listOf(\"cassandra.contact-points=${cfg.host}:${cfg.port}\")\n        }\n    )\n}\n```\n\nContainer operations: `pause()` / `unpause()` to simulate Cassandra downtime.\n\n## MongoDB\n\n```kotlin\nmongodb {\n    MongodbSystemOptions(\n        databaseOptions = DatabaseOptions(\n            default = DefaultDatabase(name = \"testdb\", collection = \"orders\")\n        ),\n        container = MongoContainerOptions(tag = \"7.0\"),\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"mongodb.connection-string=${cfg.connectionString}\",\n                \"mongodb.host=${cfg.host}\",\n                \"mongodb.port=${cfg.port}\"\n            )\n        }\n    ).migrations {\n        register<SeedDataMigration>()\n    }\n}\n```\n\nFor an externally managed MongoDB:\n\n```kotlin\nmongodb {\n    MongodbSystemOptions.provided(\n        connectionString = \"mongodb://localhost:27017\",\n        host = \"localhost\",\n        port = 27017,\n        configureExposedConfiguration = { cfg ->\n            listOf(\"spring.data.mongodb.uri=${cfg.connectionString}\")\n        }\n    )\n}\n```\n\nMigration class uses `MongodbMigrationContext` with access to `client: MongoClient`.\n\nContainer operations: `pause()` / `unpause()`.\n\n## Redis\n\n```kotlin\nredis {\n    RedisOptions(\n        database = 0,\n        password = \"redis-password\",\n        container = RedisContainerOptions(tag = \"7-alpine\"),\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"redis.host=${cfg.host}\",\n                \"redis.port=${cfg.port}\",\n                \"redis.password=${cfg.password}\"\n            )\n        }\n    )\n}\n```\n\nFor an externally managed Redis:\n\n```kotlin\nredis {\n    RedisOptions.provided(\n        host = \"localhost\",\n        port = 6379,\n        password = \"secret\",\n        database = 0,\n        configureExposedConfiguration = { cfg ->\n            listOf(\"spring.redis.url=${cfg.redisUri}\")\n        }\n    )\n}\n```\n\nContainer operations: `pause()` / `unpause()`.\n\n## Elasticsearch\n\n```kotlin\nelasticsearch {\n    ElasticsearchSystemOptions(\n        container = ElasticContainerOptions(\n            tag = \"8.15.0\",\n            password = \"elastic-password\",\n            disableSecurity = true\n        ),\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"elasticsearch.host=${cfg.host}\",\n                \"elasticsearch.port=${cfg.port}\"\n            )\n        }\n    ).migrations {\n        register<CreateIndexMigration>()\n    }\n}\n```\n\nFor an externally managed Elasticsearch:\n\n```kotlin\nelasticsearch {\n    ElasticsearchSystemOptions.provided(\n        host = \"localhost\",\n        port = 9200,\n        configureExposedConfiguration = { cfg ->\n            listOf(\"es.url=http://${cfg.host}:${cfg.port}\")\n        }\n    )\n}\n```\n\nMigration class uses `ElasticsearchClient` as context directly.\n\nContainer operations: `pause()` / `unpause()`.\n\n## Couchbase\n\n```kotlin\ncouchbase {\n    CouchbaseSystemOptions(\n        defaultBucket = \"test-bucket\",\n        containerOptions = CouchbaseContainerOptions(tag = \"7.6.1\"),\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"couchbase.connection-string=${cfg.connectionString}\",\n                \"couchbase.username=${cfg.username}\",\n                \"couchbase.password=${cfg.password}\"\n            )\n        }\n    ).migrations {\n        register<CreateBucketMigration>()\n    }\n}\n```\n\nFor an externally managed Couchbase:\n\n```kotlin\ncouchbase {\n    CouchbaseSystemOptions.provided(\n        connectionString = \"couchbase://localhost\",\n        username = \"admin\",\n        password = \"password\",\n        defaultBucket = \"test-bucket\",\n        configureExposedConfiguration = { cfg ->\n            listOf(\"couchbase.hosts=${cfg.hostsWithPort}\")\n        }\n    )\n}\n```\n\nMigration class uses `Cluster` as context. Container operations: `pause()` / `unpause()`.\n\n## Kafka\n\nUse `stove-kafka` for standalone. Use `stove-spring-kafka` for Spring Boot Kafka listeners (`shouldBeConsumed`, `shouldBeFailed`, `shouldBeRetried`).\n\n```kotlin\nkafka {\n    KafkaSystemOptions(\n        serde = StoveSerde.jackson.anyByteArraySerde(),\n        valueSerializer = JsonSerializer(),\n        containerOptions = KafkaContainerOptions(tag = \"8.0.3\") {\n            withStartupAttempts(3)\n        },\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"spring.kafka.bootstrap-servers=${cfg.bootstrapServers}\",\n                \"spring.kafka.producer.properties.interceptor.classes=${cfg.interceptorClass}\",\n                \"spring.kafka.consumer.properties.interceptor.classes=${cfg.interceptorClass}\"\n            )\n        }\n    )\n}\n```\n\nFor embedded Kafka (no Docker container):\n\n```kotlin\nkafka {\n    KafkaSystemOptions(\n        useEmbeddedKafka = true,\n        configureExposedConfiguration = { cfg ->\n            listOf(\"spring.kafka.bootstrap-servers=${cfg.bootstrapServers}\")\n        }\n    )\n}\n```\n\n**Application-side requirements (Spring Boot Kafka)**:\n- Inject `RecordInterceptor<String, String>` into your `ConcurrentKafkaListenerContainerFactory` and call `factory.setRecordInterceptor(interceptor)`.\n- Register `TestSystemKafkaInterceptor<*, *>` and a `StoveSerde` bean in test dependencies.\n\n### Test-friendly Kafka settings\n\nDefault Kafka producer/consumer settings are tuned for production throughput, not test speed. In e2e tests, this causes timeouts, flaky assertions, and slow feedback. Configure for **immediate delivery and fast commits**:\n\n**Container-level** — enable auto-topic creation so topics exist when producers/consumers first connect:\n\n```kotlin\nkafka {\n    KafkaSystemOptions(\n        containerOptions = KafkaContainerOptions(tag = \"8.0.3\") {\n            withEnv(\"KAFKA_AUTO_CREATE_TOPICS_ENABLE\", \"true\")\n        },\n        configureExposedConfiguration = { cfg ->\n            listOf(\"spring.kafka.bootstrap-servers=${cfg.bootstrapServers}\")\n        }\n    )\n}\n```\n\n**Producer settings** — flush immediately, don't batch:\n\n```properties\n# Spring Boot application.yml or exposed via Stove config\nspring.kafka.producer.properties.linger.ms=0          # Send immediately, don't wait to batch\nspring.kafka.producer.properties.batch.size=1          # Single-message batches\nspring.kafka.producer.acks=all                         # Wait for all replicas (reliable in single-broker test)\n```\n\n**Consumer settings** — commit fast, start from beginning, short timeouts:\n\n```properties\nspring.kafka.consumer.auto-offset-reset=earliest            # Start from beginning (don't miss messages)\nspring.kafka.consumer.properties.auto.commit.interval.ms=100  # Commit offsets every 100ms (default: 5000ms)\nspring.kafka.consumer.properties.max.poll.interval.ms=10000   # Shorter poll timeout (default: 300000ms)\nspring.kafka.consumer.properties.session.timeout.ms=10000     # Faster rebalance on failure (default: 45000ms)\nspring.kafka.consumer.properties.heartbeat.interval.ms=3000   # Faster heartbeat (default: 3000ms, keep ≤ session/3)\n```\n\n**Why this matters for Stove assertions:**\n\n- `shouldBePublished` checks the Stove interceptor sink — messages must reach it promptly. `linger.ms=0` and `batch.size=1` prevent the producer from holding messages.\n- `shouldBeConsumed` checks that the message was consumed AND its offset committed. `auto.commit.interval.ms=100` makes committed offsets visible within 100ms instead of the 5-second default.\n- `shouldBeFailed` / `shouldBeRetried` check error and retry sinks — short `max.poll.interval.ms` prevents long waits before Kafka considers a consumer dead.\n- Without `auto-offset-reset=earliest`, consumers joining after a message is produced will never see it, causing `shouldBeConsumed` to timeout.\n\n**Passing these via Stove's `configureExposedConfiguration`:**\n\n```kotlin\nkafka {\n    KafkaSystemOptions(\n        containerOptions = KafkaContainerOptions {\n            withEnv(\"KAFKA_AUTO_CREATE_TOPICS_ENABLE\", \"true\")\n        },\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"spring.kafka.bootstrap-servers=${cfg.bootstrapServers}\",\n                \"spring.kafka.producer.properties.interceptor.classes=${cfg.interceptorClass}\",\n                \"spring.kafka.consumer.properties.interceptor.classes=${cfg.interceptorClass}\",\n                // Test-friendly overrides\n                \"spring.kafka.producer.properties.linger.ms=0\",\n                \"spring.kafka.producer.properties.batch.size=1\",\n                \"spring.kafka.consumer.auto-offset-reset=earliest\",\n                \"spring.kafka.consumer.properties.auto.commit.interval.ms=100\"\n            )\n        }\n    )\n}\n```\n\n**Non-Spring JVM apps** — the same principles apply. Pass equivalent properties through your app's configuration mechanism:\n\n| Setting | Production default | Test-friendly value | Why |\n|---------|-------------------|--------------------|----|\n| `linger.ms` | 5-100 | `0` | Immediate send |\n| `batch.size` | 16384 | `1` | No batching |\n| `auto.commit.interval.ms` | 5000 | `100` | Fast offset visibility |\n| `auto.offset.reset` | `latest` | `earliest` | Don't miss messages |\n| `max.poll.interval.ms` | 300000 | `10000` | Faster failure detection |\n| `auto.create.topics.enable` (broker) | `true` | `true` | Topics exist on first use |\n\n**Go applications** — see [go-setup.md](go-setup.md) for per-library settings (sarama, franz-go, segmentio/kafka-go) including auto-topic creation, batch timeouts, commit intervals, and the separate producer/consumer client pattern for franz-go.\n\n## WireMock\n\n```kotlin\nwiremock {\n    WireMockSystemOptions(\n        port = 0, // Dynamic port — recommended for CI\n        serde = StoveSerde.jackson.anyByteArraySerde(),\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"payment.service.url=${cfg.baseUrl}\",\n                \"inventory.service.url=${cfg.baseUrl}\"\n            )\n        }\n    )\n}\n```\n\nAll external service URLs must be configurable so they can be pointed to WireMock.\n\n## gRPC Mock\n\n```kotlin\ngrpcMock {\n    GrpcMockSystemOptions(\n        port = 0, // Dynamic port\n        configureExposedConfiguration = { cfg ->\n            listOf(\n                \"grpcService.host=${cfg.host}\",\n                \"grpcService.port=${cfg.port}\"\n            )\n        }\n    )\n}\n```\n\n## gRPC Client\n\nFor testing your own gRPC server (not external mocks):\n\n```kotlin\ngrpc {\n    GrpcSystemOptions(host = \"localhost\", port = 50051)\n}\n```\n\n## Bridge\n\nDirect access to DI container from tests. Built into `stove-spring` / `stove-ktor` / `stove-micronaut`.\n\n```kotlin\nbridge()  // Auto-detects DI framework\n```\n\n### Ktor DI support\n\nBridge auto-detects your DI framework:\n\n```kotlin\n// Koin — add io.insert-koin:koin-ktor to classpath\nbridge()  // auto-detects Koin\n\n// Ktor-DI — add io.ktor:ktor-server-di to classpath\nbridge()  // auto-detects Ktor-DI\n\n// Custom resolver (Kodein, Dagger, etc.)\nbridge { application, type ->\n    myDiContainer.resolve(type)\n}\n```\n\n## Dashboard\n\nStreams test events to the stove CLI for real-time visualization. Requires `stove-dashboard` and `stove-extensions-kotest` or `stove-extensions-junit` — see [Reporting](#reporting).\n\n```kotlin\ndashboard {\n    DashboardSystemOptions(\n        appName = \"my-service\",\n        cliHost = \"localhost\",  // default\n        cliPort = 4041          // default\n    )\n}\n```\n\nRun `stove` CLI separately, then run your tests — the dashboard at `http://localhost:4040` shows a live tree of specs, test hierarchy, timeline entries, traces, and snapshots.\n\n## Reporting\n\nReporting and test hierarchy tracking require the framework extension. This is mandatory for Dashboard, tracing, and structured failure reports.\n\n### Kotest\n\nRegister `StoveKotestExtension` in your `AbstractProjectConfig`:\n\n```kotlin\nclass StoveConfig : AbstractProjectConfig() {\n    override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n    override suspend fun beforeProject() {\n        Stove().with { /* systems */ }.run()\n    }\n\n    override suspend fun afterProject() {\n        Stove.stop()\n    }\n}\n```\n\nRequires `stove-extensions-kotest` dependency and a `kotest.properties` file pointing to this config class.\n\n### JUnit\n\nAnnotate your base test class with `@ExtendWith(StoveJUnitExtension::class)`:\n\n```kotlin\n@ExtendWith(StoveJUnitExtension::class)\n@TestInstance(TestInstance.Lifecycle.PER_CLASS)\nabstract class BaseE2ETest {\n    companion object {\n        @JvmStatic @BeforeAll\n        fun setup() = runBlocking {\n            Stove().with { /* systems */ }.run()\n        }\n\n        @JvmStatic @AfterAll\n        fun teardown() = runBlocking { Stove.stop() }\n    }\n}\n```\n\nRequires `stove-extensions-junit` dependency. Supports `@Nested` class hierarchy.\n\n## Application runner\n\nGoes last, after all systems. Systems inject configuration via `configureExposedConfiguration`.\n\n### Spring Boot\n\n```kotlin\nspringBoot(\n    runner = { params ->\n        com.yourcompany.yourapp.run(params) {\n            addTestDependencies {\n                bean<TestSystemKafkaInterceptor<*, *>>(isPrimary = true)\n                bean { StoveSerde.jackson.anyByteArraySerde() }\n            }\n        }\n    },\n    withParameters = listOf(\n        \"server.port=8080\",\n        \"grpc.server.port=$GRPC_SERVER_PORT\"\n    )\n)\n```\n\nFor Spring Boot 4.x, use `addTestDependencies4x` with `registerBean<>()`:\n\n```kotlin\naddTestDependencies4x {\n    registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)\n    registerBean { StoveSerde.jackson.anyByteArraySerde() }\n}\n```\n\n### Ktor\n\n```kotlin\nktor(\n    runner = { params ->\n        com.yourcompany.yourapp.run(params, wait = false)\n    },\n    withParameters = listOf(\"server.port=8080\")\n)\n```\n\n### Quarkus\n\n```kotlin\nquarkus(\n    runner = { params ->\n        com.yourcompany.yourapp.main(params)\n    },\n    withParameters = listOf(\"quarkus.http.port=8080\")\n)\n```\n\nSupports both direct main runner and packaged runtime. Configurable startup timeout via `stove.quarkus.startup.timeout.ms` system property (default: 120s).\n\n### Micronaut\n\n```kotlin\nmicronaut(\n    runner = { params ->\n        com.yourcompany.yourapp.run(params)\n    },\n    withParameters = listOf(\"micronaut.server.port=8080\")\n)\n```\n\nReturns `ApplicationContext`, enabling bridge/DI access.\n\n## Migrations\n\nDatabase and infrastructure systems support migrations that run after the system starts and before tests execute. Use for schema creation, indexing, and seed data.\n\n```kotlin\npostgresql {\n    PostgresqlOptions(\n        configureExposedConfiguration = { cfg -> listOf(\"spring.datasource.url=${cfg.jdbcUrl}\") }\n    ).migrations {\n        register<CreateTablesMigration>()\n        register<SeedDataMigration>()\n    }\n}\n```\n\nMigration class:\n\n```kotlin\nclass CreateTablesMigration : PostgresqlMigration {\n    override val order: Int = MigrationPriority.HIGHEST.value  // Runs first\n\n    override suspend fun execute(connection: PostgresSqlMigrationContext) {\n        connection.operations.execute(\"CREATE TABLE IF NOT EXISTS orders (...)\")\n    }\n}\n\nclass SeedDataMigration : PostgresqlMigration {\n    override val order: Int = 100  // After schema\n\n    override suspend fun execute(connection: PostgresSqlMigrationContext) {\n        connection.operations.execute(\"INSERT INTO orders ...\")\n    }\n}\n```\n\nOrdering: migrations execute in ascending `order`. Use `MigrationPriority.HIGHEST` (schema), `MigrationPriority.LOWEST` (final setup), or custom integers.\n\nType aliases per system (use instead of `DatabaseMigration<XyzContext>`):\n\n| Module | Type Alias | Context Type |\n|---|---|---|\n| stove-postgres | `PostgresqlMigration` | `PostgresSqlMigrationContext` |\n| stove-mysql | `MySqlMigration` | `MySqlMigrationContext` |\n| stove-mssql | `MsSqlMigration` | `SqlMigrationContext` |\n| stove-mongodb | `MongodbMigration` | `MongodbMigrationContext` |\n| stove-couchbase | `CouchbaseMigration` | `Cluster` |\n| stove-elasticsearch | `ElasticsearchMigration` | `ElasticsearchClient` |\n| stove-redis | `RedisMigration` | `RedisMigrationContext` |\n| stove-kafka | `KafkaMigration` | `KafkaMigrationContext` |\n| stove-cassandra | `CassandraMigration` | `CassandraMigrationContext` |\n\nAdvanced patterns:\n\n```kotlin\n// Factory function for migrations with parameters\n.migrations {\n    register<ConfigurableMigration> {\n        ConfigurableMigration(batchSize = 1000)\n    }\n}\n\n// Replace a migration with a test-specific override\n.migrations {\n    register<ProductionSeedMigration>()\n    replace<ProductionSeedMigration, TestSeedMigration>()\n\n    // Or replace with a factory\n    replace<ProductionSeedMigration> {\n        MinimalSeedMigration()\n    }\n}\n```\n\nNotes: migrations must have no-arg constructors (unless using factory registration). Use idempotent statements (`IF NOT EXISTS`). Don't close the connection — Stove manages it.\n\n## Container customization\n\nAll container-backed systems accept a `ContainerOptions` with customization hooks:\n\n```kotlin\npostgresql {\n    PostgresqlOptions(\n        container = PostgresqlContainerOptions(\n            registry = \"my-registry.example.com\",   // Custom Docker registry\n            image = \"postgres\",                       // Image name\n            tag = \"16-alpine\",                        // Image tag\n            compatibleSubstitute = \"my-registry.example.com/postgres\",  // Alternative image\n            containerFn = {                           // Customize container before startup\n                withEnv(\"POSTGRES_INITDB_ARGS\", \"--encoding=UTF-8\")\n                withCommand(\"postgres\", \"-c\", \"max_connections=200\")\n            }\n        ),\n        configureExposedConfiguration = { cfg -> listOf(\"spring.datasource.url=${cfg.jdbcUrl}\") }\n    )\n}\n\nkafka {\n    KafkaSystemOptions(\n        containerOptions = KafkaContainerOptions(tag = \"8.0.3\") {\n            withStartupAttempts(3)\n            withEnv(\"KAFKA_AUTO_CREATE_TOPICS_ENABLE\", \"true\")\n        },\n        configureExposedConfiguration = { cfg -> listOf(\"kafka.bootstrap=${cfg.bootstrapServers}\") }\n    )\n}\n```\n\nThe `containerFn` lambda receives the container instance before startup, so you can call any Testcontainers method (env vars, commands, exposed ports, volume mounts, etc.).\n\n## Fault injection (pause/unpause)\n\nAll container-backed systems support `pause()` / `unpause()` for simulating outages. Both are idempotent.\n\n```kotlin\nstove {\n    postgresql { pause() }\n\n    http {\n        getResponse<Any>(\"/health\") { response ->\n            response.status shouldBe 503\n        }\n    }\n\n    postgresql { unpause() }\n\n    http {\n        getResponse<Any>(\"/health\") { response ->\n            response.status shouldBe 200\n        }\n    }\n}\n```\n\nSupported: PostgreSQL, MySQL, MSSQL, Cassandra, MongoDB, Redis, Elasticsearch, Couchbase, Kafka.\n\n## Serde configuration\n\nStove uses `StoveSerde` for JSON handling across systems (Kafka, MongoDB, WireMock, HTTP). Three implementations are built in:\n\n```kotlin\n// Jackson (default) — configure ObjectMapper\nval serde = StoveSerde.jackson.anyByteArraySerde()\nval stringSerde = StoveSerde.jackson.anyJsonStringSerde()\n\n// With custom ObjectMapper\nval customSerde = StoveSerde.jackson.anyByteArraySerde(\n    StoveSerde.jackson.byConfiguring {\n        disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n        enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)\n    }\n)\n\n// Kotlinx Serialization (requires @Serializable)\nval kotlinxSerde = StoveSerde.kotlinx.anyByteArraySerde()\nval customKotlinx = StoveSerde.kotlinx.anyByteArraySerde(\n    StoveSerde.kotlinx.byConfiguring {\n        ignoreUnknownKeys = true\n        isLenient = true\n    }\n)\n\n// Gson\nval gsonSerde = StoveSerde.gson.anyByteArraySerde()\nval customGson = StoveSerde.gson.anyByteArraySerde(\n    StoveSerde.gson.byConfiguring {\n        setPrettyPrinting()\n        serializeNulls()\n    }\n)\n```\n\n**Important: align Stove's serde with your application's serialization.** If your application uses a custom ObjectMapper (e.g., with snake_case naming, custom date formats, or extra modules), Stove must use the same configuration. Otherwise, Stove will fail to deserialize responses from your HTTP endpoints, Kafka messages produced by your app, or documents written to MongoDB/Elasticsearch/Couchbase. The same applies if your application uses Kotlinx Serialization or Gson — use the matching `StoveSerde` variant.\n\n```kotlin\n// Reuse your application's ObjectMapper so ser/de behavior matches\nval appObjectMapper = MyApp.objectMapper()\n\nhttpClient {\n    HttpClientSystemOptions(\n        baseUrl = \"http://localhost:8080\",\n        contentConverter = JacksonConverter(appObjectMapper)\n    )\n}\n\nkafka {\n    KafkaSystemOptions(\n        serde = StoveSerde.jackson.anyByteArraySerde(appObjectMapper),\n        configureExposedConfiguration = { cfg -> listOf(\"kafka.bootstrap=${cfg.bootstrapServers}\") }\n    )\n}\n\nmongodb {\n    MongodbSystemOptions(\n        serde = StoveSerde.jackson.anyJsonStringSerde(appObjectMapper),\n        configureExposedConfiguration = { cfg -> listOf(\"mongodb.uri=${cfg.connectionString}\") }\n    )\n}\n\nwiremock {\n    WireMockSystemOptions(\n        serde = StoveSerde.jackson.anyByteArraySerde(appObjectMapper),\n        configureExposedConfiguration = { cfg -> listOf(\"service.url=${cfg.baseUrl}\") }\n    )\n}\n```\n\n## Cleanup\n\nEvery system accepts a `cleanup` lambda in its options. This runs during `Stove.stop()` (after all tests complete) and receives the system's native client. Use it to wipe test data — especially important for provided (external) instances that persist between runs.\n\n```kotlin\npostgresql {\n    PostgresqlOptions(\n        databaseName = \"testdb\",\n        cleanup = { ops -> ops.execute(\"TRUNCATE orders, users\") },\n        configureExposedConfiguration = { cfg -> listOf(\"spring.datasource.url=${cfg.jdbcUrl}\") }\n    )\n}\n\nmongodb {\n    MongodbSystemOptions(\n        cleanup = { client -> client.getDatabase(\"testdb\").drop() },\n        configureExposedConfiguration = { cfg -> listOf(\"mongodb.uri=${cfg.connectionString}\") }\n    )\n}\n\nkafka {\n    KafkaSystemOptions(\n        cleanup = { admin ->\n            val topics = admin.listTopics().names().get().filter { it.startsWith(\"test-\") }\n            if (topics.isNotEmpty()) admin.deleteTopics(topics).all().get()\n        },\n        configureExposedConfiguration = { cfg -> listOf(\"kafka.bootstrap=${cfg.bootstrapServers}\") }\n    )\n}\n\nredis {\n    RedisOptions(\n        cleanup = { client -> client.connect().sync().flushdb() },\n        configureExposedConfiguration = { cfg -> listOf(\"redis.host=${cfg.host}\") }\n    )\n}\n\nelasticsearch {\n    ElasticsearchSystemOptions(\n        cleanup = { client -> client.indices().delete { it.index(\"test-*\") } },\n        configureExposedConfiguration = { cfg -> listOf(\"es.host=${cfg.host}\") }\n    )\n}\n\ncouchbase {\n    CouchbaseSystemOptions(\n        cleanup = { cluster -> cluster.query(\"DELETE FROM `test-bucket`\") },\n        configureExposedConfiguration = { cfg -> listOf(\"cb.conn=${cfg.connectionString}\") }\n    )\n}\n\ncassandra {\n    CassandraSystemOptions(\n        cleanup = { session -> session.execute(\"TRUNCATE my_keyspace.orders\") },\n        configureExposedConfiguration = { cfg -> listOf(\"cassandra.host=${cfg.host}\") }\n    )\n}\n\nmysql {\n    MySqlSystemOptions(\n        cleanup = { ops -> ops.execute(\"TRUNCATE orders\") },\n        configureExposedConfiguration = { cfg -> listOf(\"spring.datasource.url=${cfg.jdbcUrl}\") }\n    )\n}\n\nmssql {\n    MsSqlSystemOptions(\n        cleanup = { ops -> ops.execute(\"TRUNCATE TABLE orders\") },\n        configureExposedConfiguration = { cfg -> listOf(\"spring.datasource.url=${cfg.jdbcUrl}\") }\n    )\n}\n```\n\nCleanup client types per system:\n\n| System | Cleanup parameter type |\n|---|---|\n| PostgreSQL | `NativeSqlOperations` |\n| MySQL | `NativeSqlOperations` |\n| MSSQL | `NativeSqlOperations` |\n| Cassandra | `CqlSession` |\n| MongoDB | `MongoClient` |\n| Redis | `RedisClient` |\n| Elasticsearch | `ElasticsearchClient` |\n| Couchbase | `Cluster` |\n| Kafka | `Admin` |\n\nWireMock uses event-driven cleanup instead:\n\n```kotlin\nwiremock {\n    WireMockSystemOptions(\n        removeStubAfterRequestMatched = true,  // Auto-remove stubs after match\n        afterStubRemoved = { serveEvent, stubLog -> /* optional callback */ },\n        configureExposedConfiguration = { cfg -> listOf(\"service.url=${cfg.baseUrl}\") }\n    )\n}\n```\n\nThe `cleanup` lambda also works with `.provided()` (external instances):\n\n```kotlin\npostgresql {\n    PostgresqlOptions.provided(\n        jdbcUrl = \"jdbc:postgresql://localhost:5432/testdb\",\n        host = \"localhost\",\n        port = 5432,\n        cleanup = { ops -> ops.execute(\"TRUNCATE orders, users\") },\n        configureExposedConfiguration = { cfg -> listOf(\"spring.datasource.url=${cfg.jdbcUrl}\") }\n    )\n}\n```\n\n## Keep dependencies running\n\nKeeps containers alive between test runs for faster local iteration. Disable in CI.\n\n```kotlin\nStove {\n    keepDependenciesRunning()\n}.with { /* systems */ }.run()\n```\n"
  },
  {
    "path": ".claude/skills/stove/tracing.md",
    "content": "# Tracing Configuration\n\nTracing captures the full execution call chain inside your application, shown on test failure. Requires `stove-tracing` and `stove-extensions-kotest` or `stove-extensions-junit`.\n\n## 1. Enable span receiver\n\nAdd inside `Stove().with { }`:\n\n```kotlin\ntracing { enableSpanReceiver() }\n```\n\n## 2. Attach the OpenTelemetry agent\n\n### Gradle Plugin (default)\n\n```kotlin\nplugins { id(\"com.trendyol.stove.tracing\") version \"$stoveVersion\" }\n\nstoveTracing {\n    serviceName.set(\"my-service\")\n    testTaskNames.set(listOf(\"e2eTest\"))\n}\n```\n\n### buildSrc alternative\n\nCopy `StoveTracingConfiguration.kt` from the Stove repo to `buildSrc/src/main/kotlin/`, then use direct assignment:\n\n```kotlin\nimport com.trendyol.stove.gradle.stoveTracing\n\nstoveTracing {\n    serviceName = \"my-service\"\n    testTaskNames = listOf(\"e2eTest\")\n}\n```\n\n## 3. Plugin options\n\n| Option | Default | Description |\n|---|---|---|\n| `serviceName` | `\"stove-traced-app\"` | Service name in traces |\n| `enabled` | `true` | Toggle tracing |\n| `testTaskNames` | `[]` | Apply to specific tasks (empty = all) |\n| `otelAgentVersion` | `\"2.24.0\"` | OTel Java Agent version |\n| `disabledInstrumentations` | `[]` | Instrumentations to disable (e.g., `jdbc`, `hibernate`) |\n| `additionalInstrumentations` | `[]` | Extra instrumentations |\n| `customAnnotations` | `[]` | Custom annotation classes to instrument |\n| `protocol` | `\"grpc\"` | OTLP protocol |\n| `captureHttpHeaders` | `true` | Capture HTTP headers in spans |\n| `captureExperimentalTelemetry` | `true` | Enable experimental HTTP telemetry |\n| `bspScheduleDelay` | `100` | Batch span processor delay in ms (lower = faster export) |\n| `bspMaxBatchSize` | `1` | Batch size for span export (1 = immediate) |\n\n## 4. Runtime tracing config\n\nConfigure inside `Stove().with { }`:\n\n```kotlin\ntracing {\n    enableSpanReceiver()              // Required\n    spanCollectionTimeout(10.seconds) // Wait time for spans (default: 5s)\n    maxSpansPerTrace(2000)            // Cap per trace (default: 1000)\n    spanFilter { span ->              // Filter collected spans\n        !span.operationName.contains(\"health-check\")\n    }\n}\n```\n\n## 5. Trace validation DSL\n\n```kotlin\ntracing {\n    // Span assertions\n    shouldContainSpan(\"OrderService.processOrder\")\n    shouldContainSpanMatching { it.operationName.contains(\"Repository\") }\n    shouldNotContainSpan(\"AdminService.delete\")\n    shouldNotHaveFailedSpans()\n    shouldHaveFailedSpan(\"PaymentGateway.charge\")\n    shouldHaveSpanWithAttribute(\"http.method\", \"GET\")\n    shouldHaveSpanWithAttributeContaining(\"http.url\", \"/api/users\")\n\n    // Performance\n    executionTimeShouldBeLessThan(500.milliseconds)\n    executionTimeShouldBeGreaterThan(10.milliseconds)\n    spanCountShouldBe(10)\n    spanCountShouldBeAtLeast(5)\n    spanCountShouldBeAtMost(20)\n\n    // Debugging helpers\n    println(renderTree())     // Hierarchical tree view\n    println(renderSummary())  // Compact summary\n    val failed = getFailedSpans()\n    val duration = getTotalDuration()\n    val span = findSpanByName(\"OrderService.process\")\n\n    // Wait for async spans\n    waitForSpans(expectedCount = 5, timeoutMs = 3000)\n}\n```\n"
  },
  {
    "path": ".claude/skills/stove/writing-tests.md",
    "content": "# Writing Tests Reference\n\n## Contents\n- [HTTP requests](#http-requests)\n- [HTTP streaming](#http-streaming)\n- [PostgreSQL queries](#postgresql-queries)\n- [MySQL queries](#mysql-queries)\n- [MSSQL queries](#mssql-queries)\n- [Cassandra assertions](#cassandra-assertions)\n- [MongoDB assertions](#mongodb-assertions)\n- [Redis assertions](#redis-assertions)\n- [Elasticsearch assertions](#elasticsearch-assertions)\n- [Couchbase assertions](#couchbase-assertions)\n- [Kafka assertions](#kafka-assertions)\n- [WireMock mocking](#wiremock-mocking)\n- [gRPC Mock](#grpc-mock)\n- [gRPC Client](#grpc-client)\n- [Bridge (DI access)](#bridge-di-access)\n- [Trace validation](#trace-validation)\n- [Keyed system tests](#keyed-system-tests)\n- [Smoke testing (providedApplication)](#smoke-testing-providedapplication)\n- [Multi-system test](#multi-system-test)\n- [Anti-patterns](#anti-patterns)\n\nAll tests use the `stove { }` entry point.\n\n## HTTP requests\n\n```kotlin\n// POST with typed response\nhttp {\n    postAndExpectBody<OrderResponse>(\n        uri = \"/orders\",\n        body = CreateOrderRequest(userId = \"u1\", amount = 99.99).some()\n    ) { response ->\n        response.status shouldBe 201\n        response.body().orderId shouldNotBe null\n    }\n}\n\n// POST expecting JSON directly\nhttp {\n    postAndExpectJson<OrderResponse>(\"/orders\") {\n        CreateOrderRequest(userId = \"u1\", amount = 99.99)\n    } { order ->\n        order.id shouldNotBe null\n    }\n}\n\n// GET\nhttp {\n    get<UserResponse>(\"/users/123\") { user ->\n        user.name shouldBe \"John\"\n    }\n}\n\n// GET with full response (status + headers + body)\nhttp {\n    getResponse<UserResponse>(\"/users/123\") { response ->\n        response.status shouldBe 200\n        response.headers[\"Content-Type\"] shouldContain \"application/json\"\n        response.body().id shouldBe 123\n    }\n}\n\n// GET list\nhttp {\n    getMany<ProductResponse>(\"/products\", queryParams = mapOf(\"page\" to \"1\")) { products ->\n        products.size shouldBe 10\n    }\n}\n\n// PUT\nhttp {\n    putAndExpectBody<ProductResponse>(\n        uri = \"/products/456\",\n        body = UpdateProductRequest(price = 899.99).some()\n    ) { response ->\n        response.status shouldBe 200\n        response.body().price shouldBe 899.99\n    }\n}\n\n// DELETE\nhttp {\n    deleteAndExpectBodilessResponse(\"/users/123\") { response ->\n        response.status shouldBe 204\n    }\n}\n\n// Multipart upload\nhttp {\n    postMultipartAndExpectResponse<UploadResponse>(\n        uri = \"/products/import\",\n        body = listOf(\n            StoveMultiPartContent.Text(\"name\", \"Laptop\"),\n            StoveMultiPartContent.File(\"file\", \"data.csv\", csvBytes, MediaType.APPLICATION_OCTET_STREAM_VALUE)\n        )\n    ) { response ->\n        response.status shouldBe 200\n    }\n}\n\n// WebSocket\nhttp {\n    webSocket(\"/chat\") {\n        send(\"Hello!\")\n        val response = receiveText()\n        response shouldBe \"Echo: Hello!\"\n    }\n\n    // Collect multiple messages\n    webSocket(\"/events\") {\n        val messages = collectTexts(count = 5, timeout = 10.seconds)\n        messages.size shouldBe 5\n    }\n\n    // Streaming with Flow\n    webSocket(\"/stream\") {\n        incomingTexts().take(10).toList().size shouldBe 10\n    }\n}\n```\n\n## HTTP streaming\n\nFor JSON streaming (NDJSON) endpoints, use Flow-based extensions on `HttpStatement`:\n\n```kotlin\nhttp {\n    // Read NDJSON stream line by line, transform each line\n    val items = client().prepareGet(\"/api/events/stream\").readJsonTextStream { line ->\n        StoveSerde.jackson.default.readValue(line, EventResponse::class.java)\n    }.toList()\n\n    items.size shouldBeGreaterThan 0\n\n    // Read stream as ByteReadChannel for binary processing\n    client().prepareGet(\"/api/binary/stream\").readJsonContentStream { channel ->\n        channel.readRemaining().readText()\n    }.toList().shouldNotBeEmpty()\n}\n\n// Serialize items to NDJSON for request body\nval body = StoveSerde.jackson.anyByteArraySerde().serializeToStreamJson(\n    listOf(Event(\"e1\"), Event(\"e2\"), Event(\"e3\"))\n)\n```\n\n## PostgreSQL queries\n\n```kotlin\npostgresql {\n    // Execute DDL/DML\n    shouldExecute(\n        \"\"\"\n        INSERT INTO products (name, price) VALUES ('Laptop', 999.99)\n        \"\"\".trimIndent()\n    )\n\n    // Query with typed mapper\n    shouldQuery<OrderRow>(\n        query = \"SELECT * FROM orders WHERE user_id = '$userId'\",\n        mapper = { row ->\n            OrderRow(\n                id = row.string(\"id\"),\n                userId = row.string(\"user_id\"),\n                amount = row.double(\"amount\"),\n                status = row.string(\"status\")\n            )\n        }\n    ) { orders ->\n        orders.size shouldBe 1\n        orders.first().status shouldBe \"CONFIRMED\"\n    }\n}\n```\n\n## MySQL queries\n\nSame API as PostgreSQL — uses `shouldExecute` and `shouldQuery` with a row mapper:\n\n```kotlin\nmysql {\n    shouldExecute(\"INSERT INTO products (name, price) VALUES ('Laptop', 999.99)\")\n\n    shouldQuery<ProductRow>(\n        query = \"SELECT * FROM products WHERE name = 'Laptop'\",\n        mapper = { row -> ProductRow(row.string(\"name\"), row.double(\"price\")) }\n    ) { products ->\n        products.size shouldBe 1\n    }\n}\n```\n\n## MSSQL queries\n\nSame API as PostgreSQL/MySQL:\n\n```kotlin\nmssql {\n    shouldExecute(\"INSERT INTO orders (id, status) VALUES ('o1', 'NEW')\")\n\n    shouldQuery<OrderRow>(\n        query = \"SELECT * FROM orders WHERE id = 'o1'\",\n        mapper = { row -> OrderRow(row.string(\"id\"), row.string(\"status\")) }\n    ) { orders ->\n        orders.first().status shouldBe \"NEW\"\n    }\n}\n```\n\n## Cassandra assertions\n\n```kotlin\ncassandra {\n    // Execute CQL\n    shouldExecute(\"INSERT INTO orders (id, user_id, status) VALUES ('o1', 'u1', 'NEW')\")\n\n    // Query with ResultSet assertion\n    shouldQuery(\"SELECT * FROM orders WHERE id = 'o1'\") { resultSet ->\n        val row = resultSet.one()!!\n        row.getString(\"status\") shouldBe \"NEW\"\n    }\n\n    // Execute with BoundStatement\n    shouldExecute(session().prepare(\"DELETE FROM orders WHERE id = ?\").bind(\"o1\"))\n\n    // Query with BoundStatement\n    shouldQuery(session().prepare(\"SELECT * FROM orders WHERE id = ?\").bind(\"o1\")) { rs ->\n        rs.one() shouldBe null\n    }\n\n    // Simulate downtime\n    pause()\n    // ... test resilience ...\n    unpause()\n}\n\n// Direct session access\ncassandra {\n    session().execute(\"TRUNCATE orders\")\n}\n```\n\n## MongoDB assertions\n\n```kotlin\nmongodb {\n    // Save a document\n    save(Order(id = \"o1\", userId = \"u1\", amount = 99.99))\n\n    // Save to specific collection\n    save(Order(id = \"o2\", userId = \"u2\", amount = 50.0), collection = \"archived_orders\")\n\n    // Get by ObjectId\n    shouldGet<Order>(objectId = \"o1\") { order ->\n        order.amount shouldBe 99.99\n    }\n\n    // Query with filter string\n    shouldQuery<Order>(query = \"\"\"{ \"userId\": \"u1\" }\"\"\") { orders ->\n        orders.size shouldBe 1\n        orders.first().status shouldBe \"NEW\"\n    }\n\n    // Delete\n    shouldDelete(objectId = \"o1\")\n\n    // Verify deletion\n    shouldNotExist(objectId = \"o1\")\n\n    // Simulate downtime\n    pause()\n    unpause()\n}\n\n// Direct client access\nmongodb {\n    client().getDatabase(\"testdb\").getCollection(\"orders\").drop()\n}\n```\n\n## Redis assertions\n\nRedis uses the Lettuce client directly via `client()`:\n\n```kotlin\nredis {\n    // All operations via the Lettuce RedisClient\n    val connection = client().connect()\n    val commands = connection.sync()\n\n    commands.set(\"order:o1\", \"\"\"{\"status\":\"NEW\"}\"\"\")\n    commands.get(\"order:o1\") shouldNotBe null\n    commands.del(\"order:o1\")\n\n    connection.close()\n}\n\n// Simulate downtime\nredis {\n    pause()\n    // ... test resilience ...\n    unpause()\n}\n```\n\n## Elasticsearch assertions\n\n```kotlin\nelasticsearch {\n    // Save a document\n    save(id = \"p1\", instance = Product(\"p1\", \"Laptop\", 999.99), index = \"products\")\n\n    // Get by key\n    shouldGet<Product>(index = \"products\", key = \"p1\") { product ->\n        product.name shouldBe \"Laptop\"\n    }\n\n    // Query with JSON string\n    shouldQuery<Product>(\n        query = \"\"\"{ \"match\": { \"name\": \"Laptop\" } }\"\"\",\n        index = \"products\"\n    ) { products ->\n        products.size shouldBe 1\n    }\n\n    // Query with Elasticsearch Query DSL object\n    shouldQuery<Product>(\n        query = Query.of { q -> q.match { m -> m.field(\"name\").query(\"Laptop\") } }\n    ) { products ->\n        products.shouldNotBeEmpty()\n    }\n\n    // Delete\n    shouldDelete(key = \"p1\", index = \"products\")\n\n    // Verify deletion\n    shouldNotExist(key = \"p1\", index = \"products\")\n\n    // Simulate downtime\n    pause()\n    unpause()\n}\n\n// Direct client access\nelasticsearch {\n    client().indices().create { it.index(\"new-index\") }\n}\n```\n\n## Couchbase assertions\n\n```kotlin\ncouchbase {\n    // Save to default collection\n    saveToDefaultCollection(id = \"o1\", instance = Order(\"o1\", \"u1\", 99.99))\n\n    // Save to specific collection\n    save(collection = \"archived\", id = \"o2\", instance = Order(\"o2\", \"u2\", 50.0))\n\n    // Get by key (default collection)\n    shouldGet<Order>(key = \"o1\") { order ->\n        order.amount shouldBe 99.99\n    }\n\n    // Get from specific collection\n    shouldGet<Order>(collection = \"archived\", key = \"o2\") { order ->\n        order.userId shouldBe \"u2\"\n    }\n\n    // N1QL query\n    shouldQuery<Order>(query = \"SELECT * FROM `test-bucket` WHERE userId = 'u1'\") { orders ->\n        orders.size shouldBe 1\n    }\n\n    // Delete (default collection)\n    shouldDelete(key = \"o1\")\n\n    // Delete from specific collection\n    shouldDelete(collection = \"archived\", key = \"o2\")\n\n    // Verify deletion\n    shouldNotExist(key = \"o1\")\n    shouldNotExist(collection = \"archived\", key = \"o2\")\n\n    // Simulate downtime\n    pause()\n    unpause()\n}\n\n// Direct cluster/bucket access\ncouchbase {\n    cluster().queryIndexes().createPrimaryIndex(\"test-bucket\")\n    bucket().defaultCollection().upsert(\"doc1\", JsonObject.create())\n}\n```\n\n## Kafka assertions\n\n```kotlin\n// Verify published\nkafka {\n    shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.orderId == orderId && actual.amount == 99.99\n    }\n}\n\n// Verify consumed (stove-spring-kafka only)\nkafka {\n    shouldBeConsumed<OrderCreatedEvent>(atLeastIn = 20.seconds) {\n        actual.orderId == orderId\n    }\n}\n\n// Publish a message\nkafka {\n    publish(\n        topic = \"order-events\",\n        message = OrderCreated(orderId = \"456\", amount = 100.0),\n        key = \"order-456\".some()\n    )\n}\n\n// Verify failed handling\nkafka {\n    shouldBeFailed<FailingEvent>(atLeastIn = 10.seconds) {\n        actual.id == 5L && reason is BusinessException\n    }\n}\n\n// Verify retries (stove-spring-kafka only)\nkafka {\n    shouldBeRetried<FailingEvent>(atLeastIn = 1.minutes, times = 3) {\n        actual.id == \"789\"\n    }\n}\n\n// Access message metadata\nkafka {\n    shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.orderId == orderId &&\n        metadata.topic == \"order-events\" &&\n        metadata.headers[\"correlation-id\"] != null\n    }\n}\n```\n\n## WireMock mocking\n\n```kotlin\nwiremock {\n    mockGet(\n        url = \"/inventory/$productId\",\n        statusCode = 200,\n        responseBody = InventoryResponse(available = true).some()\n    )\n\n    mockPost(\n        url = \"/payments/charge\",\n        statusCode = 200,\n        responseBody = PaymentResult(success = true).some()\n    )\n}\n\n// PUT, PATCH, DELETE mocks\nwiremock {\n    mockPut(url = \"/products/123\", statusCode = 200,\n        responseBody = Product(\"123\", \"Updated\", 899.99).some())\n    mockPatch(url = \"/users/123\", statusCode = 200,\n        requestBody = mapOf(\"email\" to \"new@example.com\").some())\n    mockDelete(url = \"/products/123\", statusCode = 204)\n    mockHead(url = \"/products/exists/123\", statusCode = 200)\n}\n\n// Partial body matching — match specific fields, ignore the rest\nwiremock {\n    mockPostContaining(\n        url = \"/api/orders\",\n        requestContaining = mapOf(\"productId\" to 123),\n        statusCode = 201,\n        responseBody = OrderResponse(orderId = \"order-123\").some()\n    )\n\n    // Deep nested matching with dot notation\n    mockPostContaining(\n        url = \"/api/checkout\",\n        requestContaining = mapOf(\n            \"order.customer.id\" to \"cust-123\",\n            \"order.payment.method\" to \"credit_card\"\n        ),\n        statusCode = 200\n    )\n}\n\n// Sequential responses (behavioral mocking)\nwiremock {\n    behaviourFor(\"/api/service\", WireMock::get) {\n        initially { aResponse().withStatus(503) }\n        then { aResponse().withStatus(503) }\n        then { aResponse().withStatus(200).withBody(it.serialize(result)) }\n    }\n}\n```\n\n## gRPC Mock\n\n```kotlin\ngrpcMock {\n    // Unary\n    mockUnary(\n        serviceName = \"frauddetection.FraudDetectionService\",\n        methodName = \"CheckFraud\",\n        response = CheckFraudResponse.newBuilder()\n            .setIsFraudulent(false).setRiskScore(0.15).build()\n    )\n\n    // With request matching\n    mockUnary(\n        serviceName = \"users.UserService\",\n        methodName = \"GetUser\",\n        requestMatcher = RequestMatcher.ExactMessage(\n            GetUserRequest.newBuilder().setUserId(\"123\").build()\n        ),\n        response = GetUserResponse.newBuilder().setName(\"John\").build()\n    )\n\n    // With authentication\n    mockUnary(\n        serviceName = \"secure.SecureService\",\n        methodName = \"GetSecret\",\n        metadataMatcher = MetadataMatcher.BearerToken(\"valid-token\"),\n        response = SecretResponse.newBuilder().setData(\"confidential\").build()\n    )\n\n    // Server streaming\n    mockServerStream(\n        serviceName = \"streaming.ItemService\",\n        methodName = \"ListItems\",\n        responses = listOf(item1, item2, item3)\n    )\n\n    // Bidirectional streaming\n    mockBidiStream(\n        serviceName = \"chat.ChatService\",\n        methodName = \"Chat\"\n    ) { requestFlow ->\n        requestFlow.map { bytes ->\n            val req = ChatMessage.parseFrom(bytes)\n            ChatMessage.newBuilder().setMessage(\"Echo: ${req.message}\").build()\n        }\n    }\n\n    // Error\n    mockError(\n        serviceName = \"users.UserService\",\n        methodName = \"GetUser\",\n        status = Status.Code.NOT_FOUND,\n        message = \"User not found\"\n    )\n}\n```\n\n## gRPC Client\n\nFor testing your own gRPC server:\n\n```kotlin\ngrpc {\n    channel<OrderQueryServiceGrpcKt.OrderQueryServiceCoroutineStub> {\n        val response = getOrder(\n            GetOrderRequest.newBuilder().setOrderId(orderId).build()\n        )\n        response.found shouldBe true\n        response.order.status shouldBe \"CONFIRMED\"\n    }\n}\n```\n\n## Bridge (DI access)\n\n```kotlin\n// Single bean\nusing<OrderService> {\n    val order = getOrderByUserId(userId)\n    order shouldNotBe null\n    order!!.status shouldBe OrderStatus.CONFIRMED\n}\n\n// Multiple beans (up to 5 supported)\nusing<UserService, OrderService> { userService, orderService ->\n    val user = userService.findById(123)\n    val orders = orderService.findByUserId(123)\n    orders.size shouldBeGreaterThan 0\n}\n\nusing<A, B, C> { a, b, c -> /* ... */ }\n```\n\n## Trace validation\n\n```kotlin\ntracing {\n    // Span assertions\n    shouldContainSpan(\"OrderService.processOrder\")\n    shouldContainSpanMatching { it.operationName.contains(\"Repository\") }\n    shouldNotContainSpan(\"AdminService.delete\")\n    shouldNotHaveFailedSpans()\n    shouldHaveFailedSpan(\"PaymentGateway.charge\")\n    shouldHaveSpanWithAttribute(\"http.method\", \"GET\")\n\n    // Performance assertions\n    executionTimeShouldBeLessThan(500.milliseconds)\n    spanCountShouldBeAtLeast(5)\n\n    // Debugging\n    println(renderTree())    // Hierarchical trace view\n    println(renderSummary()) // Compact summary\n}\n```\n\n## Keyed system tests\n\nAccess keyed systems by passing the `SystemKey` to the validation DSL:\n\n```kotlin\n// Given keys defined in setup:\n// object AppDb : SystemKey\n// object AnalyticsDb : SystemKey\n// object PaymentService : SystemKey\n\ntest(\"should write to both databases\") {\n    stove {\n        http {\n            postAndExpectBody<OrderResponse>(\n                uri = \"/orders\",\n                body = CreateOrderRequest(\"u1\", 99.99).some()\n            ) { it.status shouldBe 201 }\n        }\n\n        // Assert against the app database\n        postgresql(AppDb) {\n            shouldQuery<OrderRow>(\n                query = \"SELECT * FROM orders WHERE user_id = 'u1'\",\n                mapper = { row -> OrderRow(row.string(\"id\"), row.string(\"status\")) }\n            ) { it.size shouldBe 1 }\n        }\n\n        // Assert against the analytics database\n        postgresql(AnalyticsDb) {\n            shouldQuery<AnalyticsRow>(\n                query = \"SELECT * FROM events WHERE user_id = 'u1'\",\n                mapper = { row -> AnalyticsRow(row.string(\"event_type\")) }\n            ) { it.first().eventType shouldBe \"ORDER_CREATED\" }\n        }\n    }\n}\n\n// Keyed WireMock and HTTP\ntest(\"should call payment and inventory services\") {\n    stove {\n        wiremock(PaymentService) {\n            mockPost(\"/charge\", 200, PaymentResult(true).some())\n        }\n        wiremock(InventoryService) {\n            mockGet(\"/stock/item-1\", 200, StockResponse(10).some())\n        }\n\n        http {\n            postAndExpectBody<OrderResponse>(\"/orders\", body = order.some()) {\n                it.status shouldBe 201\n            }\n        }\n    }\n}\n```\n\nAll systems support keyed access: `postgresql(key)`, `mysql(key)`, `mssql(key)`, `cassandra(key)`, `mongodb(key)`, `redis(key)`, `elasticsearch(key)`, `couchbase(key)`, `kafka(key)`, `wiremock(key)`, `grpcMock(key)`, `grpc(key)`, `http(key)`.\n\n## Smoke testing (providedApplication)\n\nWith `providedApplication()`, test a remote/deployed application without starting it locally. No `Bridge`/`using<T>` — only infrastructure assertions:\n\n```kotlin\ntest(\"staging smoke test — order flow\") {\n    stove {\n        val userId = \"smoke-${UUID.randomUUID()}\"\n\n        // Hit the remote API\n        http {\n            postAndExpectBody<OrderResponse>(\n                uri = \"/api/orders\",\n                body = CreateOrderRequest(userId, 49.99).some()\n            ) { response ->\n                response.status shouldBe 201\n            }\n        }\n\n        // Verify side effects in the remote database\n        postgresql(AppDb) {\n            shouldQuery<OrderRow>(\n                query = \"SELECT * FROM orders WHERE user_id = '$userId'\",\n                mapper = { row -> OrderRow(row.string(\"id\"), row.string(\"status\")) }\n            ) { orders ->\n                orders.size shouldBe 1\n                orders.first().status shouldBe \"CONFIRMED\"\n            }\n        }\n\n        // Verify Kafka event on the remote cluster\n        kafka {\n            shouldBePublished<OrderCreatedEvent>(10.seconds) {\n                actual.userId == userId\n            }\n        }\n    }\n}\n```\n\n## Multi-system test\n\n```kotlin\ntest(\"complete order flow\") {\n    stove {\n        val userId = \"user-${UUID.randomUUID()}\"\n        var orderId: String? = null\n\n        grpcMock {\n            mockUnary(\n                serviceName = \"frauddetection.FraudDetectionService\",\n                methodName = \"CheckFraud\",\n                response = CheckFraudResponse.newBuilder()\n                    .setIsFraudulent(false).build()\n            )\n        }\n\n        wiremock {\n            mockGet(\"/inventory/macbook\", 200,\n                responseBody = InventoryResponse(\"macbook\", true, 10).some())\n            mockPost(\"/payments/charge\", 200,\n                responseBody = PaymentResult(true, \"txn-123\", 2499.99).some())\n        }\n\n        http {\n            postAndExpectBody<OrderResponse>(\n                uri = \"/api/orders\",\n                body = CreateOrderRequest(userId, \"macbook\", 2499.99).some()\n            ) { response ->\n                response.status shouldBe 201\n                orderId = response.body().orderId\n            }\n        }\n\n        postgresql {\n            shouldQuery<OrderRow>(\n                query = \"SELECT * FROM orders WHERE user_id = '$userId'\",\n                mapper = { row ->\n                    OrderRow(row.string(\"id\"), row.string(\"user_id\"),\n                        row.string(\"product_id\"), row.double(\"amount\"),\n                        row.string(\"status\"))\n                }\n            ) { orders ->\n                orders.size shouldBe 1\n                orders.first().status shouldBe \"CONFIRMED\"\n            }\n        }\n\n        kafka {\n            shouldBePublished<OrderCreatedEvent>(10.seconds) {\n                actual.userId == userId\n            }\n        }\n\n        grpc {\n            channel<OrderQueryServiceGrpcKt.OrderQueryServiceCoroutineStub> {\n                val response = getOrder(\n                    GetOrderRequest.newBuilder().setOrderId(orderId!!).build()\n                )\n                response.order.status shouldBe \"CONFIRMED\"\n            }\n        }\n\n        using<OrderService> {\n            getOrderByUserId(userId)!!.status shouldBe OrderStatus.CONFIRMED\n        }\n    }\n}\n```\n\n## Anti-patterns\n\n| Don't | Do |\n|---|---|\n| `Thread.sleep(5000)` | `shouldBePublished<Event>(atLeastIn = 10.seconds) { ... }` |\n| Hardcoded IDs `\"order-123\"` | `UUID.randomUUID().toString()` |\n| Shared mutable state | Independent tests with unique data |\n| Only assert `status shouldBe 200` | Assert response body, DB state, events |\n| Call real external services | Use WireMock / gRPC Mock |\n| Configure Stove per test class | Single `AbstractProjectConfig` |\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ninsert_final_newline = true\nktlint_standard_package-name = disabled\nktlint_standard_filename = disabled\nktlint_standard_no-wildcard-imports = disabled\nktlint_standard_multiline-expression-wrapping = disabled\nktlint_standard_string-template-indent = disabled\nktlint_standard_function-signature = disabled\n\n[{*.kt,*.kts}]\nindent_style = space\nmax_line_length = 140\nindent_size = 2\nij_kotlin_code_style_defaults = KOTLIN_OFFICIAL\nij_continuation_indent_size = 2\nij_kotlin_allow_trailing_comma = false\nij_kotlin_allow_trailing_comma_on_call_site = false\nij_kotlin_name_count_to_use_star_import = 2\nij_kotlin_name_count_to_use_star_import_for_members = 2\n\n[{**/test/**.kt,**/test-e2e/**.kt,**/test-int/**.kt}]\nmax_line_length = 240\nktlint_standard_no-consecutive-comments = disabled"
  },
  {
    "path": ".gitattributes",
    "content": "#\n# https://help.github.com/articles/dealing-with-line-endings/\n#\n# Linux start script should use lf\n/gradlew        text eol=lf\n\n# These are Windows script files and should use crlf\n*.bat           text eol=crlf\n\n"
  },
  {
    "path": ".github/workflows/build-jvm-recipes.yml",
    "content": "name: Build JVM Recipes\n\non:\n  push:\n    branches: [main]\n    paths:\n      - 'recipes/jvm/**'\n  pull_request:\n    branches: [main]\n    paths:\n      - 'recipes/jvm/**'\n\n# Cancel in-progress runs for the same branch\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    permissions:\n      checks: write\n      pull-requests: write\n    services:\n      docker:\n        image: docker:dind\n        options: --privileged\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          gradle-version: current\n          cache-read-only: false\n          cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}\n\n      - name: Cache Gradle dependencies\n        uses: actions/cache@v5\n        with:\n          path: |\n            ~/.gradle/caches\n            ~/.gradle/wrapper\n          key: gradle-jvm-recipes-${{ runner.os }}-${{ hashFiles('recipes/jvm/**/gradle/wrapper/gradle-wrapper.properties', 'recipes/jvm/**/gradle/libs.versions.toml') }}\n          restore-keys: |\n            gradle-jvm-recipes-${{ runner.os }}-\n\n      # Cache Docker images for testcontainers\n      - name: Cache Docker images\n        uses: actions/cache@v5\n        with:\n          path: /var/lib/docker\n          key: docker-jvm-recipes-${{ runner.os }}-${{ hashFiles('recipes/jvm/**/gradle/libs.versions.toml') }}\n          restore-keys: |\n            docker-jvm-recipes-${{ runner.os }}-\n\n      # Run all tasks in a single Gradle invocation\n      - name: Build, Test, and E2E Test\n        run: |\n          gradle -p recipes/jvm build test e2eTest \\\n            --build-cache \\\n            --no-daemon \\\n            -Dorg.gradle.parallel=true\n\n      # Upload test results\n      - name: Upload test results\n        if: always()\n        uses: actions/upload-artifact@v7\n        with:\n          name: jvm-recipes-test-results\n          path: |\n            recipes/jvm/**/build/test-results/\n            recipes/jvm/**/build/reports/tests/\n          retention-days: 7\n\n      # Publish test report for PRs\n      - name: Publish Test Report\n        uses: mikepenz/action-junit-report@v6\n        if: always() && github.event_name == 'pull_request'\n        with:\n          report_paths: \"recipes/jvm/**/build/test-results/**/*.xml\"\n          check_name: \"JVM Recipes Test Results\"\n          detailed_summary: true\n          include_passed: false\n"
  },
  {
    "path": ".github/workflows/build-process-recipes.yml",
    "content": "name: Build Process Recipes\n\non:\n  push:\n    branches: [main]\n    paths:\n      - 'recipes/process/**'\n  pull_request:\n    branches: [main]\n    paths:\n      - 'recipes/process/**'\n\n# Cancel in-progress runs for the same branch\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  go-showcase:\n    runs-on: ubuntu-latest\n    permissions:\n      checks: write\n      pull-requests: write\n    services:\n      docker:\n        image: docker:dind\n        options: --privileged\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - uses: actions/setup-go@v6\n        with:\n          go-version-file: recipes/process/golang/go-showcase/go.mod\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          gradle-version: current\n          cache-read-only: false\n          cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}\n\n      - name: Cache Gradle dependencies\n        uses: actions/cache@v5\n        with:\n          path: |\n            ~/.gradle/caches\n            ~/.gradle/wrapper\n          key: gradle-process-recipes-${{ runner.os }}-${{ hashFiles('recipes/process/**/gradle/wrapper/gradle-wrapper.properties') }}\n          restore-keys: |\n            gradle-process-recipes-${{ runner.os }}-\n\n      # Cache Docker images for testcontainers\n      - name: Cache Docker images\n        uses: actions/cache@v5\n        with:\n          path: /var/lib/docker\n          key: docker-process-recipes-${{ runner.os }}\n          restore-keys: |\n            docker-process-recipes-${{ runner.os }}-\n\n      - name: Run E2E Tests (all Kafka libraries)\n        env:\n          GOTOOLCHAIN: auto\n        run: |\n          GO_EXECUTABLE=\"$(command -v go)\"\n          export GO_EXECUTABLE\n          \"$GO_EXECUTABLE\" version\n          gradle -p recipes/process/golang/go-showcase e2eTest \\\n            --build-cache \\\n            --no-daemon\n\n      - name: Run Container E2E Tests (sarama)\n        env:\n          GOTOOLCHAIN: auto\n        run: |\n          GO_EXECUTABLE=\"$(command -v go)\"\n          export GO_EXECUTABLE\n          \"$GO_EXECUTABLE\" version\n          gradle -p recipes/process/golang/go-showcase e2eTest-container \\\n            --build-cache \\\n            --no-daemon && \\\n          gradle -p recipes/process/golang/go-showcase removeContainerImage \\\n            --no-daemon\n\n      # Upload test results\n      - name: Upload test results\n        if: always()\n        uses: actions/upload-artifact@v7\n        with:\n          name: process-recipes-test-results\n          path: |\n            recipes/process/**/build/test-results/\n            recipes/process/**/build/reports/tests/\n          retention-days: 7\n\n      # Publish test report for PRs\n      - name: Publish Test Report\n        uses: mikepenz/action-junit-report@v6\n        if: always() && github.event_name == 'pull_request'\n        with:\n          report_paths: \"recipes/process/**/build/test-results/**/*.xml\"\n          check_name: \"Process Recipes Test Results\"\n          detailed_summary: true\n          include_passed: false\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  push:\n    branches: [main]\n    paths:\n      - 'examples/**'\n      - 'lib/**'\n      - 'starters/**'\n      - 'gradle/**'\n      - 'build.gradle.kts'\n      - 'settings.gradle.kts'\n      - 'buildSrc/**'\n  pull_request:\n    branches: [main]\n    paths:\n      - 'examples/**'\n      - 'lib/**'\n      - 'starters/**'\n      - 'gradle/**'\n      - 'build.gradle.kts'\n      - 'settings.gradle.kts'\n      - 'buildSrc/**'\n\n# Cancel in-progress runs for the same branch\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    permissions:\n      checks: write\n      id-token: write\n      issues: write\n      pull-requests: write\n    services:\n      docker:\n        image: docker:dind\n        options: --privileged\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 17\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          gradle-version: current\n          cache-read-only: false\n          cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}\n\n      - name: Cache Gradle dependencies\n        uses: actions/cache@v5\n        with:\n          path: |\n            ~/.gradle/caches\n            ~/.gradle/wrapper\n          key: gradle-${{ runner.os }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties', '**/gradle/libs.versions.toml') }}\n          restore-keys: |\n            gradle-${{ runner.os }}-\n\n      # Cache Docker images for testcontainers\n      - name: Cache Docker images\n        uses: actions/cache@v5\n        with:\n          path: /var/lib/docker\n          key: docker-${{ runner.os }}-${{ hashFiles('**/gradle/libs.versions.toml') }}\n          restore-keys: |\n            docker-${{ runner.os }}-\n\n      # Run all tasks in a single Gradle invocation for optimal build time\n      - name: Build, Test, and Coverage\n        run: |\n          gradle build test koverXmlReport \\\n            --build-cache \\\n            --configuration-cache \\\n            --parallel \\\n            --no-daemon\n\n      # Upload test results\n      - name: Upload test results\n        if: always()\n        uses: actions/upload-artifact@v7\n        with:\n          name: test-results\n          path: |\n            **/build/test-results/test/\n            **/build/reports/tests/\n          retention-days: 7\n\n      # Upload coverage to Codecov\n      - name: Upload coverage to Codecov\n        if: github.repository == 'Trendyol/stove'\n        uses: codecov/codecov-action@v6\n        with:\n          fail_ci_if_error: false\n        env:\n          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n\n      # Publish test report for PRs\n      - name: Publish Test Report\n        uses: mikepenz/action-junit-report@v6\n        if: always() && github.event_name == 'pull_request'\n        with:\n          report_paths: \"**/build/test-results/test/*.xml\"\n          check_name: \"Test Results\"\n          detailed_summary: true\n          include_passed: false\n"
  },
  {
    "path": ".github/workflows/gradle-publish-release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n    if: github.repository == 'Trendyol/stove'\n    permissions:\n      contents: write\n      packages: write\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Set up JDK\n        uses: actions/setup-java@v5\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n          server-id: github\n          settings-path: ${{ github.workspace }}\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          gradle-version: current\n\n      - name: Extract version from gradle.properties\n        run: |\n          VERSION=$(grep '^version=' gradle.properties | cut -d'=' -f2)\n          echo \"JRELEASER_PROJECT_VERSION=$VERSION\" >> $GITHUB_ENV\n          echo \"Releasing version: $VERSION\"\n\n      - name: Publish to Maven Central\n        run: gradle --no-configuration-cache publish\n        env:\n          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.gpg_private_key }}\n          ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.gpg_passphrase }}\n          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ossrh_username }}\n          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ossrh_pass }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Create GitHub Release\n        uses: jreleaser/release-action@v2\n        with:\n          arguments: full-release\n        env:\n          JRELEASER_GITHUB_TOKEN: ${{ secrets.BOT_REPO_TOKEN }}\n          JRELEASER_PROJECT_VERSION: ${{ env.JRELEASER_PROJECT_VERSION }}\n\n      - name: Tag Go stove-kafka module\n        run: |\n          git tag \"go/stove-kafka/v${{ env.JRELEASER_PROJECT_VERSION }}\"\n          git push origin \"go/stove-kafka/v${{ env.JRELEASER_PROJECT_VERSION }}\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.BOT_REPO_TOKEN }}\n\n      - name: Upload JReleaser output\n        if: always()\n        uses: actions/upload-artifact@v7\n        with:\n          name: jreleaser-release\n          path: |\n            out/jreleaser/trace.log\n            out/jreleaser/output.properties\n"
  },
  {
    "path": ".github/workflows/gradle-publish-snapshot.yml",
    "content": "name: Publish to Snapshot Maven\non:\n  workflow_dispatch:\njobs:\n  publish:\n    runs-on: ubuntu-latest\n    if: github.repository == 'Trendyol/stove'\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - uses: actions/checkout@v6\n      - name: Set up JDK\n        uses: actions/setup-java@v5\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n          server-id: github\n          settings-path: ${{ github.workspace }}\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v6\n        with:\n          gradle-version: current\n\n      - name: Publish to Maven Repository\n        run: gradle --no-configuration-cache publish --parallel\n        env:\n          SNAPSHOT: true\n          BUILD_NUMBER: ${{ github.run_number }}\n          ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.gpg_private_key }}\n          ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.gpg_passphrase }}\n          ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ossrh_username }}\n          ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ossrh_pass }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/publish-to-ghpages.yml",
    "content": "name: Publish MkDocs to GitHub Pages\n\non:\n  push:\n    branches: [ main ]\n    paths: [ 'docs/**' ]\n\njobs:\n  deploy:\n    runs-on: ubuntu-latest\n    if: github.repository == 'Trendyol/stove'\n\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v6\n\n      - name: Install Dependencies\n        run: |\n          pip install mkdocs mkdocs-material mkdocs-awesome-pages-plugin --use-deprecated=legacy-resolver\n\n      - name: Build Site\n        run: |\n          mkdocs build\n\n      - name: Deploy to GitHub Pages\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          github_token: ${{ secrets.BOT_REPO_TOKEN }}\n          publish_dir: ./site\n"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "content": "\nname: Scorecard supply-chain security\n\non:\n  branch_protection_rule:\n  schedule:\n    - cron: '29 23 * * 3'\n  push:\n    branches: [ \"main\", \"master\"]\n  pull_request:\n    branches: [\"main\", \"master\"]\n\npermissions: read-all\n\njobs:\n  visibility-check:\n    outputs:\n      visibility: ${{ steps.drv.outputs.visibility }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Determine repository visibility\n        id: drv\n        run: |\n          visibility=$(gh api /repos/$GITHUB_REPOSITORY --jq '.visibility')\n          echo \"visibility=$visibility\" >> $GITHUB_OUTPUT\n        env:\n          GH_TOKEN: ${{ github.token }}\n\n  analysis:\n    if: ${{ needs.visibility-check.outputs.visibility == 'public' }}\n    needs: visibility-check\n    runs-on: ubuntu-latest\n    permissions:\n      security-events: write\n      id-token: write\n    steps:\n      - name: \"Checkout code\"\n        uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98\n        with:\n          persist-credentials: false\n\n      - name: \"Run analysis\"\n        uses: ossf/scorecard-action@05bb7c663f6ec9bd8484da0a5b5a77d423e3f88c\n        with:\n          results_file: results.sarif\n          results_format: sarif\n          publish_results: true\n\n      - name: \"Upload artifact\"\n        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n        with:\n          name: SARIF file\n          path: results.sarif\n          retention-days: 5\n\n      # Upload the results to GitHub's code scanning dashboard (optional).\n      # Commenting out will disable upload of results to your repo's Code Scanning dashboard\n      - name: \"Upload to code-scanning\"\n        uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4\n        with:\n          sarif_file: results.sarif\n\n\n"
  },
  {
    "path": ".github/workflows/stove-cli-ci.yml",
    "content": "name: Stove CLI CI\n\non:\n  push:\n    branches: [main]\n    paths:\n      - 'tools/stove-cli/**'\n      - 'lib/stove-dashboard-api/src/main/proto/**'\n  pull_request:\n    branches: [main]\n    paths:\n      - 'tools/stove-cli/**'\n      - 'lib/stove-dashboard-api/src/main/proto/**'\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build-and-test:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: tools/stove-cli\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          cache: 'npm'\n          cache-dependency-path: tools/stove-cli/spa/package-lock.json\n\n      - name: Install SPA dependencies\n        run: cd spa && npm ci\n\n      - name: SPA lint and format check\n        run: cd spa && npx biome check src\n\n      - name: Build SPA\n        run: cd spa && npm run build\n\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          components: clippy, rustfmt\n\n      - name: Install protoc\n        run: sudo apt-get update && sudo apt-get install -y protobuf-compiler\n\n      - uses: actions/cache@v5\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n            tools/stove-cli/target\n          key: cargo-${{ runner.os }}-${{ hashFiles('tools/stove-cli/Cargo.lock') }}\n          restore-keys: cargo-${{ runner.os }}-\n\n      - name: Check formatting\n        run: cargo fmt -- --check\n\n      - name: Clippy\n        run: cargo clippy -- -D warnings\n        env:\n          SKIP_SPA_BUILD: \"1\"\n\n      - name: Run tests\n        run: cargo test\n        env:\n          SKIP_SPA_BUILD: \"1\"\n"
  },
  {
    "path": ".github/workflows/stove-cli-release.yml",
    "content": "name: Stove CLI Build\n\non:\n  push:\n    tags:\n      - 'v*'\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version to release (leave empty to read from gradle.properties)'\n        required: false\n\npermissions:\n  contents: write\n\njobs:\n  resolve-version:\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{ steps.resolve.outputs.version }}\n      tag: ${{ steps.resolve.outputs.tag }}\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Resolve version\n        id: resolve\n        run: |\n          if [ \"${{ github.event_name }}\" = \"workflow_dispatch\" ]; then\n            INPUT_VERSION=\"${{ github.event.inputs.version }}\"\n            if [ -n \"$INPUT_VERSION\" ]; then\n              VERSION=\"$INPUT_VERSION\"\n            else\n              VERSION=$(grep '^version=' gradle.properties | cut -d'=' -f2)\n            fi\n            TAG=\"v${VERSION}\"\n          else\n            TAG=\"${GITHUB_REF_NAME}\"\n            VERSION=\"${TAG#v}\"\n          fi\n          echo \"version=${VERSION}\" >> \"$GITHUB_OUTPUT\"\n          echo \"tag=${TAG}\" >> \"$GITHUB_OUTPUT\"\n          echo \"Resolved version: ${VERSION} (tag: ${TAG})\"\n\n  build:\n    needs: resolve-version\n    strategy:\n      matrix:\n        include:\n          - target: aarch64-apple-darwin\n            runner: macos-14\n            label: darwin-arm64\n          - target: x86_64-apple-darwin\n            runner: macos-14\n            label: darwin-amd64\n          - target: x86_64-unknown-linux-gnu\n            runner: ubuntu-latest\n            label: linux-amd64\n    runs-on: ${{ matrix.runner }}\n    defaults:\n      run:\n        working-directory: tools/stove-cli\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          cache: 'npm'\n          cache-dependency-path: tools/stove-cli/spa/package-lock.json\n\n      - name: Build SPA\n        run: cd spa && npm ci && npm run build\n\n      - uses: dtolnay/rust-toolchain@stable\n        with:\n          targets: ${{ matrix.target }}\n\n      - name: Install protoc (Linux)\n        if: runner.os == 'Linux'\n        run: sudo apt-get update && sudo apt-get install -y protobuf-compiler\n\n      - name: Install protoc (macOS)\n        if: runner.os == 'macOS'\n        run: brew install protobuf\n\n      - uses: actions/cache@v5\n        with:\n          path: |\n            ~/.cargo/registry\n            ~/.cargo/git\n            tools/stove-cli/target\n          key: cargo-release-${{ matrix.target }}-${{ hashFiles('tools/stove-cli/Cargo.lock') }}\n          restore-keys: cargo-release-${{ matrix.target }}-\n\n      - name: Build release binary\n        run: cargo build --release --target ${{ matrix.target }}\n        env:\n          SKIP_SPA_BUILD: \"1\"\n\n      - name: Package tarball\n        run: |\n          VERSION=\"${{ needs.resolve-version.outputs.version }}\"\n          ARCHIVE=\"stove-${VERSION}-${{ matrix.label }}.tar.gz\"\n          mkdir -p staging\n          cp \"target/${{ matrix.target }}/release/stove\" staging/\n          cp \"${GITHUB_WORKSPACE}/LICENSE\" staging/\n          tar czf \"${ARCHIVE}\" -C staging .\n          shasum -a 256 \"${ARCHIVE}\" > \"${ARCHIVE}.sha256\"\n\n      - uses: actions/upload-artifact@v7\n        with:\n          name: binaries-${{ matrix.label }}\n          path: |\n            tools/stove-cli/stove-*.tar.gz\n            tools/stove-cli/stove-*.sha256\n\n  release:\n    needs: [resolve-version, build]\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{ needs.resolve-version.outputs.version }}\n\n    steps:\n      - uses: actions/download-artifact@v8\n        with:\n          pattern: binaries-*\n          merge-multiple: true\n\n      - name: Upload CLI binaries to release\n        uses: softprops/action-gh-release@v3\n        with:\n          tag_name: ${{ needs.resolve-version.outputs.tag }}\n          name: \"Stove v${{ needs.resolve-version.outputs.version }}\"\n          files: |\n            stove-*.tar.gz\n            stove-*.sha256\n          fail_on_unmatched_files: false\n          append_body: true\n          body: |\n\n            ---\n\n            ### Stove CLI Install\n\n            **Homebrew (macOS):**\n            ```\n            brew install Trendyol/trendyol-tap/stove\n            ```\n\n            **Shell script (macOS & Linux):**\n            ```\n            curl -fsSL https://raw.githubusercontent.com/Trendyol/stove/main/tools/stove-cli/install.sh | sh\n            ```\n\n            **Manual:** Download the archive for your platform below, extract, and place `stove` in your PATH.\n\n  update-homebrew:\n    needs: release\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Generate and push Homebrew formula\n        env:\n          VERSION: ${{ needs.release.outputs.version }}\n          TAP_TOKEN: ${{ secrets.BOT_REPO_TOKEN }}\n        run: |\n          BASE_URL=\"https://github.com/Trendyol/stove/releases/download/v${VERSION}\"\n\n          SHA_DARWIN_ARM64=$(curl -fsSL \"${BASE_URL}/stove-${VERSION}-darwin-arm64.tar.gz.sha256\" | awk '{print $1}')\n          SHA_DARWIN_AMD64=$(curl -fsSL \"${BASE_URL}/stove-${VERSION}-darwin-amd64.tar.gz.sha256\" | awk '{print $1}')\n          SHA_LINUX_AMD64=$(curl -fsSL \"${BASE_URL}/stove-${VERSION}-linux-amd64.tar.gz.sha256\" | awk '{print $1}')\n\n          cp tools/stove-cli/Formula/stove.rb stove.rb\n          sed -i \"s/__VERSION__/${VERSION}/\" stove.rb\n          sed -i \"s/__SHA256_DARWIN_ARM64__/${SHA_DARWIN_ARM64}/\" stove.rb\n          sed -i \"s/__SHA256_DARWIN_AMD64__/${SHA_DARWIN_AMD64}/\" stove.rb\n          sed -i \"s/__SHA256_LINUX_AMD64__/${SHA_LINUX_AMD64}/\" stove.rb\n\n          WORKDIR=\"$(mktemp -d)\"\n          git clone \"https://x-access-token:${TAP_TOKEN}@github.com/Trendyol/homebrew-trendyol-tap.git\" \"${WORKDIR}\"\n          cp stove.rb \"${WORKDIR}/stove.rb\"\n          cd \"${WORKDIR}\"\n          git config user.name \"github-actions[bot]\"\n          git config user.email \"github-actions[bot]@users.noreply.github.com\"\n          git add stove.rb\n          git commit -m \"stove ${VERSION}\"\n          git push\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n/site\n/.idea\n.idea/shelf\n/confluence/target\n/dependencies/repo\n/android.tests.dependencies\n/dependencies/android.tests.dependencies\n/dist\n/local\n/gh-pages\n/ideaSDK\n/clionSDK\n/android-studio/sdk\nout/\n/tmp\n/intellij\nworkspace.xml\n*.versionsBackup\n/idea/testData/debugger/tinyApp/classes*\n/jps-plugin/testData/kannotator\n/js/js.translator/testData/out/\n/js/js.translator/testData/out-min/\n/js/js.translator/testData/out-pir/\n.gradle/\nbuild/\n!**/src/**/build\n!**/test/**/build\n*.iml\n!**/testData/**/*.iml\n.idea/remote-targets.xml\n.idea/libraries/Gradle*.xml\n.idea/libraries/Maven*.xml\n.idea/artifacts/PILL_*.xml\n.idea/artifacts/KotlinPlugin.xml\n.idea/modules\n.idea/runConfigurations/JPS_*.xml\n.idea/runConfigurations/PILL_*.xml\n.idea/runConfigurations/_FP_*.xml\n.idea/runConfigurations/_MT_*.xml\n.idea/libraries\n.idea/modules.xml\n.idea/gradle.xml\n.idea/compiler.xml\n.idea/inspectionProfiles/profiles_settings.xml\n.idea/.name\n.idea/artifacts/dist_auto_*\n.idea/artifacts/dist.xml\n.idea/artifacts/ideaPlugin.xml\n.idea/artifacts/kotlinc.xml\n.idea/artifacts/kotlin_compiler_jar.xml\n.idea/artifacts/kotlin_plugin_jar.xml\n.idea/artifacts/kotlin_jps_plugin_jar.xml\n.idea/artifacts/kotlin_daemon_client_jar.xml\n.idea/artifacts/kotlin_imports_dumper_compiler_plugin_jar.xml\n.idea/artifacts/kotlin_main_kts_jar.xml\n.idea/artifacts/kotlin_compiler_client_embeddable_jar.xml\n.idea/artifacts/kotlin_reflect_jar.xml\n.idea/artifacts/kotlin_stdlib_js_ir_*\n.idea/artifacts/kotlin_test_js_ir_*\n.idea/artifacts/kotlin_stdlib_wasm_*\n.idea/artifacts/kotlinx_atomicfu_runtime_*\n.idea/artifacts/kotlinx_cli_jvm_*\n.idea/jarRepositories.xml\n.idea/csv-plugin.xml\n.idea/libraries-with-intellij-classes.xml\n.idea/misc.xml\n.idea/**\nnode_modules/\n.rpt2_cache/\nlibraries/tools/kotlin-test-js-runner/lib/\nlocal.properties\nbuildSrcTmp/\ndistTmp/\noutTmp/\n/test.output\n/kotlin-native/dist\nkotlin-ide/\n**/bin/**/*\n\n# Ignore Gradle project-specific cache directory\n.gradle\n\n# Ignore Gradle build output directory\nbuild\n.kotlin\n*.tsbuildinfo\n.junie\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">Stove</h1>\n\n<p align=\"center\">\n  End-to-end testing framework for the JVM.<br/>\n  Test your application against real infrastructure with a unified Kotlin DSL.\n</p>\n\n<p align=\"center\">\n  <img src=\"https://img.shields.io/maven-central/v/com.trendyol/stove?versionPrefix=0&label=release&color=blue\" alt=\"Release\"/>\n  <a href=\"https://github.com/Trendyol/homebrew-trendyol-tap\"><img src=\"https://img.shields.io/github/v/release/Trendyol/stove?label=StoveCLI(homebrew)&logo=homebrew&color=FBB040\" alt=\"Homebrew\"/></a>\n  <a href=\"https://central.sonatype.com/service/rest/repository/browse/maven-snapshots/com/trendyol/\"><img src=\"https://img.shields.io/badge/dynamic/xml?url=https%3A%2F%2Fcentral.sonatype.com%2Frepository%2Fmaven-snapshots%2Fcom%2Ftrendyol%2Fstove%2Fmaven-metadata.xml&query=%2F%2Fmetadata%2Fversioning%2Flatest&label=snapshot&color=orange\" alt=\"Snapshot\"/></a>\n  <a href=\"https://codecov.io/gh/Trendyol/stove\"><img src=\"https://codecov.io/gh/Trendyol/stove/graph/badge.svg?token=HcKBT3chO7\" alt=\"codecov\"/></a>\n  <a href=\"https://scorecard.dev/viewer/?uri=github.com/Trendyol/stove\"><img src=\"https://img.shields.io/ossf-scorecard/github.com/Trendyol/stove?label=openssf%20scorecard&style=flat\" alt=\"OpenSSF Scorecard\"/></a>\n</p>\n\n```kotlin\nstove {\n  // Call API and verify response\n  http {\n    postAndExpectBodilessResponse(\"/orders\", body = CreateOrderRequest(userId, productId).some()) {\n      it.status shouldBe 201\n    }\n  }\n\n  // Verify database state\n  postgresql {\n    shouldQuery<Order>(\"SELECT * FROM orders WHERE user_id = '$userId'\", mapper = { row ->\n      Order(row.string(\"status\"))\n    }) {\n      it.first().status shouldBe \"CONFIRMED\"\n    }\n  }\n\n  // Verify event was published\n  kafka {\n    shouldBePublished<OrderCreatedEvent> {\n      actual.userId == userId\n    }\n  }\n\n  // Access application beans directly\n  using<InventoryService> {\n    getStock(productId) shouldBe 9\n  }\n}\n```\n\n## Why Stove?\n\nThe JVM ecosystem has excellent frameworks for building applications, but e2e testing remains fragmented. Testcontainers\nhandles infrastructure, but you still write boilerplate for configuration, app startup, and assertions. Differently for\neach framework.\n\nStove explores how the testing experience on the JVM can be improved by unifying assertions and the supporting\ninfrastructure. It creates a concise and expressive testing DSL by leveraging Kotlin's unique language features.\n\nStove works with Java, Kotlin, and Scala applications across Spring Boot, Ktor, Micronaut, and Quarkus. Because tests are\nframework-agnostic, teams can migrate between stacks without rewriting test code. It empowers developers to write clear\nassertions even for code that is traditionally hard to test (async flows, message consumers, database side effects).\n\n**What Stove does:**\n\n- Starts containers via Testcontainers or connect **provided** infra (PostgreSQL, MySQL, Kafka, etc.)\n- Launches your **actual** application with test configuration\n- Exposes a unified DSL for assertions across all components\n- Provides access to your DI container from tests\n- Debug your entire use case with one click (breakpoints work everywhere)\n- Get code coverage from e2e test execution\n- Supports Spring Boot, Ktor, Micronaut, Quarkus\n- Extensible architecture for adding new components and\n  frameworks ([Writing Custom Systems](https://trendyol.github.io/stove/writing-custom-systems/))\n\n## Dashboard (New in 0.23.0)\n\nStove Dashboard introduces a local real-time dashboard for end-to-end test runs. It captures HTTP calls, Kafka activity,\ndatabase assertions, and traces in one place so you can inspect successful and failed runs with full context.\n\nhttps://github.com/user-attachments/assets/14597dc6-e9d4-43ab-8cfa-578ab3c3e6df\n\n**Quick start**\n\n```bash\n# 1) Install and start the Dashboard CLI\nbrew install Trendyol/trendyol-tap/stove\nstove\n\n# 2) Run your tests and open the dashboard\n./gradlew test\n# http://localhost:4040\n```\n\n```kotlin\n// build.gradle.kts\nplugins {\n  id(\"com.trendyol.stove.tracing\") version \"$stoveVersion\"\n}\n\ndependencies {\n  testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n  testImplementation(\"com.trendyol:stove-extensions-kotest\")  // or stove-extensions-junit\n  testImplementation(\"com.trendyol:stove-dashboard\")\n  testImplementation(\"com.trendyol:stove-tracing\")\n}\n\nstoveTracing {\n  serviceName.set(\"product-api\")\n}\n```\n\n```kotlin\n// Kotest\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions = listOf(StoveKotestExtension())\n  override suspend fun beforeProject() {\n    Stove().with {\n      dashboard { DashboardSystemOptions(appName = \"product-api\") }\n      tracing { enableSpanReceiver() } // recommended\n    }.run()\n  }\n  override suspend fun afterProject() = Stove.stop()\n}\n\n// JUnit\n@ExtendWith(StoveJUnitExtension::class)\nabstract class BaseE2ETest { /* Stove().with { ... }.run() in @BeforeAll */ }\n```\n\nKeep `stove-cli`, the Stove BOM, the tracing Gradle plugin, and your Stove test dependencies on the same Stove version. The dashboard warns on version mismatches, but aligning versions avoids missing or inconsistent dashboard data.\n\nSee [Dashboard docs](https://trendyol.github.io/stove/Components/18-dashboard/) and\n[0.23.0 release notes](https://trendyol.github.io/stove/release-notes/0.23.0/) for full details.\n\n## Getting Started\n\n**1. Add dependencies**\n\n```kotlin\ndependencies {\n  // Import BOM for version management\n  testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n  \n  // Core and framework starter\n  testImplementation(\"com.trendyol:stove\")\n  testImplementation(\"com.trendyol:stove-spring\")  // or stove-ktor, stove-micronaut, stove-quarkus\n  \n  // Component modules\n  testImplementation(\"com.trendyol:stove-postgres\")\n  testImplementation(\"com.trendyol:stove-mysql\")\n  testImplementation(\"com.trendyol:stove-kafka\")\n}\n```\n\n> **Snapshots:** As of 5th June 2025, Stove's snapshot packages are hosted on [Central Sonatype](https://central.sonatype.com/service/rest/repository/browse/maven-snapshots/com/trendyol/).\n> ```kotlin\n> repositories {\n>   maven(\"https://central.sonatype.com/repository/maven-snapshots\")\n> }\n> ```\n\n**2. Configure Stove** (runs once before all tests)\n\n```kotlin\nclass StoveConfig : AbstractProjectConfig() {\n  override suspend fun beforeProject() = Stove()\n    .with {\n      httpClient {\n        HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n      }\n      postgresql {\n        PostgresqlOptions(\n          cleanup = { it.execute(\"TRUNCATE orders, users\") },\n          configureExposedConfiguration = { listOf(\"spring.datasource.url=${it.jdbcUrl}\") }\n        ).migrations {\n          register<CreateUsersTable>()\n        }\n      }\n      kafka {\n        KafkaSystemOptions(\n          cleanup = { it.deleteTopics(listOf(\"orders\")) },\n          configureExposedConfiguration = { listOf(\"kafka.bootstrapServers=${it.bootstrapServers}\") }\n        ).migrations {\n          register<CreateOrdersTopic>()\n        }\n      }\n      bridge()\n      springBoot(runner = { params ->\n        myApp.run(params) { addTestDependencies() }\n      })\n    }.run()\n\n  override suspend fun afterProject() = Stove.stop()\n}\n```\n\n**3. Write tests**\n\n```kotlin\ntest(\"should process order\") {\n  stove {\n    http {\n      get<Order>(\"/orders/123\") {\n        it.status shouldBe \"CONFIRMED\"\n      }\n    }\n    postgresql {\n      shouldQuery<Order>(\"SELECT * FROM orders\", mapper = { row ->\n        Order(row.string(\"status\"))\n      }) {\n        it.size shouldBe 1\n      }\n    }\n    kafka {\n      shouldBePublished<OrderCreatedEvent> {\n        actual.orderId == \"123\"\n      }\n    }\n  }\n}\n```\n\n## Writing Tests\n\nAll assertions happen inside `stove { }`. Each component has its own DSL block.\n\n### HTTP\n\n```kotlin\nhttp {\n  get<User>(\"/users/$id\") {\n    it.name shouldBe \"John\"\n  }\n  postAndExpectBodilessResponse(\"/users\", body = request.some()) {\n    it.status shouldBe 201\n  }\n  postAndExpectBody<User>(\"/users\", body = request.some()) {\n    it.id shouldNotBe null\n  }\n}\n```\n\n### Database\n\n```kotlin\npostgresql {  // also: mysql, mongodb, couchbase, mssql, elasticsearch, redis\n  shouldExecute(\"INSERT INTO users (name) VALUES ('Jane')\")\n  shouldQuery<User>(\"SELECT * FROM users\", mapper = { row ->\n    User(row.string(\"name\"))\n  }) {\n    it.size shouldBe 1\n  }\n}\n```\n\n### Kafka\n\n```kotlin\nkafka {\n  publish(\"orders.created\", OrderCreatedEvent(orderId = \"123\"))\n  shouldBeConsumed<OrderCreatedEvent> {\n    actual.orderId == \"123\"\n  }\n  shouldBePublished<OrderConfirmedEvent> {\n    actual.orderId == \"123\"\n  }\n}\n```\n\n### External API Mocking\n\n```kotlin\nwiremock {\n  mockGet(\"/external-api/users/1\", responseBody = User(id = 1, name = \"John\").some())\n  mockPost(\"/external-api/notify\", statusCode = 202)\n}\n```\n\n### Application Beans\n\nAccess your DI container directly via `bridge()`:\n\n```kotlin\nusing<OrderService> { processOrder(orderId) }\nusing<UserRepo, EmailService> { userRepo, emailService ->\n  userRepo.findById(id) shouldNotBe null\n}\n```\n\n### Reporting\n\nWhen tests fail, Stove automatically enriches exceptions with a detailed execution report showing exactly what happened:\n\n<details>\n<summary><strong>Example Report</strong></summary>\n\n```\n╔══════════════════════════════════════════════════════════════════════════════════════════════════╗\n║                                   STOVE TEST EXECUTION REPORT                                    ║\n║                                                                                                  ║\n║ Test: should create new product when send product create request from api for the allowed        ║\n║ supplier                                                                                         ║\n║ ID: ExampleTest::should create new product when send product create request from api for the     ║\n║ allowed supplier                                                                                 ║\n║ Status: FAILED                                                                                   ║\n╠══════════════════════════════════════════════════════════════════════════════════════════════════╣\n║                                                                                                  ║\n║ TIMELINE                                                                                         ║\n║ ────────                                                                                         ║\n║                                                                                                  ║\n║ 12:41:12.371 ✓ PASSED [WireMock] Register stub: GET /suppliers/99/allowed                        ║\n║     Output: kotlin.Unit                                                                          ║\n║     Metadata: {statusCode=200, responseHeaders={}}                                               ║\n║                                                                                                  ║\n║ 12:41:13.405 ✓ PASSED [HTTP] POST /api/product/create                                            ║\n║     Input: ProductCreateRequest(id=1, name=product name, supplierId=99)                          ║\n║     Output: kotlin.Unit                                                                          ║\n║     Metadata: {status=200, headers={}}                                                           ║\n║                                                                                                  ║\n║ 12:41:13.424 ✓ PASSED [Kafka] shouldBePublished<ProductCreatedEvent>                             ║\n║     Output: ProductCreatedEvent(id=1, name=product name, supplierId=99, createdDate=Thu Jan 08   ║\n║     12:41:12 CET 2026, type=ProductCreatedEvent)                                                 ║\n║     Metadata: {timeout=5s}                                                                       ║\n║                                                                                                  ║\n║ 12:41:13.455 ✗ FAILED [Couchbase] Get document                                                   ║\n║     Input: {id=product:1}                                                                        ║\n║     Error: expected:<100L> but was:<99L>                                                         ║\n║                                                                                                  ║\n╠══════════════════════════════════════════════════════════════════════════════════════════════════╣\n║                                                                                                  ║\n║ SYSTEM SNAPSHOTS                                                                                 ║\n║ ────────────────                                                                                 ║\n║                                                                                                  ║\n║ ┌─ HTTP ──────────────────────────────────────────────────────────────────────────────────────── ║\n║                                                                                                  ║\n║   No detailed state available                                                                    ║\n║                                                                                                  ║\n║ ┌─ COUCHBASE ─────────────────────────────────────────────────────────────────────────────────── ║\n║                                                                                                  ║\n║   No detailed state available                                                                    ║\n║                                                                                                  ║\n║ ┌─ KAFKA ─────────────────────────────────────────────────────────────────────────────────────── ║\n║                                                                                                  ║\n║   Consumed: 0                                                                                    ║\n║   Published: 1                                                                                   ║\n║   Committed: 0                                                                                   ║\n║                                                                                                  ║\n║   State Details:                                                                                 ║\n║     consumed: 0 item(s)                                                                          ║\n║     published: 1 item(s)                                                                         ║\n║       [0]                                                                                        ║\n║         id: 376db940-a367-4419-a628-4754c9466421                                                 ║\n║         topic: stove-standalone-example.productCreated.1                                         ║\n║         key: 1                                                                                   ║\n║         headers: {X-EventType=ProductCreatedEvent, X-MessageId=29902970-056d-4ae9-9a84-...}      ║\n║         message: {\"id\":1,\"name\":\"product name\",\"supplierId\":99,...}                              ║\n║     committed: 0 item(s)                                                                         ║\n║                                                                                                  ║\n║ ┌─ WIREMOCK ──────────────────────────────────────────────────────────────────────────────────── ║\n║                                                                                                  ║\n║   Registered stubs: 0                                                                            ║\n║   Served requests: 0 (matched: 0)                                                                ║\n║   Unmatched requests: 0                                                                          ║\n║                                                                                                  ║\n╚══════════════════════════════════════════════════════════════════════════════════════════════════╝\n```\n\n</details>\n\n**Features:**\n- Timeline of all operations with timestamps and results\n- Input/output for each action\n- Expected vs actual values on failures\n- System snapshots (Kafka messages, WireMock stubs, etc.)\n\n**Test Framework Extensions:**\n\nUse the provided extensions to automatically enrich failures:\n\n```kotlin\n// Kotest - register in project config\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions = listOf(StoveKotestExtension())\n}\n\n// JUnit 5 - annotate test class\n@ExtendWith(StoveJUnitExtension::class)\nclass MyTest { ... }\n```\n\n**Configuration:**\n\n```kotlin\nStove(\n  StoveOptions(\n    reportingEnabled = true,           // Enable/disable reporting (default: true)\n    dumpReportOnTestFailure = true,    // Enrich failures with report (default: true)\n    failureRenderer = PrettyConsoleRenderer  // Custom renderer (default: PrettyConsoleRenderer)\n  )\n).with { ... }\n```\n\n### Tracing\n\nWhen a test fails, see the **entire execution call chain** inside your application — every controller, service, database query, and Kafka message — powered by OpenTelemetry:\n\n```\nEXECUTION TRACE (Call Chain)\n═══════════════════════════════════════════════════════════════════\n✓ POST (377ms)\n  ✓ POST /api/product/create (361ms)\n    ✓ ProductController.create (141ms)\n      ✓ ProductCreator.create (0ms)\n      ✓ KafkaProducer.send (137ms)\n        ✓ orders.created publish (81ms)\n          ✗ orders.created process (82ms)  ← FAILURE POINT\n```\n\n**Setup** (two steps):\n\n```kotlin\n// 1. In your Stove config\ntracing { enableSpanReceiver() }\n\n// 2. In build.gradle.kts\nplugins { id(\"com.trendyol.stove.tracing\") version \"$stoveVersion\" }\nstoveTracing { serviceName.set(\"my-service\") }\n```\n\n**Validate traces in tests:**\n\n```kotlin\ntracing {\n    shouldContainSpan(\"OrderService.processOrder\")\n    shouldNotHaveFailedSpans()\n    executionTimeShouldBeLessThan(500.milliseconds)\n}\n```\n\nNo code changes to your application required. The OpenTelemetry agent instruments 100+ libraries automatically.\n\n### AI Agent Integration\n\nStove's execution reports and tracing data are structured and deterministic, making them ideal for **AI agent workflows**. When an AI agent runs e2e tests during implementation, it can parse the failure reports — including the full execution trace, system snapshots, and timeline — to understand exactly what went wrong inside the application. This enables agents to iterate on fixes with precise feedback rather than guessing from opaque test failures.\n\nWhen `stove` is running, it also exposes a local read-only MCP endpoint at `http://localhost:4040/mcp`. Agents can call `stove_failures` first, then drill into a specific `run_id + test_id` for timeline, trace, and snapshot evidence. MCP is optional: if it is unavailable or incomplete, agents should fall back to normal test output, Stove failure reports, and logs.\n\n**Agent Skills:** Stove ships with a ready-to-use [Claude Code skill](https://github.com/Trendyol/stove/tree/main/.claude/skills/stove) that teaches AI agents how to set up and write Stove e2e tests. Copy the `.claude/skills/stove/` directory into your project's `.claude/skills/` folder, and your AI coding agent will know how to configure systems, write tests, enable tracing, and build custom systems — following all Stove conventions automatically.\n\n## Configuration\n\n### Framework Setup\n\n<table>\n<tr><th>Spring Boot</th><th>Ktor</th></tr>\n<tr>\n<td>\n\n```kotlin\nspringBoot(\n  runner = { params ->\n    myApp.run(params) {\n      addTestDependencies()\n    }\n  }\n)\n```\n\n</td>\n<td>\n\n```kotlin\nktor(\n  runner = { params ->\n    run(params, shouldWait = false)\n  }\n)\n```\n\n</td>\n</tr>\n<tr><th>Micronaut</th><th>Quarkus</th></tr>\n<tr>\n<td>\n\n```kotlin\nmicronaut(\n  runner = { params ->\n    myApp.run(params)\n  }\n)\n```\n\n</td>\n<td>\n\n```kotlin\nquarkus(\n  runner = { params ->\n    MyApp.main(params)\n  }\n)\n```\n\n</td>\n</tr>\n</table>\n\n### Container Reuse\n\nSpeed up local development by keeping containers running between test runs:\n\n```kotlin\nStove { keepDependenciesRunning() }.with { ... }\n```\n\n### Cleanup\n\nRun cleanup logic after tests complete:\n\n```kotlin\npostgresql {\n  PostgresqlOptions(cleanup = { it.execute(\"TRUNCATE users\") }, ...)\n}\n\nkafka {\n  KafkaSystemOptions(cleanup = { it.deleteTopics(listOf(\"test-topic\")) }, ...)\n}\n```\n\nAvailable for Kafka, PostgreSQL, MySQL, MongoDB, Couchbase, Cassandra, MSSQL, Elasticsearch, Redis.\n\n### Migrations\n\nRun database migrations before tests start:\n\n```kotlin\npostgresql {\n  PostgresqlOptions(...)\n   .migrations {\n      register<CreateUsersTable>()\n      register<CreateOrdersTable>()\n  }\n}\n```\n\nAvailable for Kafka, PostgreSQL, MySQL, MongoDB, Couchbase, Cassandra, MSSQL, Elasticsearch, Redis.\n\n### Provided Instances\n\nConnect to existing infrastructure instead of starting containers (useful for CI/CD):\n\n```kotlin\npostgresql { PostgresqlOptions.provided(jdbcUrl = \"jdbc:postgresql://ci-db:5432/test\", ...) }\nkafka { KafkaSystemOptions.provided(bootstrapServers = \"ci-kafka:9092\", ...) }\n```\n\n> **Tip:** When using provided instances, use migrations to create isolated test schemas and cleanups to remove test\n> data afterwards. This ensures test isolation on shared infrastructure.\n\n<strong>Complete Example</strong>\n\n```kotlin\ntest(\"should create order with payment processing\") {\n  stove {\n    val userId = UUID.randomUUID().toString()\n    val productId = UUID.randomUUID().toString()\n\n    // 1. Seed database\n    postgresql {\n      shouldExecute(\"INSERT INTO users (id, name) VALUES ('$userId', 'John')\")\n      shouldExecute(\"INSERT INTO products (id, price, stock) VALUES ('$productId', 99.99, 10)\")\n    }\n\n    // 2. Mock external payment API\n    wiremock {\n      mockPost(\n        \"/payments/charge\", statusCode = 200,\n        responseBody = PaymentResult(success = true).some()\n      )\n    }\n\n    // 3. Call API\n    http {\n      postAndExpectBody<OrderResponse>(\n        \"/orders\",\n        body = CreateOrderRequest(userId, productId).some()\n      ) {\n        it.status shouldBe 201\n      }\n    }\n\n    // 4. Verify database\n    postgresql {\n      shouldQuery<Order>(\"SELECT * FROM orders WHERE user_id = '$userId'\", mapper = { row ->\n        Order(row.string(\"status\"))\n      }) {\n        it.first().status shouldBe \"CONFIRMED\"\n      }\n    }\n\n    // 5. Verify event published\n    kafka {\n      shouldBePublished<OrderCreatedEvent> {\n        actual.userId == userId\n      }\n    }\n\n    // 6. Verify via application service\n    using<InventoryService> { getStock(productId) shouldBe 9 }\n  }\n}\n```\n\n## Reference\n\n### Supported Components\n\n| Category   | Components                                                  |\n|------------|-------------------------------------------------------------|\n| Databases  | PostgreSQL, MySQL, MongoDB, Couchbase, Cassandra, MSSQL, Elasticsearch, Redis |\n| Messaging  | Kafka                                                       |\n| HTTP       | Built-in client, WebSockets, WireMock                       |\n| gRPC       | Client (grpc-kotlin), Mock Server (native)                  |\n| Frameworks | Spring Boot, Ktor, Micronaut, Quarkus                       |\n\n### Feature Matrix\n\n| Component     | Migrations | Cleanup | Provided Instance | Pause/Unpause |\n|---------------|:----------:|:-------:|:-----------------:|:-------------:|\n| PostgreSQL    |     ✅      |    ✅    |         ✅         |       ✅       |\n| MySQL         |     ✅      |    ✅    |         ✅         |       ✅       |\n| MSSQL         |     ✅      |    ✅    |         ✅         |       ✅       |\n| MongoDB       |     ✅      |    ✅    |         ✅         |       ✅       |\n| Couchbase     |     ✅      |    ✅    |         ✅         |       ✅       |\n| Cassandra     |     ✅      |    ✅    |         ✅         |       ✅       |\n| Elasticsearch |     ✅      |    ✅    |         ✅         |       ✅       |\n| Redis         |     ✅      |    ✅    |         ✅         |       ✅       |\n| Kafka         |     ✅      |    ✅    |         ✅         |       ✅       |\n| WireMock      |    n/a     |   n/a   |        n/a        |      n/a      |\n| HTTP Client   |    n/a     |   n/a   |        n/a        |      n/a      |\n| gRPC Mock     |    n/a     |   n/a   |        n/a        |      n/a      |\n\n<details>\n<summary><strong>FAQ</strong></summary>\n\n**Can I use Stove with Java applications?**  \nYes. Your application can be Java, Scala, or any JVM language. Tests are written in Kotlin for the DSL.\n\n**Does Stove replace Testcontainers?**  \nNo. Stove uses Testcontainers underneath and adds the unified DSL on top.\n\n**How slow is the first run?**  \nFirst run pulls Docker images (~1-2 min). Use `keepDependenciesRunning()` for instant subsequent runs.\n\n**Can I run tests in parallel?**  \nYes, with unique test data per test.\nSee [provided instances docs](https://trendyol.github.io/stove/Components/11-provided-instances/).\n\n</details>\n\n## Resources\n\n- **[Documentation](https://trendyol.github.io/stove/)**: Full guides and API reference\n- **[Examples](https://github.com/Trendyol/stove/tree/main/examples)**: Working sample projects\n- **[AI Agent Skill](https://github.com/Trendyol/stove/tree/main/.claude/skills/stove)**: Drop into `.claude/skills/` to teach AI agents Stove conventions\n- **[Blog Post](https://medium.com/trendyol-tech/a-new-approach-to-the-api-end-to-end-testing-in-kotlin-f743fd1901f5)**:\n  Motivation and design decisions\n- **[Video Walkthrough](https://youtu.be/DJ0CI5cBanc?t=669)**: Live demo (Turkish)\n\n## Community\n\n**Used by:**\n\n1. [Trendyol](https://www.trendyol.com): Leading e-commerce platform, Turkey\n\n*Using Stove? Open a PR to add your company.*\n\n**Contributions:** [Issues](https://github.com/Trendyol/stove/issues) and PRs welcome  \n**License:** Apache 2.0\n\n> **Note:** Production-ready and used at scale. API still evolving; breaking changes possible in minor releases with\n> migration guides.\n"
  },
  {
    "path": "api/stove.api",
    "content": ""
  },
  {
    "path": "build.gradle.kts",
    "content": "import org.gradle.plugins.ide.idea.model.IdeaModel\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n  kotlin(\"jvm\").version(libs.versions.kotlin)\n  alias(libs.plugins.spotless)\n  alias(libs.plugins.testLogger)\n  alias(libs.plugins.kover)\n  alias(libs.plugins.detekt)\n  alias(libs.plugins.binaryCompatibilityValidator)\n  alias(libs.plugins.maven.publish)\n  idea\n  java\n}\n\ngroup = \"com.trendyol\"\nversion = CI.version(project)\n\napiValidation {\n  ignoredProjects += listOf(\n    \"ktor-example\",\n    \"micronaut-example\",\n    \"spring-example\",\n    \"spring-4x-example\",\n    \"spring-standalone-example\",\n    \"spring-streams-example\",\n    \"tests\",\n    \"spring-test-fixtures\",\n    \"spring-2x-kafka-tests\",\n    \"spring-3x-kafka-tests\",\n    \"spring-4x-kafka-tests\",\n    \"spring-4x-tests\",\n    \"spring-3x-tests\",\n    \"spring-2x-tests\",\n    \"quarkus-example\",\n    \"ktor-di-tests\",\n    \"ktor-koin-tests\",\n    \"ktor-test-fixtures\",\n    \"stove-tracing-gradle-plugin\",\n    \"stove-dashboard-api\",\n    \"stove-dashboard\",\n  )\n}\nkover {\n  reports {\n    filters {\n      excludes {\n        classes(\n          \"com.trendyol.stove.functional.*\",\n          \"com.trendyol.stove.system.abstractions.*\",\n          \"com.trendyol.stove.system.annotations.*\",\n          \"com.trendyol.stove.serialization.*\",\n          \"stove.spring.example.*\",\n          \"stove.spring.standalone.example.*\",\n          \"stove.spring.streams.example.*\",\n          \"stove.ktor.example.*\",\n          \"stove.quarkus.example.*\",\n          \"stove.micronaut.example.*\",\n        )\n      }\n    }\n  }\n}\nval related = subprojects.of(\"lib\", \"spring\", \"examples\", \"ktor\", \"quarkus\", \"micronaut\", \"container\", \"process\", \"tests\", \"test-extensions\", except = listOf(\"stove-bom\"))\ndependencies { related.forEach { kover(it) } }\n\nsubprojects.of(\"lib\", \"spring\", \"examples\", \"ktor\", \"quarkus\", \"micronaut\", \"container\", \"process\", \"tests\", \"test-extensions\", except = listOf(\"stove-bom\")) {\n  apply {\n    plugin(\"kotlin\")\n    plugin(rootProject.libs.plugins.spotless.get().pluginId)\n    plugin(rootProject.libs.plugins.testLogger.get().pluginId)\n    plugin(rootProject.libs.plugins.kover.get().pluginId)\n    plugin(rootProject.libs.plugins.detekt.get().pluginId)\n    plugin(\"idea\")\n  }\n\n  val testImplementation by configurations\n  val libs = rootProject.libs\n  detekt {\n    buildUponDefaultConfig = true\n    parallel = true\n    config.from(rootProject.file(\"detekt.yml\"))\n  }\n  dependencies {\n    testImplementation(kotlin(\"test\"))\n    testImplementation(libs.kotest.runner.junit5)\n    testImplementation(libs.kotest.framework.engine)\n    testImplementation(libs.kotest.assertions.core)\n    detektPlugins(libs.detekt.formatting)\n  }\n\n  spotless {\n    kotlin {\n      ktlint(libs.ktlint.cli.get().version)\n        .setEditorConfigPath(rootProject.layout.projectDirectory.file(\".editorconfig\"))\n        .editorConfigOverride(\n          mapOf(\n            \"ktlint_standard_kdoc\" to \"disabled\",\n            \"ktlint_standard_class-signature\" to \"disabled\"\n          )\n        )\n      targetExclude(\"build/\", \"generated/\", \"out/\")\n      targetExcludeIfContentContains(\"generated\")\n      targetExcludeIfContentContainsRegex(\"generated.*\")\n    }\n\n    kotlinGradle {\n      ktlint(libs.ktlint.cli.get().version)\n        .setEditorConfigPath(rootProject.layout.projectDirectory.file(\".editorconfig\"))\n        .editorConfigOverride(\n          mapOf(\n            \"ktlint_standard_kdoc\" to \"disabled\",\n            \"ktlint_standard_class-signature\" to \"disabled\"\n          )\n        )\n      targetExclude(\"build/\", \"generated/\", \"out/\")\n    }\n  }\n  the<IdeaModel>().apply {\n    module {\n      isDownloadSources = true\n      isDownloadJavadoc = true\n    }\n  }\n\n  tasks {\n    test {\n      useJUnitPlatform()\n      // Fail fast on CI to save time\n      failFast = runningOnCI\n      testlogger {\n        setTheme(\"mocha\")\n        showStandardStreams = !runningOnCI\n        showExceptions = true\n        showCauses = true\n      }\n      reports {\n        junitXml.required.set(true)\n      }\n      jvmArgs(\"--add-opens\", \"java.base/java.util=ALL-UNNAMED\")\n    }\n    kotlin {\n      jvmToolchain(17)\n      compilerOptions {\n        jvmTarget.set(JvmTarget.JVM_17)\n        allWarningsAsErrors = true\n        freeCompilerArgs.addAll(\"-Xjsr305=strict\")\n      }\n    }\n  }\n}\n\nval publishedProjects = listOf(\n  projects.lib.stoveBom.name,\n  projects.lib.stove.name,\n  projects.lib.stoveTracing.name,\n  projects.lib.stoveCouchbase.name,\n  projects.lib.stoveElasticsearch.name,\n  projects.lib.stoveGrpc.name,\n  projects.lib.stoveGrpcMock.name,\n  projects.lib.stoveHttp.name,\n  projects.lib.stoveKafka.name,\n  projects.lib.stoveMongodb.name,\n  projects.lib.stoveRdbms.name,\n  projects.lib.stovePostgres.name,\n  projects.lib.stoveMysql.name,\n  projects.lib.stoveMssql.name,\n  projects.lib.stoveWiremock.name,\n  projects.lib.stoveRedis.name,\n  projects.lib.stoveCassandra.name,\n  projects.lib.stoveDashboard.name,\n  projects.lib.stoveDashboardApi.name,\n  projects.starters.ktor.stoveKtor.name,\n  projects.starters.quarkus.stoveQuarkus.name,\n  projects.starters.spring.stoveSpring.name,\n  projects.starters.spring.stoveSpringKafka.name,\n  projects.starters.micronaut.stoveMicronaut.name,\n  projects.starters.container.stoveContainer.name,\n  projects.starters.process.stoveProcess.name,\n  projects.testExtensions.stoveExtensionsKotest.name,\n  projects.testExtensions.stoveExtensionsJunit.name,\n  projects.plugins.stoveTracingGradlePlugin.name,\n)\n\nsubprojects.of(\"lib\", \"spring\", \"ktor\", \"quarkus\", \"micronaut\", \"container\", \"process\", \"test-extensions\", \"plugins\", filter = { p -> publishedProjects.contains(p.name) && p.name != \"stove-bom\" }) {\n  apply {\n    plugin(\"java\")\n    plugin(rootProject.libs.plugins.maven.publish.pluginId)\n  }\n\n  mavenPublishing {\n    coordinates(groupId = rootProject.group.toString(), artifactId = project.name, version = rootProject.version.toString())\n    publishToMavenCentral()\n    pom {\n      name.set(project.name)\n      description.set(project.properties[\"projectDescription\"].toString())\n      url.set(project.properties[\"projectUrl\"].toString())\n      licenses {\n        license {\n          name.set(project.properties[\"licence\"].toString())\n          url.set(project.properties[\"licenceUrl\"].toString())\n        }\n      }\n      developers {\n        developer {\n          id.set(\"osoykan\")\n          name.set(\"Oguzhan Soykan\")\n          email.set(\"oguzhan.soykan@trendyol.com\")\n        }\n      }\n      scm {\n        connection.set(\"scm:git@github.com:Trendyol/stove.git\")\n        developerConnection.set(\"scm:git:ssh://github.com:Trendyol/stove.git\")\n        url.set(project.properties[\"projectUrl\"].toString())\n      }\n    }\n    if (project.hasSigningKey) signAllPublications()\n  }\n\n  java {\n    withSourcesJar()\n  }\n}\n"
  },
  {
    "path": "buildSrc/build.gradle.kts",
    "content": "plugins {\n    `kotlin-dsl`\n}\n\nrepositories {\n    mavenCentral()\n    google()\n    gradlePluginPortal()\n}\n"
  },
  {
    "path": "buildSrc/settings.gradle.kts",
    "content": "rootProject.name = \"buildSrc\"\n\ndependencyResolutionManagement {\n    versionCatalogs {\n        create(\"libs\").from(files(\"../gradle/libs.versions.toml\"))\n    }\n}\n\nenableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/BuildConstants.kt",
    "content": "object TestFolders {\n  const val e2e = \"test-e2e\"\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/CI.kt",
    "content": "import org.gradle.api.Project\n\nval Project.hasSigningKey: Boolean\n  get() =\n    rootProject.findProperty(\"signing.keyId\") != null ||\n      rootProject.findProperty(\"signingInMemoryKey\") != null ||\n      System.getenv(\"ORG_GRADLE_PROJECT_signingInMemoryKey\") != null\n\nobject CI {\n  private val isSnapshot: Boolean\n    get() = System.getenv(\"SNAPSHOT\") != null && System.getenv(\"SNAPSHOT\") == \"true\"\n\n  private val Project.snapshotBase: String\n    get() = properties[\"snapshot\"].toString()\n\n  private val Project.releaseVersion: String\n    get() = properties[\"version\"].toString()\n\n  private val buildNumber: String\n    get() = System.getenv(\"BUILD_NUMBER\") ?: \"0\"\n\n  fun version(project: Project): String = when {\n    isSnapshot -> \"${project.snapshotBase}.${buildNumber}-SNAPSHOT\"\n    else -> project.properties[\"version\"].toString()\n  }\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/GenerateDashboardVersionSourceTask.kt",
    "content": "import org.gradle.api.DefaultTask\nimport org.gradle.api.file.DirectoryProperty\nimport org.gradle.api.provider.Property\nimport org.gradle.api.tasks.Input\nimport org.gradle.api.tasks.OutputDirectory\nimport org.gradle.api.tasks.TaskAction\n\nabstract class GenerateDashboardVersionSourceTask : DefaultTask() {\n  @get:Input\n  abstract val stoveCompatibilityVersion: Property<String>\n\n  @get:OutputDirectory\n  abstract val outputDir: DirectoryProperty\n\n  @TaskAction\n  fun generate() {\n    val outputFile = outputDir\n      .file(\"com/trendyol/stove/dashboard/StoveCompatibilityVersion.kt\")\n      .get()\n      .asFile\n    outputFile.parentFile.mkdirs()\n    outputFile.writeText(\n      \"\"\"\n      package com.trendyol.stove.dashboard\n\n      internal object StoveCompatibilityVersion {\n        const val VALUE: String = \"${stoveCompatibilityVersion.get()}\"\n      }\n      \"\"\".trimIndent()\n    )\n  }\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/Helpers.kt",
    "content": "import org.gradle.api.*\nimport org.gradle.api.provider.Property\nimport org.gradle.api.provider.Provider\nimport org.gradle.kotlin.dsl.invoke\nimport org.gradle.plugin.use.PluginDependency\n\nfun Collection<Project>.of(\n  vararg parentProjects: String,\n  except: List<String> = emptyList(),\n  action: Action<Project>\n): Unit = this.filter {\n  parentProjects.contains(it.parent?.name) && !except.contains(it.name)\n}.forEach { action(it) }\n\nfun Collection<Project>.of(\n  vararg parentProjects: String,\n  except: List<String> = emptyList()\n): List<Project> = this.filter {\n  parentProjects.contains(it.parent?.name) && !except.contains(it.name)\n}\n\nfun Collection<Project>.of(\n  vararg parentProjects: String,\n  action: Action<Project>\n): Unit = this.filter { parentProjects.contains(it.parent?.name) }.forEach { action(it) }\n\nfun Collection<Project>.of(\n  vararg parentProjects: String,\n  filter: (Project) -> Boolean,\n  action: Action<Project>\n): Unit = this.filter { parentProjects.contains(it.parent?.name) && filter(it) }.forEach { action(it) }\n\nval runningOnCI: Boolean\n  get() = System.getenv(\"CI\") != null\n    || System.getenv(\"GITHUB_ACTIONS\") != null\n    || System.getenv(\"GITLAB_CI\") != null\n    || System.getenv(\"CIRCLECI\") != null\n    || System.getenv(\"TRAVIS\") != null\n    || System.getenv(\"TEAMCITY_VERSION\") != null\n    || System.getenv(\"JENKINS_URL\") != null\n\nval Provider<PluginDependency>.pluginId: String\n  get() = get().pluginId\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/com/trendyol/stove/gradle/StoveTracingConfiguration.kt",
    "content": "@file:Suppress(\"TooManyFunctions\")\n\npackage com.trendyol.stove.gradle\n\nimport org.gradle.api.Project\nimport org.gradle.api.artifacts.Configuration\nimport org.gradle.api.tasks.testing.Test\nimport java.net.ServerSocket\n\n/*\n * ════════════════════════════════════════════════════════════════════════════════\n * STOVE TRACING CONFIGURATION (buildSrc version)\n * ════════════════════════════════════════════════════════════════════════════════\n *\n * PREFERRED APPROACH: Use the Gradle plugin instead of copying this file.\n *\n *   plugins {\n *       id(\"com.trendyol.stove.tracing\") version \"<stove-version>\"\n *   }\n *\n *   stoveTracing {\n *       serviceName.set(\"my-service\")\n *   }\n *\n * The plugin is available on Maven Central: com.trendyol:stove-tracing-gradle-plugin\n *\n * Add mavenCentral() to your pluginManagement repositories.\n * See: https://github.com/Trendyol/stove\n *\n * ────────────────────────────────────────────────────────────────────────\n * LEGACY: buildSrc copy-paste approach (kept for internal Stove usage)\n * ────────────────────────────────────────────────────────────────────────\n *\n * This file configures the OpenTelemetry Java Agent for Stove test tracing.\n * When a test fails, Stove can display the execution trace showing exactly\n * what happened during the test - HTTP calls, Kafka messages, database queries, etc.\n *\n * HOW TO USE IN YOUR PROJECT:\n * ───────────────────────────\n * 1. Copy this file to your project's buildSrc/src/main/kotlin/ directory\n *    (create the directory structure if it doesn't exist)\n *\n * 2. In your test module's build.gradle.kts, add:\n *\n *    import com.trendyol.stove.gradle.stoveTracing\n *\n *    stoveTracing {\n *        serviceName = \"my-service\"\n *    }\n *\n * 3. In your Stove test setup, enable tracing:\n *\n *    Stove(...)\n *        .with {\n *            tracing {\n *                enableSpanReceiver() // Port is auto-configured from STOVE_TRACING_PORT env var\n *            }\n *            // ... other systems\n *        }\n *\n *    Note: Service name is automatically extracted from incoming spans (set by OTel agent)\n *\n * CONFIGURATION OPTIONS:\n * ──────────────────────\n * - serviceName: The service name shown in traces (required)\n * - enabled: Toggle tracing on/off (default: true)\n * - protocol: grpc (default and currently the only supported protocol)\n * - testTaskNames: Apply only to specific tasks (default: all test tasks)\n * - otelAgentVersion: OTel agent version (default: 2.24.0)\n *\n * Note: The OTLP port is dynamically assigned to avoid conflicts when running\n * parallel tests. The port is passed to tests via STOVE_TRACING_PORT env var.\n *\n * ADVANCED USAGE:\n * ───────────────\n * // Apply only to integration tests\n * stoveTracing {\n *     serviceName = \"my-service\"\n *     testTaskNames = listOf(\"integrationTest\")\n * }\n *\n * // Custom OTel agent version\n * stoveTracing {\n *     serviceName = \"my-service\"\n *     otelAgentVersion = \"2.25.0\"\n * }\n *\n * // Disable specific instrumentations\n * stoveTracing {\n *     serviceName = \"my-service\"\n *     disabledInstrumentations = listOf(\"jdbc\", \"hibernate\")\n * }\n *\n * ════════════════════════════════════════════════════════════════════════════════\n */\n\n/**\n * Constants for Stove tracing configuration.\n */\nprivate object TracingDefaults {\n  const val DEFAULT_BSP_SCHEDULE_DELAY = 100\n  const val DEFAULT_BSP_MAX_BATCH_SIZE = 1\n  const val DEFAULT_OTEL_AGENT_VERSION = \"2.24.0\"\n  const val DEFAULT_PROTOCOL = \"grpc\"\n  const val SUPPORTED_PROTOCOL = DEFAULT_PROTOCOL\n\n  /** Environment variable name for passing the OTLP port to tests */\n  const val STOVE_TRACING_PORT_ENV = \"STOVE_TRACING_PORT\"\n}\n\n/**\n * Configures Stove tracing for a Gradle project.\n *\n * This function provides a simple way to set up OpenTelemetry Java Agent\n * for test tracing without needing to apply a plugin.\n *\n * Example usage in build.gradle.kts:\n * ```kotlin\n * import com.trendyol.stove.gradle.stoveTracing\n *\n * stoveTracing {\n *     serviceName = \"my-service\"\n * }\n * ```\n *\n * To configure only specific test tasks:\n * ```kotlin\n * stoveTracing {\n *     serviceName = \"my-service\"\n *     testTaskNames = listOf(\"integrationTest\") // Only apply to integrationTest task\n * }\n * ```\n */\nfun Project.stoveTracing(configure: StoveTracingConfig.() -> Unit = {}) {\n  val config = StoveTracingConfig().apply(configure)\n  validateProtocol(config.protocol)\n\n  // Create otelAgent configuration\n  val otelAgentConfig = configurations.create(\"otelAgent\").apply {\n    isTransitive = false\n    isCanBeResolved = true\n    isCanBeConsumed = false\n    description = \"OpenTelemetry Java Agent for Stove test tracing\"\n  }\n\n  // Add OTel agent dependency after evaluation\n  afterEvaluate {\n    if (!config.enabled) {\n      logger.info(\"Stove tracing is disabled, skipping configuration\")\n      return@afterEvaluate\n    }\n\n    dependencies.add(\n      \"otelAgent\",\n      \"io.opentelemetry.javaagent:opentelemetry-javaagent:${config.otelAgentVersion}\"\n    )\n\n    // Configure test tasks\n    val testTasks = resolveTestTasks(config)\n    testTasks.forEach { testTask ->\n      configureTestTask(testTask, otelAgentConfig, config)\n    }\n\n    logConfiguration(config, testTasks)\n  }\n}\n\nprivate fun validateProtocol(protocol: String) {\n  require(protocol == TracingDefaults.SUPPORTED_PROTOCOL) {\n    \"Unsupported OTLP protocol '$protocol'. Stove tracing receiver currently supports only \" +\n      \"'${TracingDefaults.SUPPORTED_PROTOCOL}'.\"\n  }\n}\n\nprivate fun Project.resolveTestTasks(config: StoveTracingConfig): List<Test> =\n  if (config.testTaskNames.isEmpty()) {\n    tasks.withType(Test::class.java).toList()\n  } else {\n    config.testTaskNames.mapNotNull { taskName ->\n      tasks.findByName(taskName) as? Test\n    }\n  }\n\nprivate fun Project.logConfiguration(config: StoveTracingConfig, testTasks: List<Test>) {\n  val taskInfo = if (config.testTaskNames.isEmpty()) {\n    \"all test tasks\"\n  } else {\n    \"tasks: ${testTasks.joinToString(\", \") { it.name }}\"\n  }\n  logger.info(\n    \"Stove tracing configured for service '${config.serviceName}' \" +\n      \"with dynamic port assignment on $taskInfo\"\n  )\n}\n\nprivate fun configureTestTask(\n  testTask: Test,\n  otelAgentConfig: Configuration,\n  config: StoveTracingConfig\n) {\n  // Resolve at configuration time to avoid capturing Configuration in doFirst\n  // This is required for Gradle configuration cache compatibility\n  val resolvedAgentPath: String? = otelAgentConfig.resolve().firstOrNull()?.absolutePath\n\n  // Extract config values to a serializable format for configuration cache compatibility\n  val tracingConfig = ResolvedTracingConfig.from(config)\n\n  testTask.doFirst {\n    if (resolvedAgentPath == null) {\n      testTask.logger.warn(\"No OTel agent JAR found in otelAgent configuration\")\n      return@doFirst\n    }\n\n    // Find an available port dynamically to avoid conflicts when running multiple test tasks\n    val port = findAvailablePort()\n    testTask.environment(TracingDefaults.STOVE_TRACING_PORT_ENV, port.toString())\n\n    val jvmArgs = buildJvmArgs(resolvedAgentPath, tracingConfig, port)\n\n    testTask.jvmArgs(jvmArgs)\n    testTask.logger.info(\n      \"Stove tracing: Attached OTel agent on port {} with {} JVM arguments\",\n      port,\n      jvmArgs.size\n    )\n  }\n}\n\nprivate fun findAvailablePort(): Int =\n  ServerSocket(0).use { it.localPort }\n\n/**\n * Serializable copy of tracing config for Gradle configuration cache compatibility.\n * Configuration cache requires all objects captured in task actions to be serializable.\n */\nprivate data class ResolvedTracingConfig(\n  val protocol: String,\n  val serviceName: String,\n  val bspScheduleDelay: Int,\n  val bspMaxBatchSize: Int,\n  val captureHttpHeaders: Boolean,\n  val captureExperimentalTelemetry: Boolean,\n  val customAnnotations: List<String>,\n  val disabledInstrumentations: List<String>,\n  val additionalInstrumentations: List<String>\n) : java.io.Serializable {\n  companion object {\n    private const val serialVersionUID: Long = 1L\n\n    fun from(config: StoveTracingConfig) = ResolvedTracingConfig(\n      protocol = config.protocol,\n      serviceName = config.serviceName,\n      bspScheduleDelay = config.bspScheduleDelay,\n      bspMaxBatchSize = config.bspMaxBatchSize,\n      captureHttpHeaders = config.captureHttpHeaders,\n      captureExperimentalTelemetry = config.captureExperimentalTelemetry,\n      customAnnotations = config.customAnnotations.toList(),\n      disabledInstrumentations = config.disabledInstrumentations.toList(),\n      additionalInstrumentations = config.additionalInstrumentations.toList()\n    )\n  }\n}\n\nprivate fun buildJvmArgs(agentPath: String, config: ResolvedTracingConfig, port: Int): List<String> = buildList {\n  // Agent attachment\n  add(\"-javaagent:$agentPath\")\n\n  // Core export configuration\n  addAll(buildCoreExportArgs(config, port))\n\n  // Propagation\n  add(\"-Dotel.propagators=tracecontext,baggage\")\n\n  // Test optimization\n  addAll(buildTestOptimizationArgs(config))\n\n  // HTTP headers capture\n  if (config.captureHttpHeaders) {\n    addAll(buildHttpHeaderCaptureArgs())\n  }\n\n  // Experimental telemetry\n  if (config.captureExperimentalTelemetry) {\n    addAll(buildExperimentalTelemetryArgs())\n  }\n\n  // Custom annotations\n  if (config.customAnnotations.isNotEmpty()) {\n    add(\"-Dotel.instrumentation.annotations.methods=${config.customAnnotations.joinToString(\",\")}\")\n  }\n\n  // Instrumentation control\n  addAll(buildInstrumentationControlArgs(config))\n}\n\nprivate fun buildCoreExportArgs(config: ResolvedTracingConfig, port: Int): List<String> = buildList {\n  val endpoint = \"http://localhost:$port\"\n  add(\"-Dotel.traces.exporter=otlp\")\n  add(\"-Dotel.exporter.otlp.protocol=${config.protocol}\")\n  add(\"-Dotel.exporter.otlp.endpoint=$endpoint\")\n  add(\"-Dotel.metrics.exporter=none\")\n  add(\"-Dotel.logs.exporter=none\")\n  add(\"-Dotel.service.name=${config.serviceName}\")\n  add(\"-Dotel.resource.attributes=service.name=${config.serviceName},deployment.environment=test\")\n\n  // Disable gRPC instrumentation when using gRPC protocol to avoid instrumenting the exporter\n  if (config.protocol == \"grpc\") {\n    add(\"-Dotel.instrumentation.grpc.enabled=false\")\n  }\n}\n\nprivate fun buildTestOptimizationArgs(config: ResolvedTracingConfig): List<String> = listOf(\n  \"-Dotel.traces.sampler=always_on\",\n  \"-Dotel.bsp.schedule.delay=${config.bspScheduleDelay}\",\n  \"-Dotel.bsp.max.export.batch.size=${config.bspMaxBatchSize}\"\n)\n\nprivate fun buildHttpHeaderCaptureArgs(): List<String> = listOf(\n  \"-Dotel.instrumentation.http.client.capture-request-headers=content-type,accept,x-stove-test-id\",\n  \"-Dotel.instrumentation.http.client.capture-response-headers=content-type\",\n  \"-Dotel.instrumentation.http.server.capture-request-headers=content-type,accept,user-agent,x-stove-test-id\",\n  \"-Dotel.instrumentation.http.server.capture-response-headers=content-type\"\n)\n\nprivate fun buildExperimentalTelemetryArgs(): List<String> = listOf(\n  \"-Dotel.instrumentation.http.client.emit-experimental-telemetry=true\",\n  \"-Dotel.instrumentation.http.server.emit-experimental-telemetry=true\",\n  \"-Dotel.instrumentation.servlet.experimental.capture-request-parameters=*\"\n)\n\nprivate fun buildInstrumentationControlArgs(config: ResolvedTracingConfig): List<String> = buildList {\n  if (config.disabledInstrumentations.isNotEmpty()) {\n    add(\"-Dotel.instrumentation.common.default-enabled=true\")\n    addAll(config.disabledInstrumentations.map { \"-Dotel.instrumentation.$it.enabled=false\" })\n  }\n  addAll(config.additionalInstrumentations.map { \"-Dotel.instrumentation.$it.enabled=true\" })\n}\n\n/**\n * Configuration for Stove tracing.\n *\n * @see stoveTracing\n */\nclass StoveTracingConfig {\n  /** The service name to use in traces. This should match your application's service name. */\n  var serviceName: String = \"stove-traced-app\"\n\n  /** Whether tracing is enabled. Set to false to disable tracing without removing configuration. */\n  var enabled: Boolean = true\n\n  /**\n   * The OTLP protocol to use.\n   * Currently only \"grpc\" is supported.\n   * Note: The port is dynamically assigned to avoid conflicts when running parallel tests.\n   */\n  var protocol: String = TracingDefaults.DEFAULT_PROTOCOL\n    set(value) {\n      require(value == TracingDefaults.SUPPORTED_PROTOCOL) {\n        \"Unsupported OTLP protocol '$value'. Supported protocol: '${TracingDefaults.SUPPORTED_PROTOCOL}'.\"\n      }\n      field = value\n    }\n\n  /** The batch span processor schedule delay in milliseconds. Lower = faster export. */\n  var bspScheduleDelay: Int = TracingDefaults.DEFAULT_BSP_SCHEDULE_DELAY\n\n  /** The maximum batch size for span export. 1 = immediate export per span. */\n  var bspMaxBatchSize: Int = TracingDefaults.DEFAULT_BSP_MAX_BATCH_SIZE\n\n  /** Whether to capture HTTP headers in spans. Useful for debugging request/response details. */\n  var captureHttpHeaders: Boolean = true\n\n  /** Whether to enable experimental HTTP telemetry features. */\n  var captureExperimentalTelemetry: Boolean = true\n\n  /** List of instrumentation modules to disable. Example: listOf(\"jdbc\", \"hibernate\") */\n  var disabledInstrumentations: List<String> = emptyList()\n\n  /** List of additional instrumentation modules to enable. */\n  var additionalInstrumentations: List<String> = emptyList()\n\n  /** List of custom annotation class names to instrument. */\n  var customAnnotations: List<String> = emptyList()\n\n  /** The OpenTelemetry Java Agent version to use. */\n  var otelAgentVersion: String = TracingDefaults.DEFAULT_OTEL_AGENT_VERSION\n\n  /**\n   * List of test task names to configure. If empty, applies to all test tasks.\n   * Example: listOf(\"integrationTest\") to only apply to the integrationTest task.\n   */\n  var testTaskNames: List<String> = emptyList()\n}\n"
  },
  {
    "path": "buildSrc/src/main/kotlin/stove-publishing.gradle.kts",
    "content": "plugins {\n  `maven-publish`\n  signing\n  java\n}\nfun getProperty(\n  projectKey: String,\n  environmentKey: String\n): String? {\n  return if (project.hasProperty(projectKey)) {\n    project.property(projectKey) as? String?\n  } else {\n    System.getenv(environmentKey)\n  }\n}\n\npublishing {\n  publications {\n    create<MavenPublication>(\"publish-${project.name}\") {\n      groupId = rootProject.group.toString()\n      version = rootProject.version.toString()\n      println(\"version to be published: ${rootProject.version}\")\n      artifactId = project.name\n      from(components[\"java\"])\n      pom {\n        name.set(project.name)\n        description.set(project.properties[\"projectDescription\"].toString())\n        url.set(project.properties[\"projectUrl\"].toString())\n        packaging = \"jar\"\n        licenses {\n          license {\n            name.set(project.properties[\"licence\"].toString())\n            url.set(project.properties[\"licenceUrl\"].toString())\n          }\n        }\n        developers {\n          developer {\n            id.set(\"osoykan\")\n            name.set(\"Oguzhan Soykan\")\n            email.set(\"oguzhan.soykan@trendyol.com\")\n          }\n        }\n        scm {\n          connection.set(\"scm:git@github.com:Trendyol/stove.git\")\n          developerConnection.set(\"scm:git:ssh://github.com:Trendyol/stove.git\")\n          url.set(project.properties[\"projectUrl\"].toString())\n        }\n      }\n    }\n  }\n\n  repositories {\n    maven {\n      val releasesRepoUrl = uri(\"https://oss.sonatype.org/service/local/staging/deploy/maven2/\")\n      val snapshotsRepoUrl = uri(\"https://oss.sonatype.org/content/repositories/snapshots/\")\n      url = if (rootProject.version.toString().endsWith(\"SNAPSHOT\")) snapshotsRepoUrl else releasesRepoUrl\n      credentials {\n        username = getProperty(\"nexus_username\", \"nexus_username\")\n        password = getProperty(\"nexus_password\", \"nexus_password\")\n      }\n    }\n    maven {\n      name = \"GitHubPackages\"\n      url = uri(\"https://maven.pkg.github.com/trendyol/stove\")\n      credentials {\n        username = System.getenv(\"GITHUB_ACTOR\")\n        password = System.getenv(\"GITHUB_TOKEN\")\n      }\n    }\n  }\n}\n\nval signingKey = getProperty(projectKey = \"gpg.key\", environmentKey = \"gpg_private_key\")\nval passPhrase = getProperty(projectKey = \"gpg.passphrase\", environmentKey = \"gpg_passphrase\")\nsigning {\n  if (passPhrase == null && runningOnCI) {\n    logger.warn(\n      \"The passphrase for the signing key was not found. \" +\n        \"Either provide it as env variable 'gpg_passphrase' or \" +\n        \"as project property 'gpg_passphrase'. Otherwise the signing might fail!\"\n    )\n  }\n  useInMemoryPgpKeys(signingKey, passPhrase)\n  sign(publishing.publications)\n}\n"
  },
  {
    "path": "codecov.yml",
    "content": "ignore:\n  - \"lib/stove/src/main/kotlin/com/trendyol/stove/functional\"\n  - \"lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions\"\n  - \"lib/stove/src/main/kotlin/com/trendyol/stove/system/annotations\"\n  - \"lib/stove/src/main/kotlin/com/trendyol/stove/serialization\"\n  - \"examples/ktor-example\"\n  - \"examples/micronaut-example\"\n  - \"examples/spring-4x-example\"\n  - \"examples/spring-example\"\n  - \"examples/spring-standalone-example\"\n  - \"examples/spring-streams-example\"\n  - \"test-extensions/stove-extensions-kotest/src/test\"\n  - \"test-extensions/stove-extensions-junit/src/test\"\n  - \"recipes\"\n  - buildSrc\n  - starters/*/tests/*"
  },
  {
    "path": "detekt.yml",
    "content": "build:\n  maxIssues: 0\n  excludeCorrectable: false\n\nconfig:\n  validation: true\n  warningsAsErrors: true\n  excludes: ''\n\nprocessors:\n  active: true\n  exclude:\n    - 'DetektProgressListener'\n\nconsole-reports:\n  active: true\n  exclude:\n    - 'ProjectStatisticsReport'\n    - 'ComplexityReport'\n    - 'NotificationReport'\n    - 'FindingsReport'\n    - 'FileBasedFindingsReport'\n\noutput-reports:\n  active: false\n\nformatting:\n  Indentation:\n    active: false\n    indentSize: 2\n    autoCorrect: true\n  NoWildcardImports:\n    active: false\n  MaximumLineLength:\n    active: true\n    maxLineLength: 140\n    excludes: [ '**/test/**', '**/test-e2e/**' ]\n  ArgumentListWrapping:\n    maxLineLength: 140\n    autoCorrect: true\n    active: true\n    indentSize: 2\n  Filename:\n    active: false\n\ncomments:\n  active: true\n  AbsentOrWrongFileLicense:\n    active: false\n    licenseTemplateFile: 'license.template'\n    licenseTemplateIsRegex: false\n  CommentOverPrivateFunction:\n    active: false\n  CommentOverPrivateProperty:\n    active: false\n  DeprecatedBlockTag:\n    active: false\n  EndOfSentenceFormat:\n    active: false\n    endOfSentenceFormat: '([.?!][ \\t\\n\\r\\f<])|([.?!:]$)'\n  KDocReferencesNonPublicProperty:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  OutdatedDocumentation:\n    active: false\n    matchTypeParameters: true\n    matchDeclarationsOrder: true\n    allowParamOnConstructorProperties: false\n  UndocumentedPublicClass:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    searchInNestedClass: true\n    searchInInnerClass: true\n    searchInInnerObject: true\n    searchInInnerInterface: true\n  UndocumentedPublicFunction:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  UndocumentedPublicProperty:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n\ncomplexity:\n  active: true\n  ComplexCondition:\n    active: true\n    threshold: 4\n  ComplexInterface:\n    active: false\n    threshold: 10\n    includeStaticDeclarations: false\n    includePrivateDeclarations: false\n  CyclomaticComplexMethod:\n    active: true\n    threshold: 15\n    ignoreSingleWhenExpression: false\n    ignoreSimpleWhenEntries: false\n    ignoreNestingFunctions: false\n    nestingFunctions:\n      - 'also'\n      - 'apply'\n      - 'forEach'\n      - 'isNotNull'\n      - 'ifNull'\n      - 'let'\n      - 'run'\n      - 'use'\n      - 'with'\n  LabeledExpression:\n    active: false\n    ignoredLabels: [ ]\n  LargeClass:\n    active: true\n    threshold: 600\n  LongMethod:\n    active: true\n    threshold: 60\n  LongParameterList:\n    active: true\n    functionThreshold: 20\n    constructorThreshold: 20\n    ignoreDefaultParameters: false\n    ignoreDataClasses: true\n    ignoreAnnotatedParameter: [ ]\n  MethodOverloading:\n    active: false\n    threshold: 6\n  NamedArguments:\n    active: false\n    threshold: 3\n    ignoreArgumentsMatchingNames: false\n  NestedBlockDepth:\n    active: true\n    threshold: 4\n  NestedScopeFunctions:\n    active: false\n    threshold: 1\n    functions:\n      - 'kotlin.apply'\n      - 'kotlin.run'\n      - 'kotlin.with'\n      - 'kotlin.let'\n      - 'kotlin.also'\n  ReplaceSafeCallChainWithRun:\n    active: false\n  StringLiteralDuplication:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    threshold: 3\n    ignoreAnnotation: true\n    excludeStringsWithLessThan5Characters: true\n    ignoreStringsRegex: '$^'\n  TooManyFunctions:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    thresholdInFiles: 30\n    thresholdInClasses: 30\n    thresholdInInterfaces: 115\n    thresholdInObjects: 15\n    thresholdInEnums: 15\n    ignoreDeprecated: false\n    ignorePrivate: false\n    ignoreOverridden: false\n\ncoroutines:\n  active: true\n  GlobalCoroutineUsage:\n    active: false\n  InjectDispatcher:\n    active: false\n    dispatcherNames:\n      - 'IO'\n      - 'Default'\n      - 'Unconfined'\n  RedundantSuspendModifier:\n    active: false\n  SleepInsteadOfDelay:\n    active: true\n  SuspendFunWithCoroutineScopeReceiver:\n    active: false\n  SuspendFunWithFlowReturnType:\n    active: true\n\nempty-blocks:\n  active: true\n  EmptyCatchBlock:\n    active: true\n    allowedExceptionNameRegex: '_|(ignore|expected).*'\n  EmptyClassBlock:\n    active: true\n  EmptyDefaultConstructor:\n    active: true\n  EmptyDoWhileBlock:\n    active: true\n  EmptyElseBlock:\n    active: true\n  EmptyFinallyBlock:\n    active: true\n  EmptyForBlock:\n    active: true\n  EmptyFunctionBlock:\n    active: true\n    ignoreOverridden: false\n  EmptyIfBlock:\n    active: true\n  EmptyInitBlock:\n    active: true\n  EmptyKtFile:\n    active: true\n  EmptySecondaryConstructor:\n    active: true\n  EmptyTryBlock:\n    active: true\n  EmptyWhenBlock:\n    active: true\n  EmptyWhileBlock:\n    active: true\n\nexceptions:\n  active: true\n  ExceptionRaisedInUnexpectedLocation:\n    active: true\n    methodNames:\n      - 'equals'\n      - 'finalize'\n      - 'hashCode'\n      - 'toString'\n  InstanceOfCheckForException:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  NotImplementedDeclaration:\n    active: false\n  ObjectExtendsThrowable:\n    active: false\n  PrintStackTrace:\n    active: true\n  RethrowCaughtException:\n    active: true\n  ReturnFromFinally:\n    active: true\n    ignoreLabeled: false\n  SwallowedException:\n    active: true\n    ignoredExceptionTypes:\n      - 'InterruptedException'\n      - 'MalformedURLException'\n      - 'NumberFormatException'\n      - 'ParseException'\n    allowedExceptionNameRegex: '_|(ignore|expected).*'\n  ThrowingExceptionFromFinally:\n    active: true\n  ThrowingExceptionInMain:\n    active: false\n  ThrowingExceptionsWithoutMessageOrCause:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    exceptions:\n      - 'ArrayIndexOutOfBoundsException'\n      - 'Exception'\n      - 'IllegalArgumentException'\n      - 'IllegalMonitorStateException'\n      - 'IllegalStateException'\n      - 'IndexOutOfBoundsException'\n      - 'NullPointerException'\n      - 'RuntimeException'\n      - 'Throwable'\n  ThrowingNewInstanceOfSameException:\n    active: true\n  TooGenericExceptionCaught:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    exceptionNames:\n      - 'ArrayIndexOutOfBoundsException'\n      - 'Error'\n      - 'Exception'\n      - 'IllegalMonitorStateException'\n      - 'IndexOutOfBoundsException'\n      - 'NullPointerException'\n      - 'RuntimeException'\n      - 'Throwable'\n    allowedExceptionNameRegex: '_|(ignore|expected).*'\n  TooGenericExceptionThrown:\n    active: true\n    exceptionNames:\n      - 'Error'\n      - 'Exception'\n      - 'RuntimeException'\n      - 'Throwable'\n\nnaming:\n  active: true\n  BooleanPropertyNaming:\n    active: false\n    allowedPattern: '^(is|has|are)'\n  ClassNaming:\n    active: true\n    classPattern: '[A-Z][a-zA-Z0-9]*'\n  EnumNaming:\n    active: true\n    enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'\n  ForbiddenClassName:\n    active: false\n    forbiddenName: [ ]\n  FunctionMaxLength:\n    active: false\n    maximumFunctionNameLength: 30\n  FunctionMinLength:\n    active: false\n    minimumFunctionNameLength: 3\n  InvalidPackageDeclaration:\n    active: true\n    rootPackage: ''\n    requireRootInDeclaration: false\n  LambdaParameterNaming:\n    active: false\n    parameterPattern: '[a-z][A-Za-z0-9]*|_'\n  MatchingDeclarationName:\n    active: false\n    mustBeFirst: true\n  MemberNameEqualsClassName:\n    active: true\n    ignoreOverridden: true\n  NoNameShadowing:\n    active: true\n  NonBooleanPropertyPrefixedWithIs:\n    active: false\n  ObjectPropertyNaming:\n    active: true\n    constantPattern: '[A-Za-z][_A-Za-z0-9]*'\n    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'\n    privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'\n  PackageNaming:\n    active: true\n    packagePattern: '[a-z]+(\\.[a-z][A-Za-z0-9]*)*'\n  TopLevelPropertyNaming:\n    active: true\n    constantPattern: '[A-Z][_A-Z0-9]*'\n    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'\n    privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'\n  VariableMaxLength:\n    active: false\n    maximumVariableNameLength: 64\n  VariableMinLength:\n    active: false\n    minimumVariableNameLength: 1\n  ConstructorParameterNaming:\n    active: false\n    parameterPattern: '[a-z][A-Za-z0-9]*|_'\n\n\nperformance:\n  active: true\n  ArrayPrimitive:\n    active: true\n  CouldBeSequence:\n    active: false\n    threshold: 3\n  ForEachOnRange:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  SpreadOperator:\n    active: false\n    excludes: [\n      '**/test/**',\n      '**/androidTest/**',\n      '**/commonTest/**',\n      '**/jvmTest/**',\n      '**/jsTest/**',\n      '**/iosTest/**',\n      '**/otel/**',\n    ]\n  UnnecessaryTemporaryInstantiation:\n    active: true\n\npotential-bugs:\n  active: true\n  AvoidReferentialEquality:\n    active: true\n    forbiddenTypePatterns:\n      - 'kotlin.String'\n  CastToNullableType:\n    active: false\n  Deprecation:\n    active: false\n  DontDowncastCollectionTypes:\n    active: false\n  DoubleMutabilityForCollection:\n    active: true\n    mutableTypes:\n      - 'kotlin.collections.MutableList'\n      - 'kotlin.collections.MutableMap'\n      - 'kotlin.collections.MutableSet'\n      - 'java.util.ArrayList'\n      - 'java.util.LinkedHashSet'\n      - 'java.util.HashSet'\n      - 'java.util.LinkedHashMap'\n      - 'java.util.HashMap'\n  ElseCaseInsteadOfExhaustiveWhen:\n    active: false\n  EqualsAlwaysReturnsTrueOrFalse:\n    active: true\n  EqualsWithHashCodeExist:\n    active: true\n  ExitOutsideMain:\n    active: false\n  ExplicitGarbageCollectionCall:\n    active: true\n  HasPlatformType:\n    active: true\n  IgnoredReturnValue:\n    active: true\n    restrictToConfig: true\n    returnValueAnnotations:\n      - '*.CheckResult'\n      - '*.CheckReturnValue'\n    ignoreReturnValueAnnotations:\n      - '*.CanIgnoreReturnValue'\n    ignoreFunctionCall: [ ]\n  ImplicitDefaultLocale:\n    active: true\n  ImplicitUnitReturnType:\n    active: false\n    allowExplicitReturnType: true\n  InvalidRange:\n    active: true\n  IteratorHasNextCallsNextMethod:\n    active: true\n  IteratorNotThrowingNoSuchElementException:\n    active: true\n  LateinitUsage:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    ignoreOnClassesPattern: ''\n  MapGetWithNotNullAssertionOperator:\n    active: true\n  MissingPackageDeclaration:\n    active: false\n    excludes: [ '**/*.kts' ]\n\n  NullCheckOnMutableProperty:\n    active: false\n  NullableToStringCall:\n    active: false\n  UnconditionalJumpStatementInLoop:\n    active: false\n  UnnecessaryNotNullOperator:\n    active: true\n  UnnecessarySafeCall:\n    active: true\n  UnreachableCatchBlock:\n    active: true\n  UnreachableCode:\n    active: true\n  UnsafeCallOnNullableType:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  UnsafeCast:\n    active: true\n  UnusedUnaryOperator:\n    active: true\n  UselessPostfixExpression:\n    active: true\n  WrongEqualsTypeParameter:\n    active: true\n\nstyle:\n  active: true\n  CanBeNonNullable:\n    active: false\n  CascadingCallWrapping:\n    active: false\n    includeElvis: true\n  ClassOrdering:\n    active: false\n  CollapsibleIfStatements:\n    active: false\n  DataClassContainsFunctions:\n    active: false\n    conversionFunctionPrefix:\n      - 'to'\n  DataClassShouldBeImmutable:\n    active: false\n  DestructuringDeclarationWithTooManyEntries:\n    active: true\n    maxDestructuringEntries: 6\n  EqualsNullCall:\n    active: true\n  EqualsOnSignatureLine:\n    active: false\n  ExplicitCollectionElementAccessMethod:\n    active: false\n  ExplicitItLambdaParameter:\n    active: true\n  ExpressionBodySyntax:\n    active: false\n    includeLineWrapping: false\n  ForbiddenComment:\n    active: false\n    comments:\n      - 'FIXME:'\n      - 'STOPSHIP:'\n      - 'TODO:'\n  ForbiddenImport:\n    active: false\n    imports: [ ]\n    forbiddenPatterns: ''\n  ForbiddenMethodCall:\n    active: false\n    methods:\n      - 'kotlin.io.print'\n      - 'kotlin.io.println'\n  ForbiddenSuppress:\n    active: false\n    rules: [ ]\n  ForbiddenVoid:\n    active: true\n    ignoreOverridden: false\n    ignoreUsageInGenerics: false\n  FunctionOnlyReturningConstant:\n    active: true\n    ignoreOverridableFunction: true\n    ignoreActualFunction: true\n    excludedFunctions:\n      - ''\n  LoopWithTooManyJumpStatements:\n    active: true\n    maxJumpCount: 1\n  MagicNumber:\n    active: true\n    excludes: [\n      '**/test/**',\n      '**/test-e2e/**',\n      '**/androidTest/**',\n      '**/commonTest/**',\n      '**/jvmTest/**',\n      '**/jsTest/**',\n      '**/iosTest/**',\n      '**/domain/**',\n      '**/core/**',\n      '**/*.kts' ]\n    ignoreNumbers:\n      - '-1'\n      - '0'\n      - '1'\n      - '2'\n    ignoreHashCodeFunction: true\n    ignorePropertyDeclaration: false\n    ignoreLocalVariableDeclaration: false\n    ignoreConstantDeclaration: true\n    ignoreCompanionObjectPropertyDeclaration: true\n    ignoreAnnotation: false\n    ignoreNamedArgument: true\n    ignoreEnums: false\n    ignoreRanges: false\n    ignoreExtensionFunctions: true\n  BracesOnIfStatements:\n    active: false\n  MandatoryBracesLoops:\n    active: false\n  MaxChainedCallsOnSameLine:\n    active: false\n    maxChainedCalls: 5\n  MaxLineLength:\n    active: true\n    maxLineLength: 140\n    excludePackageStatements: true\n    excludeImportStatements: true\n    excludeCommentStatements: false\n    excludes:\n      - '**/test/**'\n      - '**/test-e2e/**'\n      - '**/test-integration/**'\n  MayBeConst:\n    active: true\n  ModifierOrder:\n    active: true\n  MultilineLambdaItParameter:\n    active: false\n  NestedClassesVisibility:\n    active: true\n  NewLineAtEndOfFile:\n    active: true\n  NoTabs:\n    active: false\n  NullableBooleanCheck:\n    active: false\n  ObjectLiteralToLambda:\n    active: true\n  OptionalAbstractKeyword:\n    active: true\n  OptionalUnit:\n    active: false\n  BracesOnWhenStatements:\n    active: false\n  PreferToOverPairSyntax:\n    active: false\n  ProtectedMemberInFinalClass:\n    active: true\n  RedundantExplicitType:\n    active: false\n  RedundantHigherOrderMapUsage:\n    active: true\n  RedundantVisibilityModifierRule:\n    active: false\n  ReturnCount:\n    active: true\n    max: 5\n    excludedFunctions:\n      - 'equals'\n    excludeLabeled: false\n    excludeReturnFromLambda: true\n    excludeGuardClauses: false\n  SafeCast:\n    active: true\n  SerialVersionUIDInSerializableClass:\n    active: true\n  SpacingBetweenPackageAndImports:\n    active: false\n  ThrowsCount:\n    active: true\n    max: 2\n    excludeGuardClauses: false\n  TrailingWhitespace:\n    active: false\n  UnderscoresInNumericLiterals:\n    active: false\n    acceptableLength: 4\n    allowNonStandardGrouping: false\n  UnnecessaryAbstractClass:\n    active: true\n  UnnecessaryAnnotationUseSiteTarget:\n    active: false\n  UnnecessaryApply:\n    active: true\n  UnnecessaryBackticks:\n    active: false\n  UnnecessaryFilter:\n    active: true\n  UnnecessaryInheritance:\n    active: true\n  UnnecessaryInnerClass:\n    active: false\n  UnnecessaryLet:\n    active: false\n  UnnecessaryParentheses:\n    active: false\n  UntilInsteadOfRangeTo:\n    active: false\n  UnusedImports:\n    active: false\n  UnusedPrivateClass:\n    active: true\n  UnusedPrivateMember:\n    active: true\n    allowedNames: '(_|ignored|expected|serialVersionUID)'\n  UseAnyOrNoneInsteadOfFind:\n    active: true\n  UseArrayLiteralsInAnnotations:\n    active: true\n  UseCheckNotNull:\n    active: true\n  UseCheckOrError:\n    active: true\n  UseDataClass:\n    active: false\n    allowVars: false\n  UseEmptyCounterpart:\n    active: false\n  UseIfEmptyOrIfBlank:\n    active: false\n  UseIfInsteadOfWhen:\n    active: false\n  UseIsNullOrEmpty:\n    active: true\n  UseOrEmpty:\n    active: true\n  UseRequire:\n    active: true\n  UseRequireNotNull:\n    active: true\n  UselessCallOnNotNull:\n    active: true\n  UtilityClassWithPublicConstructor:\n    active: true\n  VarCouldBeVal:\n    active: true\n    ignoreLateinitVar: false\n  WildcardImport:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    excludeImports:\n      - 'java.util.*'\n"
  },
  {
    "path": "docs/Components/01-couchbase.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Couchbase</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n            testImplementation(\"com.trendyol:stove-couchbase\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you'll have access to the `couchbase` function when configuring Stove. This sets up the Couchbase Docker container that will be started for your tests.\n\nYou'll need to define a `defaultBucket` name. Make sure this matches what your application expects.\n\n!!! warning\n    Your application needs to use the same bucket names, otherwise tests will fail.\n\n```kotlin hl_lines=\"4 5-9\"\nStove()\n  .with {\n    couchbase {\n      CouchbaseSystemOptions(defaultBucket = \"test-bucket\", configureExposedConfiguration = { cfg ->\n        listOf(\n          \"couchbase.hosts=${cfg.hostsWithPort}\",\n          \"couchbase.username=${cfg.username}\",\n          \"couchbase.password=${cfg.password}\"\n        )\n      })\n    }\n  }\n  .run()\n```\n\nStove exposes the configuration it generates, so you can pass the real connection strings and credentials to your application before it starts.\nYour application will start with the physical dependencies that are spun-up by the framework.\n\n## Migrations\n\nStove provides a way to run migrations before the test starts.\n\n```kotlin\nclass CouchbaseMigration : DatabaseMigration<Cluster> {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: Cluster) {\n    val bucket = connection.bucket(CollectionConstants.BUCKET_NAME)\n    listOf(CollectionConstants.PRODUCT_COLLECTION).forEach { collection ->\n      bucket.collections.createCollection(bucket.defaultScope().name, collection)\n    }\n    connection.waitUntilReady(30.seconds)\n  }\n}\n```\n\nYou can define your migration class by implementing the `DatabaseMigration` interface. You can define the order of the\nmigration by overriding the `order` property. The migrations will be executed in the order of the `order` property.\n\nAfter defining your migration class, you can pass it to the `migrations` function of the `couchbase` configuration.\n\n```kotlin\nStove()\n  .with {\n    couchbase {\n      CouchbaseSystemOptions(defaultBucket = \"test-bucket\", configureExposedConfiguration = { cfg ->\n        listOf(\n          \"couchbase.hosts=${cfg.hostsWithPort}\",\n          \"couchbase.username=${cfg.username}\",\n          \"couchbase.password=${cfg.password}\"\n        )\n      }).migrations {\n        register<CouchbaseMigration>()\n      }\n    }\n  }\n  .run()\n```\n\n## Usage\n\n### Saving Documents\n\nSave documents to Couchbase collections:\n\n```kotlin\nstove {\n  couchbase {\n    // Save to default collection (_default)\n    saveToDefaultCollection(\n      id = \"user:123\",\n      instance = User(id = \"123\", name = \"John Doe\", email = \"john@example.com\")\n    )\n\n    // Save to a specific collection\n    save(\n      collection = \"products\",\n      id = \"product:456\",\n      instance = Product(id = \"456\", name = \"Laptop\", price = 999.99)\n    )\n  }\n}\n```\n\n### Getting Documents\n\nRetrieve and validate documents:\n\n```kotlin hl_lines=\"4 11\"\nstove {\n  couchbase {\n    // Get from default collection\n    shouldGet<User>(\"user:123\") { user ->\n      user.id shouldBe \"123\"\n      user.name shouldBe \"John Doe\"\n      user.email shouldBe \"john@example.com\"\n    }\n\n    // Get from specific collection\n    shouldGet<Product>(\"products\", \"product:456\") { product ->\n      product.id shouldBe \"456\"\n      product.name shouldBe \"Laptop\"\n      product.price shouldBe 999.99\n    }\n  }\n}\n```\n\n### Checking Non-Existence\n\nVerify that documents don't exist:\n\n```kotlin\nstove {\n  couchbase {\n    // Check default collection\n    shouldNotExist(\"user:999\")\n\n    // Check specific collection\n    shouldNotExist(\"products\", \"product:999\")\n  }\n}\n```\n\n### Deleting Documents\n\nDelete documents and verify deletion:\n\n```kotlin\nstove {\n  couchbase {\n    // Delete from default collection\n    shouldDelete(\"user:123\")\n    shouldNotExist(\"user:123\")\n\n    // Delete from specific collection\n    shouldDelete(\"products\", \"product:456\")\n    shouldNotExist(\"products\", \"product:456\")\n  }\n}\n```\n\n### N1QL Queries\n\nExecute N1QL queries and validate results:\n\n```kotlin hl_lines=\"4 11\"\nstove {\n  couchbase {\n    // Simple query\n    shouldQuery<User>(\"SELECT u.* FROM `users` u WHERE u.age > 18\") { users ->\n      users.size shouldBeGreaterThan 0\n      users.all { it.age > 18 } shouldBe true\n    }\n\n    // Query with multiple conditions\n    shouldQuery<Product>(\n      \"\"\"\n      SELECT p.* \n      FROM `products` p \n      WHERE p.price > 100 AND p.category = 'Electronics'\n      \"\"\".trimIndent()\n    ) { products ->\n      products.size shouldBeGreaterThan 0\n      products.all { it.price > 100 && it.category == \"Electronics\" } shouldBe true\n    }\n  }\n}\n```\n\n### Working with Collections and Scopes\n\nAccess bucket, collection, and cluster directly:\n\n```kotlin\nstove {\n  couchbase {\n    // Access the cluster\n    val cluster = cluster()\n    \n    // Access the bucket\n    val bucket = bucket()\n    \n    // Perform custom operations\n    val customResult = bucket.collections.getAllScopes()\n    customResult shouldNotBe null\n  }\n}\n```\n\n### Pause and Unpause Container\n\nControl the Couchbase container for testing failure scenarios:\n\n```kotlin\nstove {\n  couchbase {\n    // Pause the container\n    pause()\n    \n    // Your application should handle the failure\n    // ...\n    \n    // Unpause the container\n    unpause()\n    \n    // Verify recovery\n    shouldGet<User>(\"user:123\") { user ->\n      user.id shouldBe \"123\"\n    }\n  }\n}\n```\n\n## Complete Example\n\nHere's a complete <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">end-to-end test combining HTTP, Couchbase, and Kafka</span>:\n\n```kotlin hl_lines=\"10 19 32 42\"\ntest(\"should create product and store in couchbase\") {\n  stove {\n    val productId = UUID.randomUUID().toString()\n    val productName = \"Gaming Laptop\"\n    val categoryId = 1\n\n    // Mock external service\n    wiremock {\n      mockGet(\n        url = \"/categories/$categoryId\",\n        statusCode = 200,\n        responseBody = Category(id = categoryId, name = \"Electronics\", active = true).some()\n      )\n    }\n\n    // Create product via API\n    http {\n      postAndExpectBody<Any>(\n        uri = \"/products\",\n        body = ProductCreateRequest(\n          name = productName,\n          price = 1299.99,\n          categoryId = categoryId\n        ).some()\n      ) { response ->\n        response.status shouldBe 200\n      }\n    }\n\n    // Verify stored in Couchbase\n    couchbase {\n      shouldGet<Product>(\"products\", \"product:$productId\") { product ->\n        product.id shouldBe productId\n        product.name shouldBe productName\n        product.price shouldBe 1299.99\n        product.categoryId shouldBe categoryId\n      }\n    }\n\n    // Verify event was published\n    kafka {\n      shouldBePublished<ProductCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.id == productId &&\n        actual.name == productName &&\n        actual.price == 1299.99\n      }\n    }\n\n    // Query products by category\n    couchbase {\n      shouldQuery<Product>(\n        \"\"\"\n        SELECT p.* \n        FROM `products` p \n        WHERE p.categoryId = $categoryId\n        \"\"\".trimIndent()\n      ) { products ->\n        products.size shouldBeGreaterThan 0\n        products.any { it.id == productId } shouldBe true\n      }\n    }\n  }\n}\n```\n\n## Integration with Application\n\nVerify application behavior using the bridge:\n\n```kotlin\ntest(\"should use repository to save product\") {\n  stove {\n    val productId = UUID.randomUUID().toString()\n    val product = Product(id = productId, name = \"Test Product\", price = 99.99)\n\n    // Use application's repository\n    using<ProductRepository> {\n      save(product)\n    }\n\n    // Verify in Couchbase\n    couchbase {\n      shouldGet<Product>(\"products\", \"product:$productId\") { savedProduct ->\n        savedProduct.id shouldBe productId\n        savedProduct.name shouldBe \"Test Product\"\n        savedProduct.price shouldBe 99.99\n      }\n    }\n  }\n}\n```\n\n## Advanced Operations\n\n### Batch Operations\n\n```kotlin\nstove {\n  couchbase {\n    // Save multiple documents\n    val users = listOf(\n      User(id = \"1\", name = \"Alice\"),\n      User(id = \"2\", name = \"Bob\"),\n      User(id = \"3\", name = \"Charlie\")\n    )\n    \n    users.forEach { user ->\n      saveToDefaultCollection(\"user:${user.id}\", user)\n    }\n    \n    // Query all\n    shouldQuery<User>(\"SELECT u.* FROM `${bucket().name}` u\") { result ->\n      result.size shouldBeGreaterThanOrEqual users.size\n    }\n    \n    // Verify each\n    users.forEach { user ->\n      shouldGet<User>(\"user:${user.id}\") { actual ->\n        actual.name shouldBe user.name\n      }\n    }\n  }\n}\n```\n\n### Error Handling\n\n```kotlin\nstove {\n  couchbase {\n    // Document not found\n    shouldNotExist(\"non-existent:key\")\n    \n    // Attempting to delete non-existent document throws exception\n    assertThrows<DocumentNotFoundException> {\n      shouldDelete(\"non-existent:key\")\n    }\n    \n    // Attempting to assert non-existence on existing document throws assertion error\n    saveToDefaultCollection(\"user:123\", User(id = \"123\", name = \"John\"))\n    assertThrows<AssertionError> {\n      shouldNotExist(\"user:123\")\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/Components/02-kafka.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Kafka</span>\n\nStove supports Kafka in two ways: <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">standalone Kafka or Kafka with Spring integration</span>. You can use either one, but not both in the same project.\n\n## Standalone Kafka\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(\"com.trendyol:stove-kafka:$version\")\n        }\n    ```\n\n### Configure\n\n```kotlin hl_lines=\"6-7 10-11\"\nStove()\n  .with {\n    // other dependencies\n\n    kafka {\n      stoveKafkaObjectMapperRef = objectMapperRef\n      KafkaSystemOptions {\n        listOf(\n          \"kafka.bootstrapServers=${it.bootstrapServers}\",\n          \"kafka.interceptorClasses=${it.interceptorClass}\"\n        )\n      }\n    }\n  }.run()\n```\n\nThe configuration values are:\n\n```kotlin\nclass KafkaSystemOptions(\n  /**\n   * Suffixes for error and retry topics in the application.\n   */\n  val topicSuffixes: TopicSuffixes = TopicSuffixes(),\n  /**\n   * If true, the system will listen to the messages published by the Kafka system.\n   */\n  val listenPublishedMessagesFromStove: Boolean = false,\n  /**\n   * The port of the bridge gRPC server that is used to communicate with the Kafka system.\n   */\n  val bridgeGrpcServerPort: Int = stoveKafkaBridgePortDefault.toInt(),\n  /**\n   * The Serde that is used while asserting the messages,\n   * serializing while bridging the messages. Take a look at the [serde] property for more information.\n   *\n   * The default value is [StoveSerde.jackson]'s anyByteArraySerde.\n   * Depending on your application's needs you might want to change this value.\n   *\n   * The places where it was used listed below:\n   *\n   * @see [com.trendyol.stove.standalone.kafka.intercepting.StoveKafkaBridge] for bridging the messages.\n   * @see StoveKafkaValueSerializer for serializing the messages.\n   * @see StoveKafkaValueDeserializer for deserializing the messages.\n   * @see valueSerializer for serializing the messages.\n   */\n  val serde: StoveSerde<Any, ByteArray> = stoveSerdeRef,\n  /**\n   * The Value serializer that is used to serialize messages.\n   */\n  val valueSerializer: Serializer<Any> = StoveKafkaValueSerializer(),\n  /**\n   * The options for the Kafka container.\n   */\n  val containerOptions: KafkaContainerOptions = KafkaContainerOptions(),\n  /**\n   * The options for the Kafka system that is exposed to the application\n   */\n  override val configureExposedConfiguration: (KafkaExposedConfiguration) -> List<String>\n) : SystemOptions, ConfiguresExposedConfiguration<KafkaExposedConfiguration>\n```\n\n### Configuring Serializer and Deserializer\n\nLike every `SystemOptions` object, `KafkaSystemOptions` has a `serde` property that you can configure. It is a\n`StoveSerde` object that has two functions `serialize` and `deserialize`. You can configure them depending on your\napplication's needs.\n\n```kotlin\nval kafkaSystemOptions = KafkaSystemOptions(\n  serde = object : StoveSerde<Any, ByteArray> {\n    override fun serialize(value: Any): ByteArray {\n      return objectMapper.writeValueAsBytes(value)\n    }\n\n    override fun <T> deserialize(value: ByteArray): T {\n      return objectMapper.readValue(value, Any::class.java) as T\n    }\n  }\n)\n```\n\n### Kafka Bridge With Your Application\n\nStove Kafka bridge is a **MUST** to work with Kafka. Otherwise you can't assert any messages from your application.\n\nAs you can see in the example above, you need to add a support to your application to work with interceptor that Stove\nprovides.\n\n```kotlin\n \"kafka.interceptorClasses=com.trendyol.stove.standalone.kafka.intercepting.StoveKafkaBridge\"\n\n// or\n\n\"kafka.interceptorClasses={cfg.interceptorClass}\" // cfg.interceptorClass is exposed by Stove\n```\n\n!!! Important\n\n    `kafka.` prefix or `interceptorClasses` are assumptions that you can change it with your own prefix or configuration.\n\n## Spring Kafka\n\nWhen you want to use Kafka with Application Aware testing it provides more assertion capabilities. It is recommended way\nof working. Stove-Kafka does that with intercepting the messages.\n\n### How to get?\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n          testImplementation(\"com.trendyol:stove-spring-kafka:$version\")\n        }\n    ```\n\n=== \"Maven\"\n\n    ```xml\n     <dependency>\n        <groupId>com.trendyol</groupId>\n        <artifactId>stove-spring-kafka</artifactId>\n        <version>${stove-version}</version>\n     </dependency>\n    ```\n\n### Configure\n\n#### Configuration Values\n\nKafka works with some settings as default, your application might have these values as not configurable, to make the\napplication testable we need to tweak a little bit.\n\nIf you have the following configurations:\n\n- `AUTO_OFFSET_RESET_CONFIG | \"auto.offset.reset\" | should be \"earliest\"`\n- `ALLOW_AUTO_CREATE_TOPICS_CONFIG | \"allow.auto.create.topics\" | should be true`\n- `HEARTBEAT_INTERVAL_MS_CONFIG | \"heartbeat.interval.ms\" | should be 2 seconds`\n\nYou better make them configurable, so from the e2e testing context we can change them work with Stove-Kafka testing.\n\nAs an example:\n\n```kotlin\nStove()\n  .with {\n    httpClient {\n      HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n    }\n    kafka {\n      KafkaSystemOptions {\n        listOf(\n          \"kafka.bootstrapServers=${it.bootstrapServers}\",\n          \"kafka.interceptorClasses=${it.interceptorClass}\"\n        )\n      }\n    }\n    springBoot(\n      runner = { parameters ->\n        com.trendyol.exampleapp.run(parameters)\n      },\n      withParameters = listOf(\n        \"logging.level.root=error\",\n        \"logging.level.org.springframework.web=error\",\n        \"spring.profiles.active=default\",\n        \"server.http2.enabled=false\",\n        \"kafka.heartbeatInSeconds=2\",\n        \"kafka.autoCreateTopics=true\",\n        \"kafka.offset=earliest\"\n      )\n    )\n  }.run()\n```\n\nAs you can see, we pass these configuration values as parameters. Since they are configurable, the application considers\nthese values instead of application-default values.\n\n### Consumer Settings\n\nSecond thing we need to do is tweak your consumer configuration. For that we will provide Stove-Kafka interceptor to\nyour Kafka configuration.\n\nLocate to the point where you define your `ConcurrentKafkaListenerContainerFactory` or where you can set the\ninterceptor. Interceptor needs to implement `ConsumerAwareRecordInterceptor<String, String>` since\nStove-Kafka [relies on that](https://github.com/Trendyol/stove/blob/main/starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/testing/e2e/kafka/TestSystemInterceptor.kt).\n\n```kotlin\n@EnableKafka\n@Configuration\nclass KafkaConsumerConfiguration(\n  private val interceptor: ConsumerAwareRecordInterceptor<String, String>,\n) {\n\n  @Bean\n  fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    // ...\n    factory.setRecordInterceptor(interceptor)\n    return factory\n  }\n}\n```\n\n### Producer Settings\n\nMake sure that the [aforementioned](#configuration-values) values are also configurable for producer settings, too.\nStove will have access to `KafkaTemplate` and will use `setProducerListener` to arrange itself to listen produced\nmessages.\n\n### Plugging in\n\nWhen all the configuration is done, it is time to tell to application to use our `TestSystemInterceptor` and\nconfiguration values.\n\n#### TestSystemKafkaInterceptor and Bean Registration\n\nRegister the interceptor and serde using `addTestDependencies`:\n\n**Spring Boot 2.x / 3.x:**\n\n```kotlin\nimport com.trendyol.stove.addTestDependencies\n\nspringBoot(\n  runner = { parameters ->\n    runApplication<MyApp>(*parameters) {\n      addTestDependencies {\n        bean<TestSystemKafkaInterceptor<*, *>>(isPrimary = true)\n        bean { StoveSerde.jackson.anyByteArraySerde(yourObjectMapper()) }\n      }\n    }\n  },\n```\n\n**Spring Boot 4.x:**\n\n```kotlin\nimport com.trendyol.stove.addTestDependencies4x\n\nspringBoot(\n  runner = { parameters ->\n    runApplication<MyApp>(*parameters) {\n      addTestDependencies4x {\n        registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)\n        registerBean { StoveSerde.jackson.anyByteArraySerde(yourObjectMapper()) }\n      }\n    }\n  },\n```\n\n#### Configuring the SystemUnderTest and Parameters\n\n```kotlin hl_lines=\"4-8\"\nspringBoot(\n  runner = { parameters ->\n    runApplication<MyApp>(*parameters) {\n      addTestDependencies {\n        bean<TestSystemKafkaInterceptor<*, *>>(isPrimary = true)\n        bean { StoveSerde.jackson.anyByteArraySerde(yourObjectMapper()) }\n      }\n    }\n  },\n  withParameters = listOf(\n    \"logging.level.root=error\",\n    \"logging.level.org.springframework.web=error\",\n    \"spring.profiles.active=default\",\n    \"server.http2.enabled=false\",\n    \"kafka.heartbeatInSeconds=2\", // Added Parameter\n    \"kafka.autoCreateTopics=true\", // Added Parameter\n    \"kafka.offset=earliest\" // Added Parameter\n  )\n)\n```\n\nNow you're full set and have control over Kafka messages from the testing context.\n\n## Testing\n\n### Publishing Messages\n\nYou can publish messages to Kafka topics for testing:\n\n```kotlin\nstove {\n  kafka {\n    publish(\n      topic = \"product-events\",\n      message = ProductCreated(id = \"123\", name = \"T-Shirt\"),\n      key = \"product-123\".some(), // Optional\n      headers = mapOf(\"X-UserEmail\" to \"user@example.com\"), // Optional\n      partition = 0 // Optional\n    )\n  }\n}\n```\n\n### Asserting Published Messages\n\nTest that your application publishes messages correctly:\n\n```kotlin hl_lines=\"4 11\"\nstove {\n  // Trigger an action in your application\n  http {\n    postAndExpectBodilessResponse(\"/products\", body = CreateProductRequest(name = \"Laptop\").some()) { response ->\n      response.status shouldBe 200\n    }\n  }\n\n  // Verify the message was published\n  kafka {\n    shouldBePublished<ProductCreatedEvent>(atLeastIn = 10.seconds) {\n      actual.name == \"Laptop\" &&\n      actual.id != null &&\n      metadata.topic == \"product-events\" &&\n      metadata.headers[\"event-type\"] == \"PRODUCT_CREATED\"\n    }\n  }\n}\n```\n\n### Asserting Consumed Messages\n\nTest that your application consumes messages correctly:\n\n```kotlin hl_lines=\"4 12 20\"\nstove {\n  // Publish a message\n  kafka {\n    publish(\n      topic = \"order-events\",\n      message = OrderCreated(orderId = \"456\", amount = 100.0)\n    )\n  }\n\n  // Verify your application consumed and processed it\n  kafka {\n    shouldBeConsumed<OrderCreated>(atLeastIn = 20.seconds) {\n      actual.orderId == \"456\" &&\n      actual.amount == 100.0\n    }\n  }\n\n  // Verify side effects (e.g., database write)\n  couchbase {\n    shouldGet<Order>(\"order:456\") { order ->\n      order.orderId shouldBe \"456\"\n      order.status shouldBe \"CREATED\"\n    }\n  }\n}\n```\n\n### Testing Failed Messages\n\nTest that your application handles failures correctly:\n\n```kotlin\nstove {\n  kafka {\n    // Publish an invalid message\n    publish(\"user-events\", FailingEvent(id = 5L))\n\n    // Verify it failed with the expected reason\n    shouldBeFailed<FailingEvent>(atLeastIn = 10.seconds) {\n      actual.id == 5L &&\n      reason is BusinessException\n    }\n  }\n}\n```\n\n### Testing Retry Logic\n\nTest that your application retries failed messages:\n\n```kotlin\nstove {\n  kafka {\n    publish(\"product-failing\", ProductFailingCreated(productId = \"789\"))\n    \n    // Verify it was retried 3 times\n    shouldBeRetried<ProductFailingCreated>(atLeastIn = 1.minutes, times = 3) {\n      actual.productId == \"789\"\n    }\n\n    // Verify it ended up in error topic\n    shouldBePublished<ProductFailingCreated>(atLeastIn = 1.minutes) {\n      metadata.topic == \"product-failing.error\"\n    }\n  }\n}\n```\n\n### Working with Message Metadata\n\nAccess message metadata including headers, topic, partition, offset:\n\n```kotlin\nstove {\n  kafka {\n    shouldBeConsumed<OrderCreated> {\n      actual.orderId == \"123\" &&\n      metadata.topic == \"order-events\" &&\n      metadata.headers[\"correlation-id\"] != null &&\n      metadata.partition == 0\n    }\n  }\n}\n```\n\n### Peeking Messages\n\nInspect messages without consuming them:\n\n```kotlin\nstove {\n  kafka {\n    // Peek at published messages\n    peekPublishedMessages(atLeastIn = 5.seconds, topic = \"product-events\") { record ->\n      record.key == \"product-123\"\n    }\n\n    // Peek at consumed messages\n    peekConsumedMessages(atLeastIn = 5.seconds, topic = \"order-events\") { record ->\n      record.offset >= 10L\n    }\n\n    // Peek at committed messages\n    peekCommittedMessages(topic = \"order-events\") { record ->\n      record.offset == 101L // next offset after 100 messages\n    }\n  }\n}\n```\n\n### Admin Operations\n\nManage Kafka topics and configurations:\n\n```kotlin\nstove {\n  kafka {\n    adminOperations {\n      createTopic(NewTopic(\"test-topic\", 1, 1))\n      // Other admin operations available here\n    }\n  }\n}\n```\n\n### In-Flight Consumer\n\nCreate a consumer for advanced testing scenarios:\n\n```kotlin\nstove {\n  kafka {\n    consumer<String, ProductCreated>(\n      topic = \"product-events\",\n      readOnly = false, // commit messages\n      autoOffsetReset = \"earliest\",\n      autoCreateTopics = true,\n      keepConsumingAtLeastFor = 10.seconds\n    ) { record ->\n      println(\"Consumed: ${record.value()}\")\n      // Process the message\n    }\n  }\n}\n```\n\n## Complete Example\n\nHere's a complete <span data-rn=\"underline\" data-rn-color=\"#009688\">end-to-end test combining HTTP, Kafka, and database assertions</span>:\n\n```kotlin hl_lines=\"9 14 23 32 40\"\ntest(\"should create product and publish event\") {\n  stove {\n    val productId = UUID.randomUUID()\n    val productName = \"Laptop\"\n\n    // Mock external service\n    wiremock {\n      mockGet(\"/categories/electronics\", statusCode = 200, responseBody = Category(id = 1, active = true).some())\n    }\n\n    // Make HTTP request\n    http {\n      postAndExpectBodilessResponse(\n        uri = \"/products\",\n        body = ProductCreateRequest(id = productId, name = productName, categoryId = 1).some()\n      ) { response ->\n        response.status shouldBe 200\n      }\n    }\n\n    // Verify Kafka message was published\n    kafka {\n      shouldBePublished<ProductCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.id == productId &&\n        actual.name == productName &&\n        metadata.headers[\"X-UserEmail\"] != null\n      }\n    }\n\n    // Verify database state\n    couchbase {\n      shouldGet<Product>(\"product:$productId\") { product ->\n        product.id shouldBe productId\n        product.name shouldBe productName\n      }\n    }\n\n    // Verify the event was consumed by another service\n    kafka {\n      shouldBeConsumed<ProductCreatedEvent>(atLeastIn = 20.seconds) {\n        actual.id == productId &&\n        actual.name == productName\n      }\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/Components/03-elasticsearch.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Elasticsearch</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(\"com.trendyol:stove-elasticsearch:$version\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you'll have access to the `elasticsearch` function when configuring Stove.\nThis function configures the Elasticsearch Docker container that is going to be started.\n\n```kotlin hl_lines=\"4 5-9\"\nStove()\n  .with {\n    elasticsearch {\n      ElasticsearchSystemOptions(configureExposedConfiguration = { cfg ->\n        listOf(\n          \"elasticsearch.host=${cfg.host}\",\n          \"elasticsearch.port=${cfg.port}\",\n          \"elasticsearch.password=${cfg.password}\"\n        )\n      })\n    }\n  }\n  .run()\n```\n\n### Container Options\n\nYou can customize the Elasticsearch container:\n\n```kotlin\nStove()\n  .with {\n    elasticsearch {\n      ElasticsearchSystemOptions(\n        container = ElasticContainerOptions(\n          registry = \"docker.elastic.co/\",\n          image = \"elasticsearch/elasticsearch\",\n          tag = \"8.6.1\",\n          password = \"password\",\n          disableSecurity = true, // Disable for simpler test setup\n          exposedPorts = listOf(9200)\n        ),\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"elasticsearch.host=${cfg.host}\",\n            \"elasticsearch.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Security Configuration\n\nFor secure Elasticsearch setups with authentication:\n\n```kotlin\nStove()\n  .with {\n    elasticsearch {\n      ElasticsearchSystemOptions(\n        container = ElasticContainerOptions(\n          disableSecurity = false, // Enable security\n          password = \"your-secure-password\"\n        ),\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"elasticsearch.host=${cfg.host}\",\n            \"elasticsearch.port=${cfg.port}\",\n            \"elasticsearch.password=${cfg.password}\",\n            \"elasticsearch.ssl.enabled=true\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Client Configurer\n\nCustomize the Elasticsearch REST client:\n\n```kotlin\nStove()\n  .with {\n    elasticsearch {\n      ElasticsearchSystemOptions(\n        clientConfigurer = ElasticClientConfigurer(\n          httpClientBuilder = {\n            setDefaultRequestConfig(\n              RequestConfig.custom()\n                .setSocketTimeout(60000)\n                .setConnectTimeout(30000)\n                .build()\n            )\n          }\n        ),\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"elasticsearch.host=${cfg.host}\",\n            \"elasticsearch.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Custom JSON Mapper\n\nUse a custom Jackson ObjectMapper for serialization:\n\n```kotlin\nStove()\n  .with {\n    elasticsearch {\n      val customMapper = ObjectMapper().apply {\n        registerModule(JavaTimeModule())\n        disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n      }\n      ElasticsearchSystemOptions(\n        jsonpMapper = JacksonJsonpMapper(customMapper),\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"elasticsearch.host=${cfg.host}\",\n            \"elasticsearch.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n## Migrations\n\nStove provides a way to run index migrations before tests start:\n\n```kotlin hl_lines=\"1 4 7-10\"\nclass CreateProductIndex : DatabaseMigration<ElasticsearchClient> {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: ElasticsearchClient) {\n    connection.indices().create { c ->\n      c.index(\"products\")\n        .mappings { m ->\n          m.properties(\"name\") { p -> p.text { t -> t } }\n            .properties(\"price\") { p -> p.double_ { d -> d } }\n            .properties(\"category\") { p -> p.keyword { k -> k } }\n            .properties(\"createdAt\") { p -> p.date { d -> d } }\n        }\n    }\n  }\n}\n```\n\nRegister migrations in your Stove configuration:\n\n```kotlin\nStove()\n  .with {\n    elasticsearch {\n      ElasticsearchSystemOptions(\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"elasticsearch.host=${cfg.host}\",\n            \"elasticsearch.port=${cfg.port}\"\n          )\n        }\n      ).migrations {\n        register<CreateProductIndex>()\n      }\n    }\n  }\n  .run()\n```\n\n## Usage\n\n### Saving Documents\n\nSave documents to Elasticsearch indices:\n\n```kotlin\nstove {\n  elasticsearch {\n    // Save a document\n    save(\n      id = \"product-123\",\n      instance = Product(\n        id = \"123\",\n        name = \"Laptop\",\n        price = 999.99,\n        category = \"Electronics\"\n      ),\n      index = \"products\"\n    )\n  }\n}\n```\n\n### Getting Documents\n\nRetrieve and validate documents:\n\n```kotlin\nstove {\n  elasticsearch {\n    // Get by ID and validate\n    shouldGet<Product>(index = \"products\", key = \"product-123\") { product ->\n      product.id shouldBe \"123\"\n      product.name shouldBe \"Laptop\"\n      product.price shouldBe 999.99\n      product.category shouldBe \"Electronics\"\n    }\n  }\n}\n```\n\n### Checking Non-Existence\n\nVerify that documents don't exist:\n\n```kotlin\nstove {\n  elasticsearch {\n    // Verify document doesn't exist\n    shouldNotExist(key = \"product-999\", index = \"products\")\n  }\n}\n```\n\n### Deleting Documents\n\nDelete documents and verify deletion:\n\n```kotlin\nstove {\n  elasticsearch {\n    // Delete a document\n    shouldDelete(key = \"product-123\", index = \"products\")\n    \n    // Verify deletion\n    shouldNotExist(key = \"product-123\", index = \"products\")\n  }\n}\n```\n\n### Querying with JSON Query DSL\n\nExecute Elasticsearch queries using JSON DSL:\n\n```kotlin hl_lines=\"4 17 18\"\nstove {\n  elasticsearch {\n    // Query using JSON DSL\n    shouldQuery<Product>(\n      query = \"\"\"\n        {\n          \"bool\": {\n            \"must\": [\n              { \"match\": { \"category\": \"Electronics\" } },\n              { \"range\": { \"price\": { \"gte\": 500 } } }\n            ]\n          }\n        }\n      \"\"\".trimIndent(),\n      index = \"products\"\n    ) { products ->\n      products.size shouldBeGreaterThan 0\n      products.all { it.category == \"Electronics\" && it.price >= 500 } shouldBe true\n    }\n  }\n}\n```\n\n### Querying with Query Builder\n\nUse the Elasticsearch Java client's query builder:\n\n```kotlin\nstove {\n  elasticsearch {\n    // Query using Query builder\n    val query = Query.of { q ->\n      q.bool { b ->\n        b.must { m ->\n          m.match { t -> t.field(\"category\").query(\"Electronics\") }\n        }.filter { f ->\n          f.range { r -> r.field(\"price\").gte(JsonData.of(500)) }\n        }\n      }\n    }\n\n    shouldQuery<Product>(query) { products ->\n      products.size shouldBeGreaterThan 0\n      products.all { it.category == \"Electronics\" && it.price >= 500 } shouldBe true\n    }\n  }\n}\n```\n\n### Accessing the Client Directly\n\nFor advanced operations, access the Elasticsearch client:\n\n```kotlin\nstove {\n  elasticsearch {\n    val esClient = client()\n    \n    // Perform custom operations\n    val indexExists = esClient.indices().exists { e -> e.index(\"products\") }.value()\n    indexExists shouldBe true\n    \n    // Bulk operations\n    esClient.bulk { b ->\n      b.operations { op ->\n        op.index { i ->\n          i.index(\"products\")\n            .id(\"bulk-1\")\n            .document(Product(id = \"bulk-1\", name = \"Mouse\", price = 29.99, category = \"Electronics\"))\n        }\n      }.operations { op ->\n        op.index { i ->\n          i.index(\"products\")\n            .id(\"bulk-2\")\n            .document(Product(id = \"bulk-2\", name = \"Keyboard\", price = 79.99, category = \"Electronics\"))\n        }\n      }\n    }\n  }\n}\n```\n\n### Pause and Unpause Container\n\nControl the Elasticsearch container for testing failure scenarios:\n\n```kotlin\nstove {\n  elasticsearch {\n    // Elasticsearch is running\n    shouldGet<Product>(index = \"products\", key = \"product-123\") { product ->\n      product.id shouldBe \"123\"\n    }\n    \n    // Pause the container\n    pause()\n    \n    // Your application should handle the failure\n    // ...\n    \n    // Unpause the container\n    unpause()\n    \n    // Verify recovery\n    shouldGet<Product>(index = \"products\", key = \"product-123\") { product ->\n      product.id shouldBe \"123\"\n    }\n  }\n}\n```\n\n!!! warning\n    `pause()` and `unpause()` operations are not supported when using a provided instance.\n\n## Complete Example\n\nHere's a complete <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">end-to-end test combining HTTP, Elasticsearch, and Kafka</span>:\n\n```kotlin hl_lines=\"10 19 34 43\"\ntest(\"should create product and index in elasticsearch\") {\n  stove {\n    val productId = UUID.randomUUID().toString()\n    val productName = \"Gaming Laptop\"\n    val categoryId = 1\n\n    // Mock external service\n    wiremock {\n      mockGet(\n        url = \"/categories/$categoryId\",\n        statusCode = 200,\n        responseBody = Category(id = categoryId, name = \"Electronics\", active = true).some()\n      )\n    }\n\n    // Create product via API\n    http {\n      postAndExpectBody<ProductResponse>(\n        uri = \"/products\",\n        body = ProductCreateRequest(\n          name = productName,\n          price = 1299.99,\n          categoryId = categoryId\n        ).some()\n      ) { response ->\n        response.status shouldBe 201\n        response.body().id shouldNotBe null\n      }\n    }\n\n    // Verify indexed in Elasticsearch\n    elasticsearch {\n      shouldGet<Product>(index = \"products\", key = productId) { product ->\n        product.id shouldBe productId\n        product.name shouldBe productName\n        product.price shouldBe 1299.99\n      }\n    }\n\n    // Verify event was published\n    kafka {\n      shouldBePublished<ProductCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.id == productId &&\n        actual.name == productName\n      }\n    }\n\n    // Query products by category\n    elasticsearch {\n      shouldQuery<Product>(\n        query = \"\"\"\n          {\n            \"term\": { \"category\": \"Electronics\" }\n          }\n        \"\"\".trimIndent(),\n        index = \"products\"\n      ) { products ->\n        products.size shouldBeGreaterThan 0\n        products.any { it.id == productId } shouldBe true\n      }\n    }\n  }\n}\n```\n\n## Integration with Application\n\nVerify application behavior using the bridge:\n\n```kotlin\ntest(\"should use service to index product\") {\n  stove {\n    val productId = UUID.randomUUID().toString()\n    val product = Product(id = productId, name = \"Test Product\", price = 99.99, category = \"Test\")\n\n    // Use application's service\n    using<ProductIndexingService> {\n      indexProduct(product)\n    }\n\n    // Verify in Elasticsearch\n    elasticsearch {\n      shouldGet<Product>(index = \"products\", key = productId) { indexed ->\n        indexed.id shouldBe productId\n        indexed.name shouldBe \"Test Product\"\n        indexed.price shouldBe 99.99\n      }\n    }\n  }\n}\n```\n\n## Advanced Operations\n\n### Full-Text Search\n\n```kotlin\nstove {\n  elasticsearch {\n    // Setup test data\n    listOf(\n      Product(id = \"1\", name = \"MacBook Pro 16 inch\", price = 2499.99, category = \"Laptops\"),\n      Product(id = \"2\", name = \"MacBook Air M2\", price = 1199.99, category = \"Laptops\"),\n      Product(id = \"3\", name = \"Dell XPS 15\", price = 1799.99, category = \"Laptops\")\n    ).forEach { product ->\n      save(id = product.id, instance = product, index = \"products\")\n    }\n\n    // Full-text search\n    shouldQuery<Product>(\n      query = \"\"\"\n        {\n          \"multi_match\": {\n            \"query\": \"MacBook\",\n            \"fields\": [\"name\", \"description\"]\n          }\n        }\n      \"\"\".trimIndent(),\n      index = \"products\"\n    ) { results ->\n      results.size shouldBe 2\n      results.all { \"MacBook\" in it.name } shouldBe true\n    }\n  }\n}\n```\n\n### Aggregations\n\n```kotlin\nstove {\n  elasticsearch {\n    val esClient = client()\n    \n    // Search with aggregations\n    val response = esClient.search({ s ->\n      s.index(\"products\")\n        .size(0)\n        .aggregations(\"price_stats\") { a ->\n          a.stats { st -> st.field(\"price\") }\n        }\n        .aggregations(\"by_category\") { a ->\n          a.terms { t -> t.field(\"category.keyword\") }\n        }\n    }, Product::class.java)\n\n    // Access aggregation results\n    val priceStats = response.aggregations()[\"price_stats\"]?.stats()\n    priceStats?.avg() shouldNotBe null\n    priceStats?.min() shouldNotBe null\n    priceStats?.max() shouldNotBe null\n\n    val categoryBuckets = response.aggregations()[\"by_category\"]?.sterms()?.buckets()?.array()\n    categoryBuckets?.size shouldBeGreaterThan 0\n  }\n}\n```\n\n### Index Management\n\n```kotlin\nstove {\n  elasticsearch {\n    val esClient = client()\n    \n    // Create index with custom settings\n    esClient.indices().create { c ->\n      c.index(\"test-index\")\n        .settings { s ->\n          s.numberOfShards(\"1\")\n            .numberOfReplicas(\"0\")\n        }\n        .mappings { m ->\n          m.properties(\"title\") { p -> p.text { t -> t.analyzer(\"standard\") } }\n            .properties(\"tags\") { p -> p.keyword { k -> k } }\n        }\n    }\n    \n    // Check index exists\n    val exists = esClient.indices().exists { e -> e.index(\"test-index\") }.value()\n    exists shouldBe true\n    \n    // Delete index\n    esClient.indices().delete { d -> d.index(\"test-index\") }\n  }\n}\n```\n\n## Provided Instance (External Elasticsearch)\n\n<span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">For CI/CD pipelines or shared infrastructure:</span>\n\n```kotlin\nStove()\n  .with {\n    elasticsearch {\n      ElasticsearchSystemOptions.provided(\n        host = System.getenv(\"ELASTICSEARCH_HOST\") ?: \"localhost\",\n        port = System.getenv(\"ELASTICSEARCH_PORT\")?.toInt() ?: 9200,\n        password = System.getenv(\"ELASTICSEARCH_PASSWORD\") ?: \"\",\n        runMigrations = true,\n        cleanup = { esClient ->\n          // Clean up test indices after tests\n          esClient.indices().delete { d -> d.index(\"test-*\") }\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"elasticsearch.host=${cfg.host}\",\n            \"elasticsearch.port=${cfg.port}\",\n            \"elasticsearch.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n## Data Classes Example\n\n```kotlin\ndata class Product(\n  val id: String,\n  val name: String,\n  val description: String? = null,\n  val price: Double,\n  val category: String,\n  val tags: List<String> = emptyList(),\n  val createdAt: Instant = Instant.now()\n)\n\ndata class SearchResult(\n  val total: Long,\n  val products: List<Product>\n)\n```\n"
  },
  {
    "path": "docs/Components/04-wiremock.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">WireMock</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(\"com.trendyol:stove-wiremock:$version\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you'll have access to the `wiremock` function when configuring Stove.\n\nThis starts a WireMock server instance. By default, WireMock uses a **dynamic port** (port = 0), \nwhich lets the system pick an available port automatically. This <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">avoids port conflicts, especially in CI environments</span>.\n\n```kotlin\nStove()\n  .with {\n    wiremock {\n      WireMockSystemOptions(\n        // port = 0 by default (dynamic port)\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"external-api.url=${cfg.baseUrl}\"  // e.g., http://localhost:54321\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Dynamic Port Configuration (Recommended)\n\nUsing dynamic ports is the recommended approach as it:\n\n- **Avoids port conflicts** in CI/CD pipelines where multiple builds may run in parallel\n- **Eliminates \"Address already in use\" errors** that occur with hardcoded ports\n- **Automatically exposes** the actual port to your application via `configureExposedConfiguration`\n\n```kotlin hl_lines=\"11-13 20\"\nStove()\n  .with {\n    wiremock {\n      WireMockSystemOptions(\n        // Dynamic port (default)\n        configureExposedConfiguration = { cfg ->\n          // cfg.baseUrl = \"http://localhost:<dynamic-port>\"\n          // cfg.port = <dynamic-port>\n          // cfg.host = \"localhost\"\n          listOf(\n            \"payment.service.url=${cfg.baseUrl}\",\n            \"inventory.service.url=${cfg.baseUrl}\",\n            \"notification.service.url=${cfg.baseUrl}\"\n          )\n        }\n      )\n    }\n    springBoot(\n      runner = { params ->\n        com.myapp.run(params)\n      }\n      // No need to specify external service URLs here - \n      // they're automatically injected via configureExposedConfiguration\n    )\n  }\n  .run()\n```\n\n### Fixed Port Configuration\n\nIf you need a specific port (not recommended for CI), you can set it explicitly:\n\n```kotlin\nwiremock {\n  WireMockSystemOptions(\n    port = 9090  // Fixed port\n  )\n}\n```\n\n### Options\n\n```kotlin\ndata class WireMockSystemOptions(\n  /**\n   * Port of wiremock server.\n   * Defaults to 0, which lets WireMock pick an available port automatically.\n   * This avoids port conflicts, especially in CI environments.\n   */\n  val port: Int = 0,\n  /**\n   * Configures wiremock server\n   */\n  val configure: WireMockConfiguration.() -> WireMockConfiguration = { this.notifier(ConsoleNotifier(true)) },\n  /**\n   * Removes the stub when request matches/completes\n   * Default value is false\n   */\n  val removeStubAfterRequestMatched: Boolean = false,\n  /**\n   * Called after stub removed\n   */\n  val afterStubRemoved: AfterStubRemoved = { _, _ -> },\n  /**\n   * Called after request handled\n   */\n  val afterRequest: AfterRequestHandler = { _, _ -> },\n  /**\n   * ObjectMapper for serialization/deserialization\n   */\n  val serde: StoveSerde<Any, ByteArray> = StoveSerde.jackson.anyByteArraySerde(),\n  /**\n   * Configures the exposed configuration for the application under test.\n   * Use this to inject WireMock's URL into your application's configuration.\n   */\n  val configureExposedConfiguration: (WireMockExposedConfiguration) -> List<String> = { _ -> listOf() }\n) : SystemOptions\n\n/**\n * Configuration exposed by WireMock after it starts.\n */\ndata class WireMockExposedConfiguration(\n  val host: String,   // e.g., \"localhost\"\n  val port: Int       // The actual port WireMock is listening on\n) {\n  val baseUrl: String // e.g., \"http://localhost:54321\"\n}\n```\n\n## Mocking\n\nWiremock starts a mock server on `localhost`. With dynamic ports (the default), the actual port is \nautomatically determined and exposed via `configureExposedConfiguration`.\n\n!!! warning \"Critical: External Service URLs Must Match WireMock\"\n\n    **<span data-rn=\"box\" data-rn-color=\"#ef5350\">All external service URLs in your application must be configured to point to the WireMock server.</span>**\n    \n    This is one of the most common configuration mistakes. If your application's external service URLs \n    don't match WireMock's URL, your mocks won't be hit and tests will fail or timeout.\n\n### URL Configuration\n\nSay your application calls external services in production:\n\n- `http://payment-service.com/api/payments`\n- `http://inventory-service.com/api/stock`\n- `http://notification-service.com/api/notify`\n\nFor testing, **all** these base URLs must be replaced with the WireMock URL. \nUse `configureExposedConfiguration` to automatically inject the correct URL:\n\n```kotlin\nStove()\n  .with {\n    wiremock {\n      WireMockSystemOptions(\n        // port = 0 (default) - dynamic port\n        configureExposedConfiguration = { cfg ->\n          // cfg.baseUrl contains the actual WireMock URL (e.g., http://localhost:54321)\n          listOf(\n            \"payment.service.url=${cfg.baseUrl}\",\n            \"inventory.service.url=${cfg.baseUrl}\",\n            \"notification.service.url=${cfg.baseUrl}\"\n          )\n        }\n      )\n    }\n    springBoot( // or ktor\n      runner = { params ->\n        com.myapp.run(params)\n      }\n      // External service URLs are automatically configured via configureExposedConfiguration\n    )\n  }\n  .run()\n```\n\n!!! tip \"Why Dynamic Ports?\"\n\n    Using `port = 0` (the default) lets WireMock pick an available port automatically. This is especially \n    important in CI environments where:\n    \n    - Multiple test runs may execute in parallel\n    - Other services might already be using common ports like 8080, 9090\n    - You get \"Address already in use\" errors with fixed ports\n    \n    The `configureExposedConfiguration` callback receives the actual port after WireMock starts,\n    ensuring your application always connects to the correct URL.\n\n### Application Configuration Tips\n\nMake your external service URLs configurable in your application:\n\n=== \"Spring Boot (application.yml)\"\n\n    ```yaml\n    external:\n      payment-service:\n        url: ${PAYMENT_SERVICE_URL:http://payment-service.com}\n      inventory-service:\n        url: ${INVENTORY_SERVICE_URL:http://inventory-service.com}\n    ```\n\n=== \"Ktor\"\n\n    ```kotlin\n    val paymentUrl = environment.config.propertyOrNull(\"payment.service.url\")\n        ?.getString() ?: \"http://payment-service.com\"\n    ```\n\nThen in your tests, Stove passes the WireMock URL through parameters, overriding the defaults.\n\nAll service endpoints will be pointing to the WireMock server. You can now define the stubs for the services that your\napplication calls.\n\n## Usage\n\n### GET Requests\n\nMock GET requests with various configurations:\n\n```kotlin hl_lines=\"4-5 14-15 26-27\"\nstove {\n  wiremock {\n    // Simple GET mock\n    mockGet(\n      url = \"/api/products\",\n      statusCode = 200,\n      responseBody = listOf(\n        Product(\"1\", \"Laptop\", 999.99),\n        Product(\"2\", \"Mouse\", 29.99)\n      ).some()\n    )\n\n    // GET with custom headers\n    mockGet(\n      url = \"/api/user/profile\",\n      statusCode = 200,\n      responseBody = UserProfile(id = \"123\", name = \"John\").some(),\n      responseHeaders = mapOf(\n        \"Content-Type\" to \"application/json\",\n        \"X-Rate-Limit\" to \"100\"\n      )\n    )\n\n    // GET returning error\n    mockGet(\n      url = \"/api/products/999\",\n      statusCode = 404,\n      responseBody = ErrorResponse(\"Product not found\").some()\n    )\n  }\n}\n```\n\n### POST Requests\n\nMock POST requests with request/response bodies:\n\n```kotlin\nstove {\n  wiremock {\n    // POST with request and response body\n    mockPost(\n      url = \"/api/orders\",\n      statusCode = 201,\n      requestBody = CreateOrderRequest(items = listOf(\"item1\", \"item2\")).some(),\n      responseBody = OrderResponse(orderId = \"order-123\", status = \"CREATED\").some()\n    )\n\n    // POST with metadata matching\n    mockPost(\n      url = \"/api/users\",\n      statusCode = 201,\n      requestBody = CreateUserRequest(name = \"John\").some(),\n      responseBody = UserResponse(id = \"user-123\", name = \"John\").some(),\n      metadata = mapOf(\"Content-Type\" to \"application/json\")\n    )\n\n    // POST returning error\n    mockPost(\n      url = \"/api/orders\",\n      statusCode = 400,\n      requestBody = InvalidOrderRequest().some(),\n      responseBody = ValidationError(\"Invalid order data\").some()\n    )\n  }\n}\n```\n\n### PUT Requests\n\nMock PUT requests for updates:\n\n```kotlin\nstove {\n  wiremock {\n    // PUT with full update\n    mockPut(\n      url = \"/api/products/123\",\n      statusCode = 200,\n      requestBody = UpdateProductRequest(name = \"Updated Product\", price = 899.99).some(),\n      responseBody = Product(\"123\", \"Updated Product\", 899.99).some()\n    )\n\n    // PUT with no response body\n    mockPut(\n      url = \"/api/settings/update\",\n      statusCode = 204,\n      requestBody = UpdateSettingsRequest(theme = \"dark\").some()\n    )\n  }\n}\n```\n\n### PATCH Requests\n\nMock PATCH requests for partial updates:\n\n```kotlin\nstove {\n  wiremock {\n    // PATCH for partial update\n    mockPatch(\n      url = \"/api/users/123\",\n      statusCode = 200,\n      requestBody = mapOf(\"email\" to \"newemail@example.com\").some(),\n      responseBody = UserResponse(id = \"123\", email = \"newemail@example.com\").some()\n    )\n  }\n}\n```\n\n### DELETE Requests\n\nMock DELETE requests:\n\n```kotlin\nstove {\n  wiremock {\n    // DELETE returning success\n    mockDelete(\n      url = \"/api/products/123\",\n      statusCode = 204\n    )\n\n    // DELETE with metadata\n    mockDelete(\n      url = \"/api/users/456\",\n      statusCode = 200,\n      metadata = mapOf(\"Authorization\" to \"Bearer token123\")\n    )\n  }\n}\n```\n\n### HEAD Requests\n\nMock HEAD requests:\n\n```kotlin\nstove {\n  wiremock {\n    mockHead(\n      url = \"/api/products/exists/123\",\n      statusCode = 200\n    )\n\n    mockHead(\n      url = \"/api/products/exists/999\",\n      statusCode = 404\n    )\n  }\n}\n```\n\n### Advanced Configuration\n\nFor complex scenarios, use the configure methods:\n\n```kotlin hl_lines=\"4-5 19-20\"\nstove {\n  wiremock {\n    // Advanced GET configuration\n    mockGetConfigure(\n      url = \"/api/search\",\n      urlPatternFn = { urlPathMatching(\"/api/search.*\") }\n    ) { builder, serde ->\n      builder\n        .withQueryParam(\"q\", matching(\".*laptop.*\"))\n        .willReturn(\n          aResponse()\n            .withStatus(200)\n            .withBody(serde.serialize(SearchResults(items = listOf(\"item1\", \"item2\"))))\n        )\n    }\n\n    // Advanced POST configuration\n    mockPostConfigure(\n      url = \"/api/webhooks\",\n      urlPatternFn = { urlEqualTo(it) }\n    ) { builder, serde ->\n      builder\n        .withHeader(\"X-Webhook-Secret\", equalTo(\"secret123\"))\n        .withRequestBody(containing(\"event_type\"))\n        .willReturn(\n          aResponse()\n            .withStatus(200)\n            .withBody(\"Webhook received\")\n        )\n    }\n  }\n}\n```\n\n### Partial Body Matching\n\nWhen you only need to match specific fields in a request body without specifying the entire payload, \nuse the `*Containing` methods. This is useful when:\n\n- The request body has fields you don't control (timestamps, generated IDs)\n- You only care about matching certain business-critical fields\n- The request body structure is complex but you need to match a single unique identifier\n\n#### Basic Partial Matching\n\nMatch requests containing specific fields:\n\n```kotlin\nstove {\n  wiremock {\n    // Only matches requests where productId = 123, ignores other fields\n    mockPostContaining(\n      url = \"/api/orders\",\n      requestContaining = mapOf(\"productId\" to 123),\n      statusCode = 201,\n      responseBody = OrderResponse(orderId = \"order-123\").some()\n    )\n  }\n}\n\n// This request WILL match (extra fields are ignored):\n// POST /api/orders\n// {\"productId\": 123, \"quantity\": 5, \"userId\": \"user-456\", \"timestamp\": \"2024-01-01T00:00:00Z\"}\n```\n\n#### Multiple Field Matching (AND Logic)\n\nWhen you specify multiple fields, they are matched using **AND** logic - **all** specified fields \nmust be present and match for the stub to be triggered:\n\n```kotlin\nstove {\n  wiremock {\n    // ALL three fields must match for this stub to respond\n    mockPostContaining(\n      url = \"/api/payments\",\n      requestContaining = mapOf(\n        \"orderId\" to \"order-123\",      // AND\n        \"amount\" to 99.99,              // AND\n        \"currency\" to \"USD\"\n      ),\n      statusCode = 200,\n      responseBody = PaymentResponse(transactionId = \"txn-789\").some()\n    )\n  }\n}\n\n// ✅ MATCHES - all three fields present and correct:\n// {\"orderId\": \"order-123\", \"amount\": 99.99, \"currency\": \"USD\", \"extra\": \"ignored\"}\n\n// ❌ DOES NOT MATCH - missing \"currency\":\n// {\"orderId\": \"order-123\", \"amount\": 99.99}\n\n// ❌ DOES NOT MATCH - wrong value for \"amount\":\n// {\"orderId\": \"order-123\", \"amount\": 50.00, \"currency\": \"USD\"}\n```\n\n#### Deep Nested Matching with Dot Notation\n\nMatch specific fields deep within nested JSON structures using dot notation:\n\n```kotlin\nstove {\n  wiremock {\n    // Match a single field deep in the JSON structure\n    mockPostContaining(\n      url = \"/api/orders\",\n      requestContaining = mapOf(\"order.customer.id\" to \"cust-123\"),\n      statusCode = 200,\n      responseBody = OrderConfirmation(status = \"confirmed\").some()\n    )\n  }\n}\n\n// This request WILL match:\n// POST /api/orders\n// {\n//   \"order\": {\n//     \"id\": \"order-999\",\n//     \"customer\": {\n//       \"id\": \"cust-123\",           <-- Only this field is matched\n//       \"name\": \"John Doe\",\n//       \"email\": \"john@example.com\"\n//     },\n//     \"items\": [...]\n//   },\n//   \"metadata\": {...}\n// }\n```\n\n#### Multiple Deep Nested Fields\n\nMatch multiple fields at different levels of nesting:\n\n```kotlin\nstove {\n  wiremock {\n    mockPostContaining(\n      url = \"/api/checkout\",\n      requestContaining = mapOf(\n        \"order.customer.id\" to \"cust-123\",\n        \"order.payment.method\" to \"credit_card\",\n        \"metadata.source\" to \"mobile_app\"\n      ),\n      statusCode = 200,\n      responseBody = CheckoutResponse(success = true).some()\n    )\n  }\n}\n```\n\n#### Nested Object Matching\n\nMatch nested objects with partial comparison (extra fields in nested objects are ignored):\n\n```kotlin\nstove {\n  wiremock {\n    // Match if the \"settings\" object contains at least {enabled: true}\n    mockPutContaining(\n      url = \"/api/config\",\n      requestContaining = mapOf(\n        \"settings\" to mapOf(\"enabled\" to true)\n      ),\n      statusCode = 200\n    )\n  }\n}\n\n// This request WILL match (extra fields in settings are ignored):\n// PUT /api/config\n// {\n//   \"settings\": {\n//     \"enabled\": true,      <-- Matched\n//     \"level\": 5,           <-- Ignored\n//     \"features\": [...]     <-- Ignored\n//   }\n// }\n```\n\n#### Available Partial Matching Methods\n\n| Method | HTTP Method | Description |\n|--------|-------------|-------------|\n| `mockPostContaining` | POST | Partial body matching for POST requests |\n| `mockPutContaining` | PUT | Partial body matching for PUT requests |\n| `mockPatchContaining` | PATCH | Partial body matching for PATCH requests |\n\nAll methods support:\n\n- **Simple values**: strings, numbers, booleans\n- **Dot notation**: `\"order.customer.id\"` for deep nested access\n- **Nested objects**: `mapOf(\"user\" to mapOf(\"id\" to 123))`\n- **Arrays**: `mapOf(\"tags\" to listOf(\"important\", \"urgent\"))`\n- **URL patterns**: Use `urlPatternFn` parameter for regex URL matching\n\n#### URL Pattern with Partial Matching\n\nCombine URL patterns with partial body matching:\n\n```kotlin\nstove {\n  wiremock {\n    mockPostContaining(\n      url = \"/api/v[0-9]+/orders\",\n      requestContaining = mapOf(\"orderId\" to \"order-123\"),\n      statusCode = 200,\n      urlPatternFn = { urlPathMatching(it) }  // Enable regex URL matching\n    )\n  }\n}\n\n// Matches: POST /api/v1/orders, POST /api/v2/orders, etc.\n```\n\n### Behavioral Mocking\n\nSimulate service behavior changes over multiple calls:\n\n```kotlin\ntest(\"service recovers from failure\") {\n  stove {\n    wiremock {\n      behaviourFor(\"/api/external-service\", WireMock::get) {\n        initially {\n          aResponse()\n            .withStatus(503)\n            .withBody(\"Service unavailable\")\n        }\n        then {\n          aResponse()\n            .withStatus(503)\n            .withBody(\"Still unavailable\")\n        }\n        then {\n          aResponse()\n            .withStatus(200)\n            .withHeader(\"Content-Type\", \"application/json\")\n            .withBody(it.serialize(ServiceResponse(status = \"OK\")))\n        }\n      }\n    }\n\n    http {\n      // First call - failure\n      getResponse(\"/api/external-service\") { response ->\n        response.status shouldBe 503\n      }\n\n      // Second call - still failing\n      getResponse(\"/api/external-service\") { response ->\n        response.status shouldBe 503\n      }\n\n      // Third call - success\n      get<ServiceResponse>(\"/api/external-service\") { response ->\n        response.status shouldBe \"OK\"\n      }\n    }\n  }\n}\n```\n\n### Testing Circuit Breaker\n\nTest circuit breaker patterns with WireMock:\n\n```kotlin\ntest(\"circuit breaker opens after failures\") {\n  stove {\n    wiremock {\n      // Mock service that fails\n      mockGet(\n        url = \"/api/unreliable-service\",\n        statusCode = 500,\n        responseBody = \"Internal Server Error\".some()\n      )\n    }\n\n    // Application calls the service multiple times\n    repeat(5) {\n      http {\n        getResponse(\"/api/call-external\") { response ->\n          // First few calls fail\n          response.status shouldBe 500\n        }\n      }\n    }\n\n    // Update mock to return success\n    wiremock {\n      mockGet(\n        url = \"/api/unreliable-service\",\n        statusCode = 200,\n        responseBody = ServiceResponse(status = \"OK\").some()\n      )\n    }\n\n    // Circuit breaker should open, need to wait for recovery\n    delay(5.seconds)\n\n    http {\n      get<ServiceResponse>(\"/api/call-external\") { response ->\n        response.status shouldBe \"OK\"\n      }\n    }\n  }\n}\n```\n\n## Complete Example\n\nHere's a complete test with multiple external service mocks:\n\n```kotlin\ntest(\"should create order with external service validation\") {\n  stove {\n    val userId = \"user-123\"\n    val productId = \"product-456\"\n    val categoryId = 1\n\n    // Mock user service\n    wiremock {\n      mockGet(\n        url = \"/users/$userId\",\n        statusCode = 200,\n        responseBody = User(id = userId, name = \"John Doe\", active = true).some(),\n        responseHeaders = mapOf(\"X-Service\" to \"UserService\")\n      )\n    }\n\n    // Mock product catalog service\n    wiremock {\n      mockGet(\n        url = \"/products/$productId\",\n        statusCode = 200,\n        responseBody = Product(\n          id = productId,\n          name = \"Laptop\",\n          price = 999.99,\n          stock = 10\n        ).some()\n      )\n    }\n\n    // Mock category service\n    wiremock {\n      mockGet(\n        url = \"/categories/$categoryId\",\n        statusCode = 200,\n        responseBody = Category(id = categoryId, name = \"Electronics\", active = true).some()\n      )\n    }\n\n    // Mock inventory service (POST to reserve stock)\n    wiremock {\n      mockPost(\n        url = \"/inventory/reserve\",\n        statusCode = 200,\n        requestBody = ReserveStockRequest(productId = productId, quantity = 1).some(),\n        responseBody = ReservationResponse(reservationId = \"res-789\", success = true).some()\n      )\n    }\n\n    // Create order via your API\n    http {\n      postAndExpectBody<OrderResponse>(\n        uri = \"/orders\",\n        body = CreateOrderRequest(\n          userId = userId,\n          productId = productId,\n          quantity = 1\n        ).some()\n      ) { response ->\n        response.status shouldBe 201\n        response.body().orderId shouldNotBe null\n        response.body().status shouldBe \"CREATED\"\n      }\n    }\n\n    // Verify order was stored\n    postgresql {\n      shouldQuery<Order>(\n        \"SELECT * FROM orders WHERE user_id = ?\",\n        mapper = { row ->\n          Order(\n            id = row.long(\"id\"),\n            userId = row.string(\"user_id\"),\n            productId = row.string(\"product_id\"),\n            quantity = row.int(\"quantity\")\n          )\n        }\n      ) { orders ->\n        orders.size shouldBe 1\n        orders.first().userId shouldBe userId\n        orders.first().productId shouldBe productId\n      }\n    }\n\n    // Verify event was published\n    kafka {\n      shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.userId == userId &&\n        actual.productId == productId\n      }\n    }\n  }\n}\n```\n\n## Error Scenarios\n\nTest how your application handles external service failures:\n\n```kotlin\ntest(\"should handle external service unavailability\") {\n  stove {\n    // Mock external service returning 503\n    wiremock {\n      mockGet(\n        url = \"/external-api/data\",\n        statusCode = 503,\n        responseBody = ErrorResponse(\"Service temporarily unavailable\").some()\n      )\n    }\n\n    // Your application should handle this gracefully\n    http {\n      getResponse(\"/api/fetch-data\") { response ->\n        response.status shouldBe 503 // or your fallback status\n      }\n    }\n  }\n}\n\ntest(\"should handle timeout\") {\n  stove {\n    wiremock {\n      mockGetConfigure(\"/slow-endpoint\") { builder, _ ->\n        builder.willReturn(\n          aResponse()\n            .withStatus(200)\n            .withBody(\"Response\")\n            .withFixedDelay(5000) // 5 second delay\n        )\n      }\n    }\n\n    http {\n      getResponse(\"/api/call-slow-service\") { response ->\n        // Your application should timeout and handle it\n        response.status shouldBe 504 // Gateway timeout\n      }\n    }\n  }\n}\n```\n\n## Integration Testing\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">Test complex integrations with multiple services:</span>\n\n```kotlin\ntest(\"should orchestrate multiple services\") {\n  stove {\n    val userId = \"user-123\"\n\n    // Mock authentication service\n    wiremock {\n      mockPost(\n        url = \"/auth/validate\",\n        statusCode = 200,\n        requestBody = TokenRequest(token = \"jwt-token\").some(),\n        responseBody = TokenValidation(valid = true, userId = userId).some()\n      )\n    }\n\n    // Mock permissions service\n    wiremock {\n      mockGet(\n        url = \"/permissions/$userId\",\n        statusCode = 200,\n        responseBody = Permissions(\n          userId = userId,\n          roles = listOf(\"USER\", \"ADMIN\")\n        ).some()\n      )\n    }\n\n    // Make authenticated request\n    http {\n      get<SecureData>(\n        uri = \"/api/secure-data\",\n        token = \"jwt-token\".some()\n      ) { data ->\n        data.accessible shouldBe true\n      }\n    }\n  }\n}\n```\n\n## Request Verification\n\nVerify that requests were made as expected:\n\n```kotlin\ntest(\"should verify request details\") {\n  stove {\n    wiremock {\n      mockPost(\n        url = \"/api/webhook\",\n        statusCode = 200,\n        metadata = mapOf(\n          \"X-Signature\" to \"expected-signature\"\n        )\n      )\n    }\n\n    // Trigger webhook\n    http {\n      postAndExpectBodilessResponse(\n        uri = \"/trigger-webhook\",\n        body = WebhookTrigger(event = \"user.created\").some()\n      ) { response ->\n        response.status shouldBe 200\n      }\n    }\n\n    // Verify the webhook was called with correct signature\n    // (WireMock will only match if headers match)\n  }\n}\n```\n"
  },
  {
    "path": "docs/Components/05-http.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">HTTP Client</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(\"com.trendyol:stove-http:$version\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you'll have access to the `httpClient` function when configuring Stove:\n\n```kotlin hl_lines=\"3 5\"\nStove()\n  .with {\n    httpClient {\n      HttpClientSystemOptions(\n        baseUrl = \"http://localhost:8080\",\n      )\n    }\n  }\n  .run()\n```\n\nThe other options that you can set are:\n```kotlin\ndata class HttpClientSystemOptions(\n  /**\n   * Base URL of the HTTP client.\n   */\n  val baseUrl: String,\n\n  /**\n   * Content converter for the HTTP client. Default is JacksonConverter. You can use GsonConverter or any other converter.\n   * If you want to use your own converter, you can implement ContentConverter interface.\n   */\n  val contentConverter: ContentConverter = JacksonConverter(StoveSerde.jackson.default),\n\n  /**\n   * Timeout for the HTTP client. Default is 30 seconds.\n   */\n  val timeout: Duration = 30.seconds,\n\n  /**\n   * Create client function for the HTTP client. Default is jsonHttpClient.\n   */\n  val createClient: () -> io.ktor.client.HttpClient = { jsonHttpClient(timeout, contentConverter) }\n)\n```\n\n## Usage\n\n### GET Requests\n\nMaking GET requests with various options:\n\n```kotlin hl_lines=\"4 10 20 25\"\nstove {\n  http {\n    // Simple GET request with type-safe response\n    get<UserResponse>(\"/users/123\") { user ->\n      user.id shouldBe 123\n      user.name shouldBe \"John Doe\"\n    }\n\n    // GET with query parameters\n    get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"search-term\")) { response ->\n      response shouldContain \"search-term\"\n    }\n\n    // GET with headers\n    get<UserProfile>(\"/profile\", headers = mapOf(\"X-Custom-Header\" to \"value\")) { profile ->\n      profile.email shouldNotBe null\n    }\n\n    // GET with authentication token\n    get<SecureData>(\"/secure-endpoint\", token = \"jwt-token\".some()) { data ->\n      data.isAuthorized shouldBe true\n    }\n\n    // GET multiple items (list response)\n    getMany<ProductResponse>(\"/products\", queryParams = mapOf(\"page\" to \"1\", \"size\" to \"10\")) { products ->\n      products.size shouldBe 10\n      products.first().name shouldNotBe null\n    }\n  }\n}\n```\n\n### GET with Full Response Access\n\nWhen you need access to status code and headers:\n\n```kotlin\nstove {\n  http {\n    getResponse<UserResponse>(\"/users/123\") { response ->\n      response.status shouldBe 200\n      response.headers[\"Content-Type\"] shouldContain \"application/json\"\n      response.body().id shouldBe 123\n    }\n\n    // Bodiless response (only status and headers)\n    getResponse(\"/health\") { response ->\n      response.status shouldBe 200\n    }\n  }\n}\n```\n\n### POST Requests\n\nVarious POST request patterns:\n\n```kotlin\nstove {\n  http {\n    // POST with request body and expect JSON response\n    postAndExpectJson<UserResponse>(\"/users\") {\n      CreateUserRequest(name = \"John\", email = \"john@example.com\")\n    } { user ->\n      user.id shouldNotBe null\n      user.name shouldBe \"John\"\n    }\n\n    // POST and expect bodiless response (only status)\n    postAndExpectBodilessResponse(\n      uri = \"/products/activate\",\n      body = ActivateRequest(productId = 123).some()\n    ) { response ->\n      response.status shouldBe 200\n    }\n\n    // POST with full response access\n    postAndExpectBody<ProductResponse>(\n      uri = \"/products\",\n      body = CreateProductRequest(name = \"Laptop\", price = 999.99).some()\n    ) { response ->\n      response.status shouldBe 201\n      response.headers[\"Location\"] shouldNotBe null\n      response.body().id shouldNotBe null\n    }\n\n    // POST with headers and token\n    postAndExpectJson<OrderResponse>(\n      uri = \"/orders\",\n      body = CreateOrderRequest(items = listOf(\"item1\", \"item2\")).some(),\n      headers = mapOf(\"X-Request-ID\" to \"12345\"),\n      token = \"jwt-token\".some()\n    ) { order ->\n      order.id shouldNotBe null\n      order.status shouldBe \"CREATED\"\n    }\n  }\n}\n```\n\n### PUT Requests\n\nUpdate operations with PUT:\n\n```kotlin\nstove {\n  http {\n    // PUT with response body\n    putAndExpectJson<UserResponse>(\"/users/123\") {\n      UpdateUserRequest(name = \"Jane Doe\", email = \"jane@example.com\")\n    } { user ->\n      user.name shouldBe \"Jane Doe\"\n      user.email shouldBe \"jane@example.com\"\n    }\n\n    // PUT without response body\n    putAndExpectBodilessResponse(\n      uri = \"/products/123\",\n      body = UpdateProductRequest(name = \"Updated Product\").some()\n    ) { response ->\n      response.status shouldBe 200\n    }\n\n    // PUT with full response access\n    putAndExpectBody<ProductResponse>(\n      uri = \"/products/456\",\n      body = UpdateProductRequest(price = 899.99).some()\n    ) { response ->\n      response.status shouldBe 200\n      response.body().price shouldBe 899.99\n    }\n  }\n}\n```\n\n### PATCH Requests\n\nPartial updates with PATCH:\n\n```kotlin\nstove {\n  http {\n    // PATCH with response body\n    patchAndExpectBody<UserResponse>(\n      uri = \"/users/123\",\n      body = mapOf(\"email\" to \"newemail@example.com\").some()\n    ) { response ->\n      response.status shouldBe 200\n      response.body().email shouldBe \"newemail@example.com\"\n    }\n  }\n}\n```\n\n### DELETE Requests\n\nDelete operations:\n\n```kotlin\nstove {\n  http {\n    // DELETE without response body\n    deleteAndExpectBodilessResponse(\"/users/123\") { response ->\n      response.status shouldBe 204\n    }\n\n    // DELETE with authentication\n    deleteAndExpectBodilessResponse(\n      uri = \"/products/456\",\n      token = \"jwt-token\".some()\n    ) { response ->\n      response.status shouldBe 200\n    }\n  }\n}\n```\n\n### File Upload with Multipart\n\nUpload files using multipart form data:\n\n```kotlin\nstove {\n  http {\n    postMultipartAndExpectResponse<UploadResponse>(\n      uri = \"/products/import\",\n      body = listOf(\n        StoveMultiPartContent.Text(\"productName\", \"Laptop\"),\n        StoveMultiPartContent.Text(\"description\", \"A powerful laptop\"),\n        StoveMultiPartContent.File(\n          param = \"file\",\n          fileName = \"products.csv\",\n          content = csvBytes,\n          contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE\n        )\n      )\n    ) { response ->\n      response.status shouldBe 200\n      response.body().uploadedFiles.size shouldBe 1\n      response.body().message shouldContain \"products.csv\"\n    }\n  }\n}\n```\n\n### Advanced: Using Ktor Client Directly\n\nFor advanced scenarios, access the underlying Ktor HttpClient:\n\n```kotlin\nstove {\n  http {\n    client { baseUrl ->\n      // Direct access to Ktor HttpClient\n      val response = get {\n        url(baseUrl.buildString() + \"/custom-endpoint\")\n        header(\"Custom-Header\", \"value\")\n      }\n      println(response.status)\n    }\n  }\n}\n```\n\n## Complete Example\n\nHere's a <span data-rn=\"underline\" data-rn-color=\"#009688\">complete CRUD test example</span>:\n\n```kotlin hl_lines=\"7 20 30 38\"\ntest(\"should perform CRUD operations on products\") {\n  stove {\n    var productId: Long? = null\n\n    // CREATE\n    http {\n      postAndExpectBody<ProductResponse>(\n        uri = \"/products\",\n        body = CreateProductRequest(name = \"Laptop\", price = 999.99, categoryId = 1).some()\n      ) { response ->\n        response.status shouldBe 201\n        productId = response.body().id\n        response.body().name shouldBe \"Laptop\"\n      }\n    }\n\n    // READ\n    http {\n      get<ProductResponse>(\"/products/$productId\") { product ->\n        product.id shouldBe productId\n        product.name shouldBe \"Laptop\"\n        product.price shouldBe 999.99\n      }\n    }\n\n    // UPDATE\n    http {\n      putAndExpectJson<ProductResponse>(\"/products/$productId\") {\n        UpdateProductRequest(price = 899.99)\n      } { product ->\n        product.price shouldBe 899.99\n      }\n    }\n\n    // DELETE\n    http {\n      deleteAndExpectBodilessResponse(\"/products/$productId\") { response ->\n        response.status shouldBe 204\n      }\n    }\n\n    // Verify deletion\n    http {\n      getResponse<ErrorResponse>(\"/products/$productId\") { response ->\n        response.status shouldBe 404\n      }\n    }\n  }\n}\n```\n\n## Integration with Other Components\n\n### HTTP + Database\n\n```kotlin hl_lines=\"4 12\"\nstove {\n  // Create via API and capture user ID\n  var userId: Long = 0\n  http {\n    postAndExpectBody<UserResponse>(\"/users\", body = CreateUserRequest(name = \"John\").some()) { response ->\n      userId = response.body().id\n    }\n  }\n\n  // Verify in database\n  postgresql {\n    shouldQuery(\n      query = \"SELECT * FROM users WHERE id = $userId\",\n      mapper = { row -> User(row.long(\"id\"), row.string(\"name\")) }\n    ) { users ->\n      users.size shouldBe 1\n      users.first().name shouldBe \"John\"\n    }\n  }\n}\n```\n\n### HTTP + Kafka\n\n```kotlin\nstove {\n  // Trigger event via API\n  http {\n    postAndExpectBodilessResponse(\"/orders\", body = CreateOrderRequest(amount = 100.0).some()) { response ->\n      response.status shouldBe 201\n    }\n  }\n\n  // Verify event was published\n  kafka {\n    shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {\n      actual.amount == 100.0\n    }\n  }\n}\n```\n\n### HTTP + WireMock\n\n```kotlin\nstove {\n  // Mock external service\n  wiremock {\n    mockGet(\n      url = \"/external-api/data\",\n      statusCode = 200,\n      responseBody = ExternalData(id = 1, value = \"test\").some()\n    )\n  }\n\n  // Call your API that depends on external service\n  http {\n    get<ResponseData>(\"/data\") { response ->\n      response.value shouldBe \"test\"\n    }\n  }\n}\n```\n\n## Error Handling\n\n```kotlin\nstove {\n  http {\n    // Test validation errors\n    postAndExpectBody<ValidationErrorResponse>(\"/users\", body = InvalidUserRequest().some()) { response ->\n      response.status shouldBe 400\n      response.body().errors shouldContain \"name is required\"\n    }\n\n    // Test authentication errors\n    getResponse<ErrorResponse>(\"/secure-endpoint\") { response ->\n      response.status shouldBe 401\n    }\n\n    // Test not found\n    getResponse<ErrorResponse>(\"/users/999999\") { response ->\n      response.status shouldBe 404\n    }\n\n    // Test business logic errors\n    postAndExpectBody<ErrorResponse>(\"/products\", body = InvalidProductRequest().some()) { response ->\n      response.status shouldBe 409 // Conflict\n      response.body().message shouldContain \"already exists\"\n    }\n  }\n}\n```\n\n## WebSocket Support\n\nStove provides <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">built-in support for testing WebSocket endpoints</span>. The WebSocket functionality is integrated into the HTTP system and uses Ktor's WebSocket client under the hood.\n\n### Basic WebSocket Usage\n\nSend and receive messages through a WebSocket connection:\n\n```kotlin hl_lines=\"3 5\"\nstove {\n  http {\n    webSocket(\"/chat\") {\n      // Send a text message\n      send(\"Hello, WebSocket!\")\n      \n      // Receive a text message\n      val response = receiveText()\n      response shouldBe \"Echo: Hello, WebSocket!\"\n    }\n  }\n}\n```\n\n### Sending Messages\n\nMultiple ways to send messages:\n\n```kotlin\nstove {\n  http {\n    webSocket(\"/endpoint\") {\n      // Send text message\n      send(\"Hello\")\n      \n      // Send binary data\n      send(byteArrayOf(1, 2, 3, 4, 5))\n      \n      // Send using sealed class\n      send(StoveWebSocketMessage.Text(\"Hello via sealed class\"))\n      send(StoveWebSocketMessage.Binary(byteArrayOf(1, 2, 3)))\n    }\n  }\n}\n```\n\n### Receiving Messages\n\nVarious methods to receive messages:\n\n```kotlin\nstove {\n  http {\n    webSocket(\"/endpoint\") {\n      // Receive text\n      val text = receiveText()\n      text shouldBe \"expected message\"\n      \n      // Receive binary\n      val bytes = receiveBinary()\n      bytes shouldBe byteArrayOf(1, 2, 3)\n      \n      // Receive as sealed class (auto-detect type)\n      val message = receive()\n      when (message) {\n        is StoveWebSocketMessage.Text -> println(message.content)\n        is StoveWebSocketMessage.Binary -> println(message.content.size)\n        null -> println(\"Connection closed\")\n      }\n      \n      // Receive with timeout\n      val response = receiveTextWithTimeout(5.seconds)\n      response.isSome() shouldBe true\n      response.getOrNull() shouldBe \"expected\"\n    }\n  }\n}\n```\n\n### Collecting Multiple Messages\n\nCollect a batch of messages:\n\n```kotlin\nstove {\n  http {\n    webSocket(\"/broadcast\") {\n      // Collect 5 text messages with a 10 second timeout\n      val messages = collectTexts(count = 5, timeout = 10.seconds)\n      messages.size shouldBe 5\n      messages[0] shouldBe \"Message 1\"\n      messages[4] shouldBe \"Message 5\"\n      \n      // Collect binary messages\n      val binaryMessages = collectBinaries(count = 3, timeout = 5.seconds)\n      binaryMessages.size shouldBe 3\n    }\n  }\n}\n```\n\n### Streaming with Flow\n\nUse Kotlin Flow for streaming scenarios:\n\n```kotlin\nstove {\n  http {\n    webSocket(\"/events\") {\n      // Stream text messages\n      val messages = incomingTexts()\n        .take(10)\n        .toList()\n      \n      messages.size shouldBe 10\n      \n      // Stream binary messages\n      incomingBinaries()\n        .take(5)\n        .collect { bytes ->\n          println(\"Received ${bytes.size} bytes\")\n        }\n      \n      // Stream all message types\n      incoming()\n        .take(5)\n        .collect { message ->\n          when (message) {\n            is StoveWebSocketMessage.Text -> println(message.content)\n            is StoveWebSocketMessage.Binary -> println(message.content.size)\n          }\n        }\n    }\n  }\n}\n```\n\n### Authentication and Headers\n\nConnect with authentication or custom headers:\n\n```kotlin\nstove {\n  http {\n    // With bearer token\n    webSocket(\n      uri = \"/secure-chat\",\n      token = \"jwt-token\".some()\n    ) {\n      val response = receiveText()\n      response shouldBe \"Authenticated successfully\"\n    }\n    \n    // With custom headers\n    webSocket(\n      uri = \"/chat\",\n      headers = mapOf(\n        \"X-Custom-Header\" to \"value\",\n        \"Authorization\" to \"Bearer custom-token\"\n      )\n    ) {\n      send(\"Hello with custom headers\")\n      receiveText() shouldNotBe null\n    }\n  }\n}\n```\n\n### WebSocket Expect (Assertion Alias)\n\nUse `webSocketExpect` for assertion-focused tests:\n\n```kotlin\nstove {\n  http {\n    webSocketExpect(\"/notifications\") {\n      val messages = collectTexts(count = 3)\n      messages.size shouldBe 3\n      messages.all { it.startsWith(\"notification:\") } shouldBe true\n    }\n  }\n}\n```\n\n### Raw WebSocket Access\n\nFor advanced scenarios, access the underlying Ktor WebSocket session:\n\n```kotlin\nstove {\n  http {\n    webSocketRaw(\"/advanced\") {\n      // Direct access to Ktor's DefaultClientWebSocketSession\n      send(Frame.Text(\"raw frame\"))\n      \n      for (frame in incoming) {\n        when (frame) {\n          is Frame.Text -> println(frame.readText())\n          is Frame.Binary -> println(frame.readBytes().size)\n          is Frame.Close -> break\n          else -> {}\n        }\n      }\n    }\n  }\n}\n```\n\n### Underlying Session Access\n\nAccess the underlying session from within `StoveWebSocketSession`:\n\n```kotlin\nstove {\n  http {\n    webSocket(\"/endpoint\") {\n      // Use simplified API first\n      send(\"Hello\")\n      \n      // Then access underlying session for advanced operations\n      underlyingSession {\n        send(Frame.Text(\"Advanced operation\"))\n        val frame = incoming.receive()\n        (frame as Frame.Text).readText() shouldBe \"Response\"\n      }\n    }\n  }\n}\n```\n\n### Closing Connections\n\nGracefully close WebSocket connections:\n\n```kotlin\nstove {\n  http {\n    webSocket(\"/chat\") {\n      send(\"Hello\")\n      receiveText()\n      \n      // Close with custom reason\n      close(\"Test completed\")\n    }\n  }\n}\n```\n\n### Complete WebSocket Test Example\n\nA comprehensive example testing a chat application:\n\n```kotlin\ntest(\"should handle chat room operations\") {\n  stove {\n    http {\n      // Test echo functionality\n      webSocket(\"/chat/echo\") {\n        send(\"Hello, World!\")\n        receiveText() shouldBe \"Echo: Hello, World!\"\n        \n        send(\"Another message\")\n        receiveText() shouldBe \"Echo: Another message\"\n      }\n      \n      // Test broadcast with authentication\n      webSocket(\n        uri = \"/chat/room/123\",\n        token = \"user-jwt-token\".some()\n      ) {\n        // Verify join notification\n        val joinMessage = receiveText()\n        joinMessage shouldContain \"joined\"\n        \n        // Send a message\n        send(\"Hi everyone!\")\n        \n        // Collect broadcast responses\n        val messages = collectTexts(count = 2, timeout = 5.seconds)\n        messages.any { it.contains(\"Hi everyone!\") } shouldBe true\n      }\n      \n      // Test binary data (e.g., file sharing)\n      webSocket(\"/chat/files\") {\n        val fileData = \"Hello\".toByteArray()\n        send(fileData)\n        \n        val response = receiveBinary()\n        response shouldNotBe null\n      }\n    }\n  }\n}\n```\n\n### WebSocket + Kafka Integration\n\nTest WebSocket events that trigger Kafka messages:\n\n```kotlin\nstove {\n  http {\n    webSocket(\"/events\") {\n      send(\"\"\"{\"type\": \"order\", \"action\": \"create\", \"amount\": 100.0}\"\"\")\n      \n      val confirmation = receiveText()\n      confirmation shouldContain \"received\"\n    }\n  }\n  \n  kafka {\n    shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {\n      actual.amount == 100.0\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/Components/06-postgresql.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">PostgreSQL</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n            testImplementation(\"com.trendyol:stove-postgres\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you can configure PostgreSQL in your Stove setup:\n\n```kotlin hl_lines=\"4 6-12\"\nStove()\n  .with {\n    postgresql {\n      PostgresqlOptions(\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"postgresql.host=${cfg.host}\",\n            \"postgresql.port=${cfg.port}\",\n            \"postgresql.database=${cfg.database}\",\n            \"postgresql.username=${cfg.username}\",\n            \"postgresql.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }.run()\n```\n\nThe `it` reference gives you access to the PostgreSQL container's connection details, which you can pass to your application.\n\n## Migrations\n\nStove provides a way to run database migrations before tests start:\n\n```kotlin hl_lines=\"1-2 5-6\"\nclass InitialMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: PostgresSqlMigrationContext) {\n    connection.operations.execute(\n      \"\"\"\n      CREATE TABLE IF NOT EXISTS users (\n        id serial PRIMARY KEY,\n        name VARCHAR(100) NOT NULL,\n        email VARCHAR(100) NOT NULL UNIQUE,\n        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n      );\n      \"\"\".trimIndent()\n    )\n  }\n}\n```\n\nRegister migrations in your Stove configuration:\n\n```kotlin\nStove()\n  .with {\n    postgresql {\n      PostgresqlOptions(\n        databaseName = \"testing\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      ).migrations {\n        register<InitialMigration>()\n      }\n    }\n  }\n  .run()\n```\n\n## Usage\n\n### Executing SQL\n\nExecute DDL and DML statements:\n\n```kotlin\nstove {\n  postgresql {\n    // Create tables\n    shouldExecute(\n      \"\"\"\n      DROP TABLE IF EXISTS products;\n      CREATE TABLE IF NOT EXISTS products (\n        id serial PRIMARY KEY,\n        name VARCHAR(100) NOT NULL,\n        price DECIMAL(10, 2) NOT NULL,\n        stock INT DEFAULT 0\n      );\n      \"\"\".trimIndent()\n    )\n\n    // Insert data\n    shouldExecute(\n      \"\"\"\n      INSERT INTO products (name, price, stock) \n      VALUES ('Laptop', 999.99, 10)\n      \"\"\".trimIndent()\n    )\n\n    // Update data\n    shouldExecute(\"UPDATE products SET stock = 5 WHERE name = 'Laptop'\")\n\n    // Delete data\n    shouldExecute(\"DELETE FROM products WHERE stock = 0\")\n  }\n}\n```\n\n### Querying Data\n\nQuery data with type-safe mappers:\n\n```kotlin hl_lines=\"11 13 21\"\ndata class Product(\n  val id: Long,\n  val name: String,\n  val price: Double,\n  val stock: Int\n)\n\nstove {\n  postgresql {\n    shouldQuery<Product>(\n      query = \"SELECT * FROM products WHERE price > 500\",\n      mapper = { row ->\n        Product(\n          id = row.long(\"id\"),\n          name = row.string(\"name\"),\n          price = row.double(\"price\"),\n          stock = row.int(\"stock\")\n        )\n      }\n    ) { products ->\n      products.size shouldBeGreaterThan 0\n      products.all { it.price > 500 } shouldBe true\n    }\n  }\n}\n```\n\n### Query with Parameters\n\nUse parameterized queries for safety:\n\n```kotlin\nstove {\n  postgresql {\n    val minPrice = 100.0\n    shouldQuery<Product>(\n      query = \"SELECT * FROM products WHERE price >= ?\",\n      mapper = { row ->\n        Product(\n          id = row.long(\"id\"),\n          name = row.string(\"name\"),\n          price = row.double(\"price\"),\n          stock = row.int(\"stock\")\n        )\n      }\n    ) { products ->\n      products.all { it.price >= minPrice } shouldBe true\n    }\n  }\n}\n```\n\n### Working with Nullable Fields\n\nHandle nullable columns:\n\n```kotlin\ndata class User(\n  val id: Long,\n  val name: String,\n  val email: String?,\n  val phone: String?\n)\n\nstove {\n  postgresql {\n    shouldQuery<User>(\n      query = \"SELECT * FROM users\",\n      mapper = { row ->\n        User(\n          id = row.long(\"id\"),\n          name = row.string(\"name\"),\n          email = row.stringOrNull(\"email\"),\n          phone = row.stringOrNull(\"phone\")\n        )\n      }\n    ) { users ->\n      users.size shouldBeGreaterThan 0\n    }\n  }\n}\n```\n\n### Complex Queries\n\nExecute joins and aggregations:\n\n```kotlin\ndata class OrderSummary(\n  val userId: Long,\n  val userName: String,\n  val totalOrders: Int,\n  val totalAmount: Double\n)\n\nstove {\n  postgresql {\n    shouldQuery<OrderSummary>(\n      query = \"\"\"\n        SELECT \n          u.id as user_id,\n          u.name as user_name,\n          COUNT(o.id) as total_orders,\n          SUM(o.amount) as total_amount\n        FROM users u\n        LEFT JOIN orders o ON u.id = o.user_id\n        GROUP BY u.id, u.name\n        HAVING COUNT(o.id) > 0\n      \"\"\".trimIndent(),\n      mapper = { row ->\n        OrderSummary(\n          userId = row.long(\"user_id\"),\n          userName = row.string(\"user_name\"),\n          totalOrders = row.int(\"total_orders\"),\n          totalAmount = row.double(\"total_amount\")\n        )\n      }\n    ) { summaries ->\n      summaries.all { it.totalOrders > 0 } shouldBe true\n    }\n  }\n}\n```\n\n### Pause and Unpause Container\n\nTest failure scenarios:\n\n```kotlin\nstove {\n  postgresql {\n    // Database is running\n    shouldQuery<Product>(\n      \"SELECT COUNT(*) as count FROM products\",\n      mapper = { row -> row.int(\"count\") }\n    ) { result ->\n      result.first() shouldBeGreaterThanOrEqual 0\n    }\n\n    // Pause the database\n    pause()\n\n    // Your application should handle the failure\n    // ...\n\n    // Unpause the database\n    unpause()\n\n    // Verify recovery\n    shouldQuery<Product>(\n      \"SELECT COUNT(*) as count FROM products\",\n      mapper = { row -> row.int(\"count\") }\n    ) { result ->\n      result.first() shouldBeGreaterThanOrEqual 0\n    }\n  }\n}\n```\n\n## Complete Example\n\nHere's a <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">complete end-to-end test</span>:\n\n```kotlin hl_lines=\"7 12 24 28\"\ntest(\"should create user via API and verify in database\") {\n  stove {\n    val userName = \"John Doe\"\n    val userEmail = \"john@example.com\"\n\n    // Create user via API\n    http {\n      postAndExpectBody<UserResponse>(\n        uri = \"/users\",\n        body = CreateUserRequest(name = userName, email = userEmail).some()\n      ) { response ->\n        response.status shouldBe 201\n        response.body().name shouldBe userName\n      }\n    }\n\n    // Verify in PostgreSQL\n    postgresql {\n      shouldQuery<User>(\n        query = \"SELECT * FROM users WHERE email = ?\",\n        mapper = { row ->\n          User(\n            id = row.long(\"id\"),\n            name = row.string(\"name\"),\n            email = row.string(\"email\")\n          )\n        }\n      ) { users ->\n        users.size shouldBe 1\n        users.first().name shouldBe userName\n        users.first().email shouldBe userEmail\n      }\n    }\n\n    // Verify event was published\n    kafka {\n      shouldBePublished<UserCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.name == userName &&\n        actual.email == userEmail\n      }\n    }\n  }\n}\n```\n\n## Integration with Application\n\nUse the bridge to access application components:\n\n```kotlin\ntest(\"should use repository to save user\") {\n  stove {\n    val user = User(id = 1L, name = \"Jane Doe\", email = \"jane@example.com\")\n\n    // Use application's repository\n    using<UserRepository> {\n      save(user)\n    }\n\n    // Verify in database\n    postgresql {\n      shouldQuery<User>(\n        query = \"SELECT * FROM users WHERE id = ?\",\n        mapper = { row ->\n          User(\n            id = row.long(\"id\"),\n            name = row.string(\"name\"),\n            email = row.string(\"email\")\n          )\n        }\n      ) { users ->\n        users.size shouldBe 1\n        users.first().name shouldBe \"Jane Doe\"\n      }\n    }\n  }\n}\n```\n\n## Batch Operations\n\nExecute multiple operations:\n\n```kotlin\nstove {\n  postgresql {\n    // Create tables\n    shouldExecute(\n      \"\"\"\n      CREATE TABLE IF NOT EXISTS categories (\n        id serial PRIMARY KEY,\n        name VARCHAR(50) NOT NULL\n      );\n      CREATE TABLE IF NOT EXISTS products (\n        id serial PRIMARY KEY,\n        name VARCHAR(100) NOT NULL,\n        category_id INT REFERENCES categories(id)\n      );\n      \"\"\".trimIndent()\n    )\n\n    // Insert categories\n    listOf(\"Electronics\", \"Books\", \"Clothing\").forEach { category ->\n      shouldExecute(\"INSERT INTO categories (name) VALUES ('$category')\")\n    }\n\n    // Verify all inserted\n    shouldQuery<String>(\n      \"SELECT name FROM categories\",\n      mapper = { it.string(\"name\") }\n    ) { categories ->\n      categories.size shouldBe 3\n      categories shouldContain \"Electronics\"\n      categories shouldContain \"Books\"\n    }\n  }\n}\n```\n\n## Advanced: Direct SQL Operations\n\nAccess SQL operations directly for advanced use cases:\n\n```kotlin\nstove {\n  postgresql {\n    val ops = operations()\n    \n    // Execute with parameters\n    ops.execute(\n      \"INSERT INTO users (name, email) VALUES (?, ?)\",\n      Parameter(\"name\", \"Alice\"),\n      Parameter(\"email\", \"alice@example.com\")\n    )\n\n    // Custom select operation\n    val users = ops.select(\"SELECT * FROM users\") { row ->\n      User(\n        id = row.long(\"id\"),\n        name = row.string(\"name\"),\n        email = row.string(\"email\")\n      )\n    }\n\n    users.size shouldBeGreaterThan 0\n  }\n}\n```\n\n## Multiple Databases\n\nIn production, your application might connect to multiple PostgreSQL instances (e.g., separate databases for users, orders, analytics). With Stove, you can achieve the same behavior using a **single PostgreSQL container** by creating multiple databases through migrations.\n\n### The Pattern\n\n1. Create additional databases in migrations\n2. Expose all database configurations to your application\n3. Your application connects to each database as if they were separate instances\n\n### Implementation\n\n#### Step 1: Create a Multi-Database Migration\n\n```kotlin\nclass CreateDatabasesMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n    override val order: Int = 0  // Run first!\n\n    override suspend fun execute(connection: PostgresSqlMigrationContext) {\n        // Create additional databases\n        // Note: You're connected to the default database, create others from here\n        connection.operations.execute(\"CREATE DATABASE IF NOT EXISTS users_db\")\n        connection.operations.execute(\"CREATE DATABASE IF NOT EXISTS orders_db\")\n        connection.operations.execute(\"CREATE DATABASE IF NOT EXISTS analytics_db\")\n    }\n}\n\nclass UsersDbMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n    override val order: Int = 1\n\n    override suspend fun execute(connection: PostgresSqlMigrationContext) {\n        // This runs on the default database\n        // For users_db schema, you'll set it up via application or separate connection\n        connection.operations.execute(\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS users (\n                id SERIAL PRIMARY KEY,\n                name VARCHAR(100),\n                email VARCHAR(100)\n            )\n            \"\"\".trimIndent()\n        )\n    }\n}\n```\n\n#### Step 2: Configure Stove with Multiple Database URLs\n\n```kotlin\nStove()\n    .with {\n        postgresql {\n            PostgresqlOptions(\n                databaseName = \"main_db\",  // Default/main database\n                configureExposedConfiguration = { cfg ->\n                    // Expose multiple database URLs to the application\n                    // All databases are on the same host:port, just different DB names\n                    listOf(\n                        // Main database\n                        \"spring.datasource.url=${cfg.jdbcUrl}\",\n                        \"spring.datasource.username=${cfg.username}\",\n                        \"spring.datasource.password=${cfg.password}\",\n                        \n                        // Users database (same host, different database name)\n                        \"users.datasource.url=jdbc:postgresql://${cfg.host}:${cfg.port}/users_db\",\n                        \"users.datasource.username=${cfg.username}\",\n                        \"users.datasource.password=${cfg.password}\",\n                        \n                        // Orders database\n                        \"orders.datasource.url=jdbc:postgresql://${cfg.host}:${cfg.port}/orders_db\",\n                        \"orders.datasource.username=${cfg.username}\",\n                        \"orders.datasource.password=${cfg.password}\",\n                        \n                        // Analytics database\n                        \"analytics.datasource.url=jdbc:postgresql://${cfg.host}:${cfg.port}/analytics_db\",\n                        \"analytics.datasource.username=${cfg.username}\",\n                        \"analytics.datasource.password=${cfg.password}\"\n                    )\n                }\n            ).migrations {\n                register<CreateDatabasesMigration>()\n                register<UsersDbMigration>()\n            }\n        }\n    }\n    .run()\n```\n\n#### Step 3: Application Configuration\n\nYour application should read these separate datasource configurations:\n\n```kotlin\n// Spring Boot example with multiple DataSources\n@Configuration\nclass DataSourceConfig {\n    \n    @Bean\n    @Primary\n    @ConfigurationProperties(\"spring.datasource\")\n    fun mainDataSource(): DataSource = DataSourceBuilder.create().build()\n    \n    @Bean\n    @ConfigurationProperties(\"users.datasource\")\n    fun usersDataSource(): DataSource = DataSourceBuilder.create().build()\n    \n    @Bean\n    @ConfigurationProperties(\"orders.datasource\")\n    fun ordersDataSource(): DataSource = DataSourceBuilder.create().build()\n    \n    @Bean\n    @ConfigurationProperties(\"analytics.datasource\")\n    fun analyticsDataSource(): DataSource = DataSourceBuilder.create().build()\n}\n```\n\n### Complete Example\n\n```kotlin\nobject DatabaseNames {\n    const val USERS = \"users_db\"\n    const val ORDERS = \"orders_db\"\n    const val ANALYTICS = \"analytics_db\"\n}\n\nclass SetupDatabasesMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n    override val order: Int = 0\n\n    override suspend fun execute(connection: PostgresSqlMigrationContext) {\n        listOf(DatabaseNames.USERS, DatabaseNames.ORDERS, DatabaseNames.ANALYTICS).forEach { db ->\n            connection.operations.execute(\"CREATE DATABASE IF NOT EXISTS $db\")\n        }\n    }\n}\n\n// Test configuration\nStove()\n    .with {\n        postgresql {\n            PostgresqlOptions(\n                databaseName = \"main\",\n                configureExposedConfiguration = { cfg ->\n                    val baseUrl = \"jdbc:postgresql://${cfg.host}:${cfg.port}\"\n                    listOf(\n                        \"db.users.url=$baseUrl/${DatabaseNames.USERS}\",\n                        \"db.users.username=${cfg.username}\",\n                        \"db.users.password=${cfg.password}\",\n                        \n                        \"db.orders.url=$baseUrl/${DatabaseNames.ORDERS}\",\n                        \"db.orders.username=${cfg.username}\",\n                        \"db.orders.password=${cfg.password}\",\n                        \n                        \"db.analytics.url=$baseUrl/${DatabaseNames.ANALYTICS}\",\n                        \"db.analytics.username=${cfg.username}\",\n                        \"db.analytics.password=${cfg.password}\"\n                    )\n                }\n            ).migrations {\n                register<SetupDatabasesMigration>()\n            }\n        }\n        springBoot(\n            runner = { params -> myApp.run(params) }\n        )\n    }\n    .run()\n\n// In tests\ntest(\"should save user and create order in separate databases\") {\n    stove {\n        // Create user (goes to users_db)\n        http {\n            postAndExpectBodilessResponse(\"/users\", body = CreateUserRequest(...).some()) {\n                it.status shouldBe 201\n            }\n        }\n        \n        // Create order (goes to orders_db)\n        http {\n            postAndExpectBodilessResponse(\"/orders\", body = CreateOrderRequest(...).some()) {\n                it.status shouldBe 201\n            }\n        }\n        \n        // Verify using application repositories (each connects to its own DB)\n        using<UserRepository, OrderRepository> { userRepo, orderRepo ->\n            userRepo.count() shouldBe 1\n            orderRepo.count() shouldBe 1\n        }\n    }\n}\n```\n\n### With Provided Instances\n\nThe same pattern works with provided PostgreSQL instances:\n\n```kotlin\nStove()\n    .with {\n        postgresql {\n            PostgresqlOptions.provided(\n                jdbcUrl = \"jdbc:postgresql://shared-postgres:5432/main_db\",\n                host = \"shared-postgres\",\n                port = 5432,\n                databaseName = \"main_db\",\n                username = \"postgres\",\n                password = \"postgres\",\n                runMigrations = true,  // Creates additional databases\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"db.users.url=jdbc:postgresql://${cfg.host}:${cfg.port}/users_db\",\n                        \"db.orders.url=jdbc:postgresql://${cfg.host}:${cfg.port}/orders_db\",\n                        // ... credentials\n                    )\n                }\n            ).migrations {\n                register<SetupDatabasesMigration>()\n            }\n        }\n    }\n```\n\n!!! tip \"Production vs Test\"\n    In production, these might be completely separate PostgreSQL instances (even in different regions). In tests, <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">they're all in one container but behave identically from your application's perspective</span>.\n"
  },
  {
    "path": "docs/Components/07-mongodb.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">MongoDB</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(\"com.trendyol:stove-mongodb:$version\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you'll have access to the `mongodb` function when configuring Stove.\nThis function configures the MongoDB Docker container that is going to be started.\n\n```kotlin hl_lines=\"4 6-9\"\nStove()\n  .with {\n    mongodb {\n      MongodbSystemOptions(\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"mongodb.uri=${cfg.connectionString}\",\n            \"mongodb.host=${cfg.host}\",\n            \"mongodb.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Container Options\n\nCustomize the MongoDB container:\n\n```kotlin\nStove()\n  .with {\n    mongodb {\n      MongodbSystemOptions(\n        container = MongoContainerOptions(\n          registry = \"docker.io\",\n          image = \"mongo\",\n          tag = \"6.0\",\n          containerFn = { container ->\n            // Additional container configuration\n            container.withEnv(\"MONGO_INITDB_DATABASE\", \"testdb\")\n          }\n        ),\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"mongodb.uri=${cfg.connectionString}\",\n            \"mongodb.host=${cfg.host}\",\n            \"mongodb.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Database Options\n\nConfigure the default database and collection:\n\n```kotlin\nStove()\n  .with {\n    mongodb {\n      MongodbSystemOptions(\n        databaseOptions = DatabaseOptions(\n          default = DatabaseOptions.DefaultDatabase(\n            name = \"myDatabase\",\n            collection = \"myCollection\"\n          )\n        ),\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"mongodb.uri=${cfg.connectionString}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Custom Client Configuration\n\nCustomize the MongoDB client settings:\n\n```kotlin\nStove()\n  .with {\n    mongodb {\n      MongodbSystemOptions(\n        configureClient = { settings ->\n          settings.applyToConnectionPoolSettings { pool ->\n            pool.maxSize(10)\n            pool.minSize(1)\n          }\n          settings.applyToSocketSettings { socket ->\n            socket.connectTimeout(10, TimeUnit.SECONDS)\n            socket.readTimeout(30, TimeUnit.SECONDS)\n          }\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\"mongodb.uri=${cfg.connectionString}\")\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Custom Serialization\n\nConfigure custom serialization for your documents:\n\n```kotlin\nStove()\n  .with {\n    mongodb {\n      val customSerde = StoveSerde.jackson.anyJsonStringSerde(\n        StoveSerde.jackson.byConfiguring {\n          disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n          enable(MapperFeature.DEFAULT_VIEW_INCLUSION)\n          registerModule(JavaTimeModule())\n          registerModule(KotlinModule.Builder().build())\n        }\n      )\n      \n      MongodbSystemOptions(\n        serde = customSerde,\n        configureExposedConfiguration = { cfg ->\n          listOf(\"mongodb.uri=${cfg.connectionString}\")\n        }\n      )\n    }\n  }\n  .run()\n```\n\n## Migrations\n\nStove provides a way to run migrations before tests start:\n\n```kotlin\nclass CreateIndexesMigration : DatabaseMigration<MongodbMigrationContext> {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: MongodbMigrationContext) {\n    val db = connection.client.getDatabase(connection.options.databaseOptions.default.name)\n    \n    // Create indexes\n    db.getCollection<Document>(\"users\").createIndex(\n      Indexes.ascending(\"email\"),\n      IndexOptions().unique(true)\n    )\n    \n    db.getCollection<Document>(\"products\").createIndex(\n      Indexes.compoundIndex(\n        Indexes.ascending(\"category\"),\n        Indexes.descending(\"createdAt\")\n      )\n    )\n  }\n}\n```\n\nRegister migrations in your Stove configuration:\n\n```kotlin\nStove()\n  .with {\n    mongodb {\n      MongodbSystemOptions(\n        configureExposedConfiguration = { cfg ->\n          listOf(\"mongodb.uri=${cfg.connectionString}\")\n        }\n      ).migrations {\n        register<CreateIndexesMigration>()\n      }\n    }\n  }\n  .run()\n```\n\n## Usage\n\n### Saving Documents\n\nSave documents to MongoDB collections:\n\n```kotlin\ndata class User(\n  val id: String,\n  val name: String,\n  val email: String,\n  val age: Int\n)\n\nstove {\n  mongodb {\n    val userId = ObjectId().toHexString()\n    \n    // Save to default collection\n    save(\n      instance = User(id = userId, name = \"John Doe\", email = \"john@example.com\", age = 30),\n      objectId = userId\n    )\n    \n    // Save to specific collection\n    save(\n      instance = User(id = userId, name = \"Jane Doe\", email = \"jane@example.com\", age = 28),\n      objectId = userId,\n      collection = \"users\"\n    )\n  }\n}\n```\n\n### Getting Documents\n\nRetrieve and validate documents by ObjectId:\n\n```kotlin hl_lines=\"5 12\"\nstove {\n  mongodb {\n    val userId = ObjectId().toHexString()\n    \n    // First save the document\n    save(\n      instance = User(id = userId, name = \"John Doe\", email = \"john@example.com\", age = 30),\n      objectId = userId,\n      collection = \"users\"\n    )\n    \n    // Get from specific collection\n    shouldGet<User>(objectId = userId, collection = \"users\") { user ->\n      user.id shouldBe userId\n      user.name shouldBe \"John Doe\"\n      user.email shouldBe \"john@example.com\"\n      user.age shouldBe 30\n    }\n  }\n}\n```\n\n### Checking Non-Existence\n\nVerify that documents don't exist:\n\n```kotlin\nstove {\n  mongodb {\n    val nonExistentId = ObjectId().toHexString()\n    \n    // Check default collection\n    shouldNotExist(objectId = nonExistentId)\n    \n    // Check specific collection\n    shouldNotExist(objectId = nonExistentId, collection = \"users\")\n  }\n}\n```\n\n### Deleting Documents\n\nDelete documents and verify deletion:\n\n```kotlin\nstove {\n  mongodb {\n    val userId = ObjectId().toHexString()\n    \n    // Save a document\n    save(\n      instance = User(id = userId, name = \"John Doe\", email = \"john@example.com\", age = 30),\n      objectId = userId,\n      collection = \"users\"\n    )\n    \n    // Delete it\n    shouldDelete(objectId = userId, collection = \"users\")\n    \n    // Verify deletion\n    shouldNotExist(objectId = userId, collection = \"users\")\n  }\n}\n```\n\n### Querying Documents\n\nQuery documents using MongoDB query syntax:\n\n```kotlin hl_lines=\"13 22\"\nstove {\n  mongodb {\n    // Setup test data\n    listOf(\n      User(id = ObjectId().toHexString(), name = \"Alice\", email = \"alice@example.com\", age = 25),\n      User(id = ObjectId().toHexString(), name = \"Bob\", email = \"bob@example.com\", age = 35),\n      User(id = ObjectId().toHexString(), name = \"Charlie\", email = \"charlie@example.com\", age = 28)\n    ).forEach { user ->\n      save(instance = user, objectId = ObjectId().toHexString(), collection = \"users\")\n    }\n    \n    // Simple query\n    shouldQuery<User>(\n      query = \"\"\"{ \"age\": { \"${'$'}gte\": 30 } }\"\"\",\n      collection = \"users\"\n    ) { users ->\n      users.size shouldBe 1\n      users.first().name shouldBe \"Bob\"\n    }\n    \n    // Query with multiple conditions\n    shouldQuery<User>(\n      query = \"\"\"\n        {\n          \"${'$'}and\": [\n            { \"age\": { \"${'$'}gte\": 25 } },\n            { \"age\": { \"${'$'}lte\": 30 } }\n          ]\n        }\n      \"\"\".trimIndent(),\n      collection = \"users\"\n    ) { users ->\n      users.size shouldBe 2\n      users.map { it.name } shouldContainAll listOf(\"Alice\", \"Charlie\")\n    }\n  }\n}\n```\n\n### Accessing the Client Directly\n\nFor advanced operations, access the MongoDB client:\n\n```kotlin\nstove {\n  mongodb {\n    val mongoClient = client()\n    \n    // Access the database\n    val db = mongoClient.getDatabase(\"myDatabase\")\n    \n    // List collections\n    val collections = db.listCollectionNames().toList()\n    \n    // Perform custom operations\n    db.getCollection<Document>(\"users\")\n      .find()\n      .limit(10)\n      .toList()\n      .also { documents ->\n        documents.size shouldBeLessThanOrEqual 10\n      }\n  }\n}\n```\n\n### Pause and Unpause Container\n\nControl the MongoDB container for testing failure scenarios:\n\n```kotlin\nstove {\n  mongodb {\n    val userId = ObjectId().toHexString()\n    \n    // MongoDB is running\n    save(\n      instance = User(id = userId, name = \"John\", email = \"john@example.com\", age = 30),\n      objectId = userId,\n      collection = \"users\"\n    )\n    \n    // Pause the container\n    pause()\n    \n    // Your application should handle the failure\n    // ...\n    \n    // Unpause the container\n    unpause()\n    \n    // Verify recovery\n    shouldGet<User>(objectId = userId, collection = \"users\") { user ->\n      user.name shouldBe \"John\"\n    }\n  }\n}\n```\n\n!!! warning\n    `pause()`, `unpause()`, and `inspect()` operations are not supported when using a provided instance.\n\n### Container Inspection\n\nInspect the MongoDB container:\n\n```kotlin\nstove {\n  mongodb {\n    val info = inspect()\n    info?.let {\n      println(\"Container ID: ${it.containerId}\")\n      println(\"Network: ${it.network}\")\n      println(\"IP Address: ${it.ipAddress}\")\n    }\n  }\n}\n```\n\n## Complete Example\n\nHere's a complete end-to-end test combining HTTP, MongoDB, and Kafka:\n\n```kotlin hl_lines=\"20 30 46\"\ndata class Product(\n  val id: String,\n  val name: String,\n  val description: String,\n  val price: Double,\n  val categoryId: Int,\n  val stock: Int,\n  val createdAt: Instant = Instant.now()\n)\n\ntest(\"should create product and store in mongodb\") {\n  stove {\n    val productId = ObjectId().toHexString()\n    val productName = \"Gaming Laptop\"\n    val categoryId = 1\n\n    // Mock external service\n    wiremock {\n      mockGet(\n        url = \"/categories/$categoryId\",\n        statusCode = 200,\n        responseBody = Category(id = categoryId, name = \"Electronics\", active = true).some()\n      )\n    }\n\n    // Create product via API\n    http {\n      postAndExpectBody<ProductResponse>(\n        uri = \"/products\",\n        body = ProductCreateRequest(\n          name = productName,\n          description = \"High-performance gaming laptop\",\n          price = 1299.99,\n          categoryId = categoryId,\n          stock = 10\n        ).some()\n      ) { response ->\n        response.status shouldBe 201\n        response.body().id shouldNotBe null\n      }\n    }\n\n    // Verify stored in MongoDB\n    mongodb {\n      shouldQuery<Product>(\n        query = \"\"\"{ \"name\": \"$productName\" }\"\"\",\n        collection = \"products\"\n      ) { products ->\n        products.size shouldBe 1\n        products.first().also { product ->\n          product.name shouldBe productName\n          product.price shouldBe 1299.99\n          product.categoryId shouldBe categoryId\n          product.stock shouldBe 10\n        }\n      }\n    }\n\n    // Verify event was published\n    kafka {\n      shouldBePublished<ProductCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.name == productName &&\n        actual.price == 1299.99\n      }\n    }\n\n    // Update product stock via API\n    http {\n      putAndExpectBodilessResponse(\n        uri = \"/products/$productId/stock\",\n        body = UpdateStockRequest(quantity = -2).some()\n      ) { response ->\n        response.status shouldBe 200\n      }\n    }\n\n    // Verify stock updated in MongoDB\n    mongodb {\n      shouldQuery<Product>(\n        query = \"\"\"{ \"name\": \"$productName\" }\"\"\",\n        collection = \"products\"\n      ) { products ->\n        products.first().stock shouldBe 8\n      }\n    }\n  }\n}\n```\n\n## Integration with Application\n\nVerify application behavior using the bridge:\n\n```kotlin\ntest(\"should use repository to save product\") {\n  stove {\n    val productId = ObjectId().toHexString()\n    val product = Product(\n      id = productId,\n      name = \"Test Product\",\n      description = \"Test Description\",\n      price = 99.99,\n      categoryId = 1,\n      stock = 5\n    )\n\n    // Use application's repository\n    using<ProductRepository> {\n      save(product)\n    }\n\n    // Verify in MongoDB\n    mongodb {\n      shouldQuery<Product>(\n        query = \"\"\"{ \"name\": \"Test Product\" }\"\"\",\n        collection = \"products\"\n      ) { products ->\n        products.size shouldBe 1\n        products.first().id shouldBe productId\n        products.first().price shouldBe 99.99\n      }\n    }\n  }\n}\n```\n\n## Advanced Operations\n\n### Aggregation Queries\n\n```kotlin\nstove {\n  mongodb {\n    val mongoClient = client()\n    val db = mongoClient.getDatabase(\"myDatabase\")\n    \n    // Aggregation pipeline\n    val pipeline = listOf(\n      Aggregates.match(Filters.gte(\"price\", 100)),\n      Aggregates.group(\"${'$'}categoryId\", \n        Accumulators.sum(\"totalProducts\", 1),\n        Accumulators.avg(\"avgPrice\", \"${'$'}price\")\n      ),\n      Aggregates.sort(Sorts.descending(\"totalProducts\"))\n    )\n    \n    db.getCollection<Document>(\"products\")\n      .aggregate(pipeline)\n      .toList()\n      .also { results ->\n        results.size shouldBeGreaterThan 0\n        // Each result has categoryId, totalProducts, and avgPrice\n      }\n  }\n}\n```\n\n### Bulk Operations\n\n```kotlin\nstove {\n  mongodb {\n    val mongoClient = client()\n    val db = mongoClient.getDatabase(\"myDatabase\")\n    val collection = db.getCollection<Document>(\"users\")\n    \n    // Bulk insert\n    val users = (1..100).map { i ->\n      Document()\n        .append(\"_id\", ObjectId())\n        .append(\"name\", \"User $i\")\n        .append(\"email\", \"user$i@example.com\")\n        .append(\"age\", 20 + (i % 50))\n    }\n    \n    collection.insertMany(users)\n    \n    // Bulk update\n    collection.updateMany(\n      Filters.gte(\"age\", 40),\n      Updates.set(\"status\", \"senior\")\n    )\n    \n    // Verify\n    val seniorCount = collection.countDocuments(Filters.eq(\"status\", \"senior\"))\n    seniorCount shouldBeGreaterThan 0\n  }\n}\n```\n\n### Transaction Support\n\n```kotlin\nstove {\n  mongodb {\n    val mongoClient = client()\n    \n    mongoClient.startSession().use { session ->\n      session.startTransaction()\n      try {\n        val db = mongoClient.getDatabase(\"myDatabase\")\n        \n        // Perform operations in transaction\n        db.getCollection<Document>(\"accounts\")\n          .updateOne(\n            session,\n            Filters.eq(\"accountId\", \"sender\"),\n            Updates.inc(\"balance\", -100.0)\n          )\n        \n        db.getCollection<Document>(\"accounts\")\n          .updateOne(\n            session,\n            Filters.eq(\"accountId\", \"receiver\"),\n            Updates.inc(\"balance\", 100.0)\n          )\n        \n        session.commitTransaction()\n      } catch (e: Exception) {\n        session.abortTransaction()\n        throw e\n      }\n    }\n  }\n}\n```\n\n### Working with Indexes\n\n```kotlin\nstove {\n  mongodb {\n    val mongoClient = client()\n    val db = mongoClient.getDatabase(\"myDatabase\")\n    val collection = db.getCollection<Document>(\"users\")\n    \n    // Create unique index\n    collection.createIndex(\n      Indexes.ascending(\"email\"),\n      IndexOptions().unique(true)\n    )\n    \n    // Create compound index\n    collection.createIndex(\n      Indexes.compoundIndex(\n        Indexes.ascending(\"status\"),\n        Indexes.descending(\"createdAt\")\n      )\n    )\n    \n    // Create text index for search\n    collection.createIndex(\n      Indexes.text(\"name\")\n    )\n    \n    // List indexes\n    collection.listIndexes().toList().also { indexes ->\n      indexes.size shouldBeGreaterThan 1\n    }\n  }\n}\n```\n\n## Provided Instance (External MongoDB)\n\nFor CI/CD pipelines or shared infrastructure:\n\n```kotlin\nStove()\n  .with {\n    mongodb {\n      MongodbSystemOptions.provided(\n        connectionString = System.getenv(\"MONGODB_URI\") ?: \"mongodb://localhost:27017\",\n        host = System.getenv(\"MONGODB_HOST\") ?: \"localhost\",\n        port = System.getenv(\"MONGODB_PORT\")?.toInt() ?: 27017,\n        cleanup = { client ->\n          // Clean up test data after tests\n          client.getDatabase(\"testdb\").drop()\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"mongodb.uri=${cfg.connectionString}\",\n            \"mongodb.host=${cfg.host}\",\n            \"mongodb.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n## Error Handling\n\n```kotlin\nstove {\n  mongodb {\n    // Document not found\n    val nonExistentId = ObjectId().toHexString()\n    shouldNotExist(objectId = nonExistentId, collection = \"users\")\n    \n    // Attempting to get non-existent document throws exception\n    assertThrows<NoSuchElementException> {\n      shouldGet<User>(objectId = nonExistentId, collection = \"users\") { }\n    }\n    \n    // Verify existence check on existing document\n    val existingId = ObjectId().toHexString()\n    save(\n      instance = User(id = existingId, name = \"Existing\", email = \"existing@example.com\", age = 25),\n      objectId = existingId,\n      collection = \"users\"\n    )\n    \n    assertThrows<AssertionError> {\n      shouldNotExist(objectId = existingId, collection = \"users\")\n    }\n  }\n}\n```\n\n## Working with ObjectId\n\nMongoDB uses `ObjectId` as the default identifier. <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">Stove handles this transparently:</span>\n\n```kotlin\ndata class UserWithStringId(\n  val id: String, // String representation of ObjectId\n  val name: String,\n  val email: String\n)\n\nstove {\n  mongodb {\n    // Generate ObjectId\n    val objectId = ObjectId()\n    val stringId = objectId.toHexString()\n    \n    // Save with string ID\n    save(\n      instance = UserWithStringId(id = stringId, name = \"Test\", email = \"test@example.com\"),\n      objectId = stringId,\n      collection = \"users\"\n    )\n    \n    // Retrieve using string ID\n    shouldGet<UserWithStringId>(objectId = stringId, collection = \"users\") { user ->\n      user.id shouldBe stringId\n      user.name shouldBe \"Test\"\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/Components/08-mssql.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Microsoft SQL Server (MSSQL)</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n            testImplementation(\"com.trendyol:stove-mssql\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you'll have access to the `mssql` function when configuring Stove.\nThis function configures the MSSQL Docker container that is going to be started.\n\n```kotlin hl_lines=\"4 9-12\"\nStove()\n  .with {\n    mssql {\n      MsSqlOptions(\n        databaseName = \"testdb\",\n        userName = \"sa\",\n        password = \"YourStrong@Passw0rd\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Container Options\n\nCustomize the MSSQL container:\n\n```kotlin\nStove()\n  .with {\n    mssql {\n      MsSqlOptions(\n        container = MsSqlContainerOptions(\n          registry = \"mcr.microsoft.com/\",\n          image = \"mssql/server\",\n          tag = \"2019-latest\",\n          containerFn = { container ->\n            container.withEnv(\"ACCEPT_EULA\", \"Y\")\n          }\n        ),\n        applicationName = \"stove-tests\",\n        databaseName = \"testdb\",\n        userName = \"sa\",\n        password = \"YourStrong@Passw0rd\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n## Migrations\n\nStove provides a way to run database migrations before tests start:\n\n```kotlin\nclass InitialMigration : DatabaseMigration<MsSqlMigrationContext> {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: MsSqlMigrationContext) {\n    connection.operations.execute(\n      \"\"\"\n      CREATE TABLE Person (\n        PersonID INT PRIMARY KEY IDENTITY(1,1),\n        LastName VARCHAR(255) NOT NULL,\n        FirstName VARCHAR(255) NOT NULL,\n        Address VARCHAR(255),\n        City VARCHAR(255)\n      );\n      \"\"\".trimIndent()\n    )\n  }\n}\n\nclass CreateOrdersTableMigration : DatabaseMigration<MsSqlMigrationContext> {\n  override val order: Int = 2\n\n  override suspend fun execute(connection: MsSqlMigrationContext) {\n    connection.operations.execute(\n      \"\"\"\n      CREATE TABLE Orders (\n        OrderID INT PRIMARY KEY IDENTITY(1,1),\n        PersonID INT NOT NULL,\n        OrderDate DATETIME DEFAULT GETDATE(),\n        Amount DECIMAL(10, 2),\n        FOREIGN KEY (PersonID) REFERENCES Person(PersonID)\n      );\n      \"\"\".trimIndent()\n    )\n  }\n}\n```\n\nRegister migrations in your Stove configuration:\n\n```kotlin\nStove()\n  .with {\n    mssql {\n      MsSqlOptions(\n        databaseName = \"testdb\",\n        userName = \"sa\",\n        password = \"YourStrong@Passw0rd\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      ).migrations {\n        register<InitialMigration>()\n        register<CreateOrdersTableMigration>()\n      }\n    }\n  }\n  .run()\n```\n\n## Usage\n\n### Executing SQL\n\nExecute DDL and DML statements:\n\n```kotlin hl_lines=\"11 16 24 27\"\nstove {\n  mssql {\n    // Create tables\n    shouldExecute(\n      \"\"\"\n      CREATE TABLE Products (\n        ProductID INT PRIMARY KEY IDENTITY(1,1),\n        ProductName NVARCHAR(100) NOT NULL,\n        Price DECIMAL(10, 2) NOT NULL,\n        Stock INT DEFAULT 0,\n        CreatedAt DATETIME DEFAULT GETDATE()\n      );\n      \"\"\".trimIndent()\n    )\n\n    // Insert data\n    shouldExecute(\n      \"\"\"\n      INSERT INTO Products (ProductName, Price, Stock) \n      VALUES ('Laptop', 999.99, 10)\n      \"\"\".trimIndent()\n    )\n\n    // Update data\n    shouldExecute(\"UPDATE Products SET Stock = 5 WHERE ProductName = 'Laptop'\")\n\n    // Delete data\n    shouldExecute(\"DELETE FROM Products WHERE Stock = 0\")\n  }\n}\n```\n\n### Querying Data\n\nQuery data with type-safe mappers:\n\n```kotlin hl_lines=\"14 17 28-29\"\ndata class Person(\n  val personId: Int,\n  val lastName: String,\n  val firstName: String,\n  val address: String?,\n  val city: String?\n)\n\nstove {\n  mssql {\n    // Insert test data\n    shouldExecute(\"INSERT INTO Person (LastName, FirstName, Address, City) VALUES ('Doe', 'John', '123 Main St', 'Springfield')\")\n\n    // Query with mapper\n    shouldQuery<Person>(\n      query = \"SELECT * FROM Person\",\n      mapper = { resultSet ->\n        Person(\n          personId = resultSet.getInt(1),\n          lastName = resultSet.getString(2),\n          firstName = resultSet.getString(3),\n          address = resultSet.getString(4),\n          city = resultSet.getString(5)\n        )\n      }\n    ) { result ->\n      result.size shouldBe 1\n      result.first().apply {\n        personId shouldBe 1\n        lastName shouldBe \"Doe\"\n        firstName shouldBe \"John\"\n        address shouldBe \"123 Main St\"\n        city shouldBe \"Springfield\"\n      }\n    }\n  }\n}\n```\n\n### Using Operations Directly\n\nAccess SQL operations directly for advanced use cases:\n\n```kotlin\nstove {\n  mssql {\n    ops {\n      // Simple select\n      val result = select(\"SELECT 1 AS value\") {\n        it.getInt(1)\n      }\n      result.first() shouldBe 1\n\n      // Execute insert\n      execute(\"INSERT INTO Person (LastName, FirstName) VALUES ('Smith', 'Jane')\")\n\n      // Select with parameters\n      val users = select(\"SELECT * FROM Person WHERE LastName = 'Smith'\") { rs ->\n        Person(\n          personId = rs.getInt(\"PersonID\"),\n          lastName = rs.getString(\"LastName\"),\n          firstName = rs.getString(\"FirstName\"),\n          address = rs.getString(\"Address\"),\n          city = rs.getString(\"City\")\n        )\n      }\n      users.size shouldBeGreaterThan 0\n    }\n  }\n}\n```\n\n### Complex Queries\n\nExecute joins, aggregations, and complex queries:\n\n```kotlin\ndata class OrderSummary(\n  val personId: Int,\n  val personName: String,\n  val totalOrders: Int,\n  val totalAmount: Double\n)\n\nstove {\n  mssql {\n    // Setup test data\n    shouldExecute(\"INSERT INTO Person (LastName, FirstName, Address, City) VALUES ('Doe', 'John', '123 Main St', 'NYC')\")\n    shouldExecute(\"INSERT INTO Orders (PersonID, Amount) VALUES (1, 100.00)\")\n    shouldExecute(\"INSERT INTO Orders (PersonID, Amount) VALUES (1, 250.50)\")\n    shouldExecute(\"INSERT INTO Orders (PersonID, Amount) VALUES (1, 75.25)\")\n\n    // Aggregate query\n    shouldQuery<OrderSummary>(\n      query = \"\"\"\n        SELECT \n          p.PersonID,\n          CONCAT(p.FirstName, ' ', p.LastName) AS PersonName,\n          COUNT(o.OrderID) AS TotalOrders,\n          SUM(o.Amount) AS TotalAmount\n        FROM Person p\n        INNER JOIN Orders o ON p.PersonID = o.PersonID\n        GROUP BY p.PersonID, p.FirstName, p.LastName\n        HAVING COUNT(o.OrderID) > 0\n      \"\"\".trimIndent(),\n      mapper = { rs ->\n        OrderSummary(\n          personId = rs.getInt(\"PersonID\"),\n          personName = rs.getString(\"PersonName\"),\n          totalOrders = rs.getInt(\"TotalOrders\"),\n          totalAmount = rs.getDouble(\"TotalAmount\")\n        )\n      }\n    ) { summaries ->\n      summaries.size shouldBe 1\n      summaries.first().apply {\n        personName shouldBe \"John Doe\"\n        totalOrders shouldBe 3\n        totalAmount shouldBe 425.75\n      }\n    }\n  }\n}\n```\n\n### Working with Nullable Fields\n\nHandle nullable columns properly:\n\n```kotlin\ndata class PersonWithNullable(\n  val personId: Int,\n  val firstName: String,\n  val lastName: String,\n  val address: String?,\n  val city: String?,\n  val email: String?\n)\n\nstove {\n  mssql {\n    // Insert with null values\n    shouldExecute(\"INSERT INTO Person (LastName, FirstName) VALUES ('Solo', 'Han')\")\n\n    shouldQuery<PersonWithNullable>(\n      query = \"SELECT * FROM Person WHERE LastName = 'Solo'\",\n      mapper = { rs ->\n        PersonWithNullable(\n          personId = rs.getInt(\"PersonID\"),\n          firstName = rs.getString(\"FirstName\"),\n          lastName = rs.getString(\"LastName\"),\n          address = rs.getString(\"Address\"), // Can be null\n          city = rs.getString(\"City\"), // Can be null\n          email = rs.getString(\"Email\") // Can be null\n        )\n      }\n    ) { persons ->\n      persons.first().apply {\n        firstName shouldBe \"Han\"\n        lastName shouldBe \"Solo\"\n        address shouldBe null\n        city shouldBe null\n      }\n    }\n  }\n}\n```\n\n### Pause and Unpause Container\n\nTest failure scenarios:\n\n```kotlin\nstove {\n  mssql {\n    // Database is running\n    shouldQuery<Int>(\n      \"SELECT COUNT(*) FROM Person\",\n      mapper = { rs -> rs.getInt(1) }\n    ) { result ->\n      result.first() shouldBeGreaterThanOrEqual 0\n    }\n\n    // Pause the database\n    pause()\n\n    // Your application should handle the failure\n    // ...\n\n    // Unpause the database\n    unpause()\n\n    // Verify recovery\n    shouldQuery<Int>(\n      \"SELECT COUNT(*) FROM Person\",\n      mapper = { rs -> rs.getInt(1) }\n    ) { result ->\n      result.first() shouldBeGreaterThanOrEqual 0\n    }\n  }\n}\n```\n\n## Complete Example\n\nHere's a <span data-rn=\"underline\" data-rn-color=\"#009688\">complete end-to-end test</span>:\n\n```kotlin hl_lines=\"15 25\"\ndata class User(\n  val id: Int,\n  val username: String,\n  val email: String,\n  val createdAt: LocalDateTime\n)\n\ntest(\"should create user via API and verify in database\") {\n  stove {\n    val username = \"johndoe\"\n    val email = \"john@example.com\"\n\n    // Create user via API\n    http {\n      postAndExpectBody<UserResponse>(\n        uri = \"/users\",\n        body = CreateUserRequest(username = username, email = email).some()\n      ) { response ->\n        response.status shouldBe 201\n        response.body().username shouldBe username\n      }\n    }\n\n    // Verify in MSSQL\n    mssql {\n      shouldQuery<User>(\n        query = \"SELECT * FROM Users WHERE Email = '$email'\",\n        mapper = { rs ->\n          User(\n            id = rs.getInt(\"UserID\"),\n            username = rs.getString(\"Username\"),\n            email = rs.getString(\"Email\"),\n            createdAt = rs.getTimestamp(\"CreatedAt\").toLocalDateTime()\n          )\n        }\n      ) { users ->\n        users.size shouldBe 1\n        users.first().apply {\n          username shouldBe \"johndoe\"\n          email shouldBe \"john@example.com\"\n        }\n      }\n    }\n\n    // Verify event was published\n    kafka {\n      shouldBePublished<UserCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.username == username &&\n        actual.email == email\n      }\n    }\n  }\n}\n```\n\n## Integration with Application\n\nUse the bridge to access application components:\n\n```kotlin\ntest(\"should use repository to save user\") {\n  stove {\n    val user = User(id = 0, username = \"janedoe\", email = \"jane@example.com\", createdAt = LocalDateTime.now())\n\n    // Use application's repository\n    using<UserRepository> {\n      save(user)\n    }\n\n    // Verify in database\n    mssql {\n      shouldQuery<User>(\n        query = \"SELECT * FROM Users WHERE Username = 'janedoe'\",\n        mapper = { rs ->\n          User(\n            id = rs.getInt(\"UserID\"),\n            username = rs.getString(\"Username\"),\n            email = rs.getString(\"Email\"),\n            createdAt = rs.getTimestamp(\"CreatedAt\").toLocalDateTime()\n          )\n        }\n      ) { users ->\n        users.size shouldBe 1\n        users.first().email shouldBe \"jane@example.com\"\n      }\n    }\n  }\n}\n```\n\n## Batch Operations\n\nExecute multiple operations:\n\n```kotlin\nstove {\n  mssql {\n    // Create tables\n    shouldExecute(\n      \"\"\"\n      CREATE TABLE Categories (\n        CategoryID INT PRIMARY KEY IDENTITY(1,1),\n        CategoryName NVARCHAR(50) NOT NULL\n      );\n      CREATE TABLE Products (\n        ProductID INT PRIMARY KEY IDENTITY(1,1),\n        ProductName NVARCHAR(100) NOT NULL,\n        CategoryID INT REFERENCES Categories(CategoryID)\n      );\n      \"\"\".trimIndent()\n    )\n\n    // Insert categories\n    listOf(\"Electronics\", \"Books\", \"Clothing\").forEach { category ->\n      shouldExecute(\"INSERT INTO Categories (CategoryName) VALUES ('$category')\")\n    }\n\n    // Verify all inserted\n    shouldQuery<String>(\n      \"SELECT CategoryName FROM Categories\",\n      mapper = { it.getString(\"CategoryName\") }\n    ) { categories ->\n      categories.size shouldBe 3\n      categories shouldContainAll listOf(\"Electronics\", \"Books\", \"Clothing\")\n    }\n  }\n}\n```\n\n## Stored Procedures\n\nTest stored procedures:\n\n```kotlin\nstove {\n  mssql {\n    // Create stored procedure\n    shouldExecute(\n      \"\"\"\n      CREATE PROCEDURE GetPersonsByCity\n        @City NVARCHAR(100)\n      AS\n      BEGIN\n        SELECT * FROM Person WHERE City = @City\n      END\n      \"\"\".trimIndent()\n    )\n\n    // Insert test data\n    shouldExecute(\"INSERT INTO Person (LastName, FirstName, City) VALUES ('Doe', 'John', 'NYC')\")\n    shouldExecute(\"INSERT INTO Person (LastName, FirstName, City) VALUES ('Smith', 'Jane', 'NYC')\")\n    shouldExecute(\"INSERT INTO Person (LastName, FirstName, City) VALUES ('Brown', 'Bob', 'LA')\")\n\n    // Execute stored procedure\n    shouldQuery<Person>(\n      query = \"EXEC GetPersonsByCity @City = 'NYC'\",\n      mapper = { rs ->\n        Person(\n          personId = rs.getInt(\"PersonID\"),\n          lastName = rs.getString(\"LastName\"),\n          firstName = rs.getString(\"FirstName\"),\n          address = rs.getString(\"Address\"),\n          city = rs.getString(\"City\")\n        )\n      }\n    ) { persons ->\n      persons.size shouldBe 2\n      persons.all { it.city == \"NYC\" } shouldBe true\n    }\n  }\n}\n```\n\n## Transactions\n\nTest transaction behavior:\n\n```kotlin\nstove {\n  mssql {\n    ops {\n      // Start transaction manually via SQL\n      execute(\"BEGIN TRANSACTION\")\n      \n      try {\n        execute(\"INSERT INTO Person (LastName, FirstName) VALUES ('Test', 'User1')\")\n        execute(\"INSERT INTO Person (LastName, FirstName) VALUES ('Test', 'User2')\")\n        \n        // Commit transaction\n        execute(\"COMMIT TRANSACTION\")\n      } catch (e: Exception) {\n        execute(\"ROLLBACK TRANSACTION\")\n        throw e\n      }\n\n      // Verify\n      val count = select(\"SELECT COUNT(*) FROM Person WHERE LastName = 'Test'\") { it.getInt(1) }\n      count.first() shouldBe 2\n    }\n  }\n}\n```\n\n## Provided Instance (External MSSQL)\n\nFor CI/CD pipelines or shared infrastructure:\n\n```kotlin\nStove()\n  .with {\n    mssql {\n      MsSqlOptions.provided(\n        jdbcUrl = System.getenv(\"MSSQL_JDBC_URL\") ?: \"jdbc:sqlserver://localhost:1433;databaseName=testdb\",\n        host = System.getenv(\"MSSQL_HOST\") ?: \"localhost\",\n        port = System.getenv(\"MSSQL_PORT\")?.toInt() ?: 1433,\n        databaseName = \"testdb\",\n        username = System.getenv(\"MSSQL_USERNAME\") ?: \"sa\",\n        password = System.getenv(\"MSSQL_PASSWORD\") ?: \"YourStrong@Passw0rd\",\n        runMigrations = true,\n        cleanup = { operations ->\n          operations.execute(\"DELETE FROM Orders\")\n          operations.execute(\"DELETE FROM Person\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n## Data Types\n\nWorking with various SQL Server data types:\n\n```kotlin\ndata class DataTypesExample(\n  val id: Int,\n  val intValue: Int,\n  val bigIntValue: Long,\n  val decimalValue: BigDecimal,\n  val floatValue: Double,\n  val bitValue: Boolean,\n  val dateValue: LocalDate,\n  val timeValue: LocalTime,\n  val dateTimeValue: LocalDateTime,\n  val nvarcharValue: String,\n  val varcharValue: String\n)\n\nstove {\n  mssql {\n    // Create table with various types\n    shouldExecute(\n      \"\"\"\n      CREATE TABLE DataTypes (\n        ID INT PRIMARY KEY IDENTITY(1,1),\n        IntValue INT,\n        BigIntValue BIGINT,\n        DecimalValue DECIMAL(18, 4),\n        FloatValue FLOAT,\n        BitValue BIT,\n        DateValue DATE,\n        TimeValue TIME,\n        DateTimeValue DATETIME2,\n        NVarcharValue NVARCHAR(100),\n        VarcharValue VARCHAR(100)\n      )\n      \"\"\".trimIndent()\n    )\n\n    // Insert test data\n    shouldExecute(\n      \"\"\"\n      INSERT INTO DataTypes \n        (IntValue, BigIntValue, DecimalValue, FloatValue, BitValue, DateValue, TimeValue, DateTimeValue, NVarcharValue, VarcharValue)\n      VALUES \n        (42, 9223372036854775807, 1234.5678, 3.14159, 1, '2024-01-15', '14:30:00', '2024-01-15 14:30:00', N'Unicode: 日本語', 'ASCII text')\n      \"\"\".trimIndent()\n    )\n\n    // Query and verify\n    shouldQuery<DataTypesExample>(\n      query = \"SELECT * FROM DataTypes\",\n      mapper = { rs ->\n        DataTypesExample(\n          id = rs.getInt(\"ID\"),\n          intValue = rs.getInt(\"IntValue\"),\n          bigIntValue = rs.getLong(\"BigIntValue\"),\n          decimalValue = rs.getBigDecimal(\"DecimalValue\"),\n          floatValue = rs.getDouble(\"FloatValue\"),\n          bitValue = rs.getBoolean(\"BitValue\"),\n          dateValue = rs.getDate(\"DateValue\").toLocalDate(),\n          timeValue = rs.getTime(\"TimeValue\").toLocalTime(),\n          dateTimeValue = rs.getTimestamp(\"DateTimeValue\").toLocalDateTime(),\n          nvarcharValue = rs.getString(\"NVarcharValue\"),\n          varcharValue = rs.getString(\"VarcharValue\")\n        )\n      }\n    ) { results ->\n      results.first().apply {\n        intValue shouldBe 42\n        bitValue shouldBe true\n        nvarcharValue shouldContain \"日本語\"\n      }\n    }\n  }\n}\n```\n"
  },
  {
    "path": "docs/Components/09-redis.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Redis</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(\"com.trendyol:stove-redis:$version\")\n        }\n    ```\n\n## Configure\n\n```kotlin hl_lines=\"4 6-9\"\nStove()\n  .with {\n    redis {\n      RedisOptions(\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"redis.host=${cfg.host}\",\n            \"redis.port=${cfg.port}\",\n            \"redis.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }.run()\n```\n\n## Migrations\n\nRedis supports migrations for setting up initial data or configuration:\n\n```kotlin\nclass SeedCacheData : DatabaseMigration<RedisMigrationContext> {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: RedisMigrationContext) {\n    connection.connection.sync().apply {\n      // Seed initial cache data\n      set(\"config:feature-flag\", \"enabled\")\n      hset(\"defaults:settings\", mapOf(\n        \"timeout\" to \"30\",\n        \"retries\" to \"3\"\n      ))\n    }\n  }\n}\n\n// Register migrations\nredis {\n  RedisOptions(\n    configureExposedConfiguration = { cfg -> listOf(...) }\n  ).migrations {\n    register<SeedCacheData>()\n  }\n}\n```\n\n## Usage\n\nThe Redis component provides access to the underlying Lettuce Redis client, allowing you to <span data-rn=\"underline\" data-rn-color=\"#009688\">test all Redis operations</span>.\n\n### Accessing the Redis Client\n\nAccess the Redis client using the `client()` extension function:\n\n```kotlin\nstove {\n  redis {\n    val redisClient = client()\n    val connection = redisClient.connect()\n    // Use the connection for Redis operations\n    connection.close()\n  }\n}\n```\n\n### String Operations\n\nTest basic string operations:\n\n```kotlin hl_lines=\"3 7 8 12\"\nstove {\n  redis {\n    val connection = client().connect().sync()\n    \n    // SET and GET\n    connection.set(\"user:123:name\", \"John Doe\")\n    val name = connection.get(\"user:123:name\")\n    name shouldBe \"John Doe\"\n    \n    // SET with expiration\n    connection.setex(\"session:abc\", 3600, \"session-data\")\n    val ttl = connection.ttl(\"session:abc\")\n    ttl shouldBeGreaterThan 0\n    \n    // INCREMENT\n    connection.set(\"counter\", \"0\")\n    connection.incr(\"counter\")\n    connection.incr(\"counter\")\n    val counter = connection.get(\"counter\")\n    counter shouldBe \"2\"\n    \n    // Multiple keys\n    connection.mset(mapOf(\n      \"key1\" to \"value1\",\n      \"key2\" to \"value2\",\n      \"key3\" to \"value3\"\n    ))\n    val values = connection.mget(\"key1\", \"key2\", \"key3\")\n    values.size shouldBe 3\n  }\n}\n```\n\n### Hash Operations\n\nTest Redis hash operations:\n\n```kotlin\nstove {\n  redis {\n    val connection = client().connect().sync()\n    \n    // HSET and HGET\n    connection.hset(\"user:123\", \"name\", \"John Doe\")\n    connection.hset(\"user:123\", \"email\", \"john@example.com\")\n    connection.hset(\"user:123\", \"age\", \"30\")\n    \n    val name = connection.hget(\"user:123\", \"name\")\n    name shouldBe \"John Doe\"\n    \n    // HGETALL\n    val user = connection.hgetall(\"user:123\")\n    user[\"name\"] shouldBe \"John Doe\"\n    user[\"email\"] shouldBe \"john@example.com\"\n    user[\"age\"] shouldBe \"30\"\n    \n    // HMSET\n    connection.hmset(\"product:456\", mapOf(\n      \"name\" to \"Laptop\",\n      \"price\" to \"999.99\",\n      \"stock\" to \"10\"\n    ))\n    \n    // HINCRBY\n    connection.hincrby(\"product:456\", \"stock\", -1)\n    val stock = connection.hget(\"product:456\", \"stock\")\n    stock shouldBe \"9\"\n    \n    // HDEL\n    connection.hdel(\"user:123\", \"age\")\n    val age = connection.hget(\"user:123\", \"age\")\n    age shouldBe null\n  }\n}\n```\n\n### List Operations\n\nTest Redis list operations:\n\n```kotlin\nstove {\n  redis {\n    val connection = client().connect().sync()\n    \n    // LPUSH and RPUSH\n    connection.rpush(\"queue:tasks\", \"task1\", \"task2\", \"task3\")\n    connection.lpush(\"queue:tasks\", \"urgent-task\")\n    \n    // LRANGE\n    val tasks = connection.lrange(\"queue:tasks\", 0, -1)\n    tasks.size shouldBe 4\n    tasks.first() shouldBe \"urgent-task\"\n    \n    // LPOP and RPOP\n    val firstTask = connection.lpop(\"queue:tasks\")\n    firstTask shouldBe \"urgent-task\"\n    \n    val lastTask = connection.rpop(\"queue:tasks\")\n    lastTask shouldBe \"task3\"\n    \n    // LLEN\n    val length = connection.llen(\"queue:tasks\")\n    length shouldBe 2\n  }\n}\n```\n\n### Set Operations\n\nTest Redis set operations:\n\n```kotlin\nstove {\n  redis {\n    val connection = client().connect().sync()\n    \n    // SADD\n    connection.sadd(\"tags:123\", \"kotlin\", \"testing\", \"redis\")\n    \n    // SMEMBERS\n    val tags = connection.smembers(\"tags:123\")\n    tags.size shouldBe 3\n    tags shouldContain \"kotlin\"\n    \n    // SISMEMBER\n    val isKotlin = connection.sismember(\"tags:123\", \"kotlin\")\n    isKotlin shouldBe true\n    \n    // SREM\n    connection.srem(\"tags:123\", \"redis\")\n    val remainingTags = connection.smembers(\"tags:123\")\n    remainingTags.size shouldBe 2\n    \n    // Set operations\n    connection.sadd(\"set1\", \"a\", \"b\", \"c\")\n    connection.sadd(\"set2\", \"b\", \"c\", \"d\")\n    \n    // SINTER (intersection)\n    val intersection = connection.sinter(\"set1\", \"set2\")\n    intersection.size shouldBe 2\n    intersection shouldContain \"b\"\n    intersection shouldContain \"c\"\n    \n    // SUNION\n    val union = connection.sunion(\"set1\", \"set2\")\n    union.size shouldBe 4\n  }\n}\n```\n\n### Sorted Set Operations\n\nTest Redis sorted set operations:\n\n```kotlin\nstove {\n  redis {\n    val connection = client().connect().sync()\n    \n    // ZADD\n    connection.zadd(\"leaderboard\", 100.0, \"player1\")\n    connection.zadd(\"leaderboard\", 250.0, \"player2\")\n    connection.zadd(\"leaderboard\", 175.0, \"player3\")\n    \n    // ZRANGE (ascending)\n    val ascending = connection.zrange(\"leaderboard\", 0, -1)\n    ascending.size shouldBe 3\n    ascending.first() shouldBe \"player1\"\n    ascending.last() shouldBe \"player2\"\n    \n    // ZREVRANGE (descending)\n    val descending = connection.zrevrange(\"leaderboard\", 0, -1)\n    descending.first() shouldBe \"player2\"\n    \n    // ZSCORE\n    val score = connection.zscore(\"leaderboard\", \"player2\")\n    score shouldBe 250.0\n    \n    // ZRANK\n    val rank = connection.zrank(\"leaderboard\", \"player3\")\n    rank shouldBe 1L // 0-indexed\n    \n    // ZINCRBY\n    connection.zincrby(\"leaderboard\", 50.0, \"player1\")\n    val newScore = connection.zscore(\"leaderboard\", \"player1\")\n    newScore shouldBe 150.0\n  }\n}\n```\n\n### Async Operations\n\nUse async operations for better performance:\n\n```kotlin\nstove {\n  redis {\n    val connection = client().connect().async()\n    \n    // Async SET\n    val setFuture = connection.set(\"async:key\", \"async:value\")\n    setFuture.await() shouldBe \"OK\"\n    \n    // Async GET\n    val getFuture = connection.get(\"async:key\")\n    val value = getFuture.await()\n    value shouldBe \"async:value\"\n    \n    // Pipeline multiple operations\n    connection.setAutoFlushCommands(false)\n    val futures = listOf(\n      connection.set(\"key1\", \"value1\"),\n      connection.set(\"key2\", \"value2\"),\n      connection.set(\"key3\", \"value3\")\n    )\n    connection.flushCommands()\n    \n    futures.forEach { it.await() shouldBe \"OK\" }\n  }\n}\n```\n\n### Pub/Sub Operations\n\nTest Redis Pub/Sub:\n\n```kotlin\nstove {\n  redis {\n    val pubConnection = client().connectPubSub().sync()\n    val subConnection = client().connectPubSub().sync()\n    \n    // Subscribe to channel\n    val messages = mutableListOf<String>()\n    subConnection.addListener(object : RedisPubSubAdapter<String, String>() {\n      override fun message(channel: String, message: String) {\n        messages.add(message)\n      }\n    })\n    \n    subConnection.subscribe(\"notifications\")\n    \n    // Publish messages\n    pubConnection.publish(\"notifications\", \"User logged in\")\n    pubConnection.publish(\"notifications\", \"Order created\")\n    \n    // Wait for messages\n    delay(1.seconds)\n    \n    messages.size shouldBe 2\n    messages shouldContain \"User logged in\"\n    messages shouldContain \"Order created\"\n    \n    subConnection.unsubscribe(\"notifications\")\n  }\n}\n```\n\n### Expiration and TTL\n\nTest key expiration:\n\n```kotlin\nstove {\n  redis {\n    val connection = client().connect().sync()\n    \n    // Set with expiration\n    connection.setex(\"temp:data\", 5, \"temporary-value\")\n    \n    // Check TTL\n    val ttl = connection.ttl(\"temp:data\")\n    ttl shouldBeGreaterThan 0\n    ttl shouldBeLessThanOrEqual 5\n    \n    // Set expiration on existing key\n    connection.set(\"permanent\", \"data\")\n    connection.expire(\"permanent\", 10)\n    val newTtl = connection.ttl(\"permanent\")\n    newTtl shouldBeGreaterThan 0\n    \n    // Remove expiration\n    connection.persist(\"permanent\")\n    val persistedTtl = connection.ttl(\"permanent\")\n    persistedTtl shouldBe -1 // No expiration\n  }\n}\n```\n\n### Transactions\n\nTest Redis transactions:\n\n```kotlin\nstove {\n  redis {\n    val connection = client().connect().sync()\n    \n    connection.multi()\n    connection.set(\"account:1:balance\", \"1000\")\n    connection.decrby(\"account:1:balance\", 100)\n    connection.incrby(\"account:2:balance\", 100)\n    val results = connection.exec()\n    \n    results.size shouldBe 3\n    \n    val balance1 = connection.get(\"account:1:balance\")\n    balance1 shouldBe \"900\"\n    \n    val balance2 = connection.get(\"account:2:balance\")\n    balance2 shouldBe \"100\"\n  }\n}\n```\n\n### Pause and Unpause Container\n\nTest failure scenarios:\n\n```kotlin hl_lines=\"11 15 19\"\nstove {\n  redis {\n    val connection = client().connect().sync()\n    \n    // Redis is running\n    connection.set(\"test\", \"value\")\n    connection.get(\"test\") shouldBe \"value\"\n    \n    // Pause container\n    pause()\n    \n    // Operations should fail\n    shouldThrow<RedisException> {\n      connection.get(\"test\")\n    }\n    \n    // Unpause container\n    unpause()\n    \n    // Wait for recovery\n    delay(2.seconds)\n    \n    // Operations should work again\n    val value = connection.get(\"test\")\n    value shouldBe \"value\"\n  }\n}\n```\n\n## Complete Example\n\nHere's a complete caching test example:\n\n```kotlin hl_lines=\"7 14 22 30\"\ntest(\"should cache product data in redis\") {\n  stove {\n    val productId = \"product-123\"\n    \n    // Product not in cache - verify using client()\n    redis {\n      val conn = client().connect().sync()\n      val cached = conn.get(\"cache:product:$productId\")\n      cached shouldBe null\n    }\n    \n    // Fetch from database via API (application should cache the result)\n    http {\n      get<ProductResponse>(\"/products/$productId\") { product ->\n        product.id shouldBe productId\n        product.name shouldNotBe null\n      }\n    }\n    \n    // Application should have cached the product - verify\n    redis {\n      val conn = client().connect().sync()\n      val cachedData = conn.get(\"cache:product:$productId\")\n      cachedData shouldNotBe null\n      \n      val cachedProduct = objectMapper.readValue(cachedData, ProductResponse::class.java)\n      cachedProduct.id shouldBe productId\n    }\n    \n    // Verify TTL is set\n    redis {\n      val conn = client().connect().sync()\n      val ttl = conn.ttl(\"cache:product:$productId\")\n      ttl shouldBeGreaterThan 0\n      ttl shouldBeLessThanOrEqual 3600\n    }\n  }\n}\n```\n\n## Integration with Application\n\nTest application caching behavior:\n\n```kotlin\ntest(\"should use redis for session management\") {\n  stove {\n    val sessionId = UUID.randomUUID().toString()\n    \n    // Create session via API\n    http {\n      postAndExpectBody<SessionResponse>(\n        uri = \"/auth/login\",\n        body = LoginRequest(username = \"user\", password = \"pass\").some()\n      ) { response ->\n        response.status shouldBe 200\n        response.body().sessionId shouldBe sessionId\n      }\n    }\n    \n    // Verify session in Redis\n    redis {\n      val connection = client().connect().sync()\n      val sessionData = connection.get(\"session:$sessionId\")\n      sessionData shouldNotBe null\n      \n      val session = objectMapper.readValue(sessionData, Session::class.java)\n      session.username shouldBe \"user\"\n      session.createdAt shouldNotBe null\n    }\n    \n    // Use session\n    http {\n      get<UserProfile>(\n        uri = \"/profile\",\n        headers = mapOf(\"X-Session-ID\" to sessionId)\n      ) { profile ->\n        profile.username shouldBe \"user\"\n      }\n    }\n    \n    // Logout\n    http {\n      postAndExpectBodilessResponse(\n        uri = \"/auth/logout\",\n        body = LogoutRequest(sessionId = sessionId).some()\n      ) { response ->\n        response.status shouldBe 200\n      }\n    }\n    \n    // Verify session removed from Redis\n    redis {\n      val connection = client().connect().sync()\n      val sessionData = connection.get(\"session:$sessionId\")\n      sessionData shouldBe null\n    }\n  }\n}\n```\n\n## Advanced: Custom Extensions\n\n<span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">Create reusable extensions for common patterns:</span>\n\n```kotlin\n// Custom extension functions\nfun RedisSystem.shouldGet(key: String, assertion: (String?) -> Unit): RedisSystem {\n  val connection = client().connect().sync()\n  val value = connection.get(key)\n  assertion(value)\n  return this\n}\n\nfun RedisSystem.shouldSet(key: String, value: String): RedisSystem {\n  val connection = client().connect().sync()\n  connection.set(key, value)\n  return this\n}\n\n// Usage in tests\nstove {\n  redis {\n    shouldSet(\"user:123\", \"John Doe\")\n    shouldGet(\"user:123\") { value ->\n      value shouldBe \"John Doe\"\n    }\n  }\n}\n```"
  },
  {
    "path": "docs/Components/10-bridge.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Bridge</span>\n\nThe Bridge component gives you <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">direct access to your application's dependency injection (DI) container</span> from your tests. This lets you grab any bean or service your application has registered, which is super useful for testing internal state, verifying side effects, or setting up test data through your application's own services.\n\n## When You'd Use This\n\nWhen writing end-to-end tests, you often need to:\n\n- **Check internal state** that isn't exposed through APIs\n- **Use application services** to set up test data\n- **Call domain services directly** to test business logic\n- **Swap out time-dependent implementations** for deterministic tests\n- **Verify side effects** that happen inside the application\n\nBridge gives you a type-safe way to access any component from your application's DI container.\n\n## Configuration\n\nBridge is built into the supported framework starters, so no extra dependency is needed.\n\n!!! warning \"Quarkus\"\n    `stove-quarkus` does not provide `bridge()` support yet. Quarkus application beans live under the Quarkus runtime classloader, so use HTTP, Kafka, database, gRPC, and tracing assertions instead.\n\n=== \"Spring Boot\"\n\n    ```kotlin\n    dependencies {\n        testImplementation(\"com.trendyol:stove-spring:$version\")\n    }\n    ```\n\n=== \"Ktor\"\n\n    ```kotlin\n    dependencies {\n        testImplementation(\"com.trendyol:stove-ktor:$version\")\n    }\n    ```\n\n=== \"Micronaut\"\n\n    ```kotlin\n    dependencies {\n        testImplementation(\"com.trendyol:stove-micronaut:$version\")\n    }\n    ```\n\n### Setup\n\nEnable Bridge in your Stove configuration:\n\n```kotlin hl_lines=\"5 7\"\nStove()\n  .with {\n    httpClient { HttpClientSystemOptions(baseUrl = \"http://localhost:8080\") }\n    \n    bridge()  // Enable access to DI container\n    \n    springBoot(\n      runner = { params -> myApp.run(params) },\n      withParameters = listOf(\"server.port=8080\")\n    )\n  }\n  .run()\n```\n\n## Framework Support\n\n### Spring Boot\n\nFor Spring Boot applications, Bridge provides access to the `ApplicationContext`:\n\n```kotlin hl_lines=\"2\"\n// Bridge resolves beans from ApplicationContext\nusing<UserService> {\n    // 'this' is the UserService bean from Spring context\n    findById(123)\n}\n```\n\nUnder the hood, it uses `ApplicationContext.getBean()`:\n\n```kotlin\nclass SpringBridgeSystem(testSystem: TestSystem) : BridgeSystem<ApplicationContext>(testSystem) {\n    override fun <D : Any> get(klass: KClass<D>): D = ctx.getBean(klass.java)\n}\n```\n\n### Ktor\n\nKtor Bridge supports multiple dependency injection frameworks with automatic detection:\n\n- **Koin** - Popular DI framework for Kotlin\n- **Ktor-DI** - Ktor's native DI plugin\n- **Custom** - Any DI framework via custom resolver\n\nAuto-detection is based on **runtime installation state** in the Ktor `Application` (not classpath presence only):\n\n- If `dependencies { ... }` is active, Bridge uses **Ktor-DI**\n- Otherwise, if Koin is active (for example `install(Koin) { ... }`), Bridge uses **Koin**\n- If both are active, Bridge prefers **Ktor-DI**\n- If neither is active, Bridge throws a setup error with guidance\n\n```kotlin\n// Bridge resolves beans from your DI container\nusing<UserRepository> {\n    // 'this' is the UserRepository from your DI\n    save(user)\n}\n```\n\n#### DI Framework Setup\n\n**Using Koin:**\n\n```kotlin\ndependencies {\n    testImplementation(\"io.insert-koin:koin-ktor:$koinVersion\")\n}\n\n// In your test setup - bridge() uses Koin when Ktor-DI is not active at runtime\nStove()\n    .with {\n        bridge()\n        ktor(runner = { params -> MyApp.run(params) })\n    }\n    .run()\n```\n\n**Using Ktor-DI:**\n\n```kotlin\ndependencies {\n    testImplementation(\"io.ktor:ktor-server-di:$ktorVersion\")\n}\n\n// In your test setup - bridge() uses Ktor-DI when dependencies { ... } is active\nStove()\n    .with {\n        bridge()\n        ktor(runner = { params -> MyApp.run(params) })\n    }\n    .run()\n```\n\n**Using Custom Resolver:**\n\n```kotlin\n// For any other DI framework (Kodein, Dagger, etc.)\nStove()\n    .with {\n        bridge { application, type ->\n            // type is KType - preserves generic info like List<T>\n            myDiContainer.resolve(type)\n        }\n        ktor(runner = { params -> MyApp.run(params) })\n    }\n    .run()\n```\n\n#### Generic Type Resolution\n\nBridge preserves generic type information, enabling resolution of types like `List<Service>`:\n\n```kotlin\n// Works with Koin or Ktor-DI\nusing<List<PaymentService>> {\n    forEach { service -> service.pay(order) }\n}\n```\n\n#### Registering Test Dependencies in Ktor\n\nUnlike Spring Boot's unified `addTestDependencies`, Ktor test dependency registration differs by DI framework:\n\n**Koin - Using Modules:**\n\n```kotlin\nobject MyApp {\n    fun run(\n        args: Array<String>,\n        testModules: List<Module> = emptyList()  // Accept test modules\n    ): Application {\n        return embeddedServer(Netty, port = args.getPort()) {\n            install(Koin) {\n                modules(\n                    productionModule,\n                    *testModules.toTypedArray()  // Add test modules\n                )\n            }\n            configureRouting()\n        }.start(wait = false).application\n    }\n}\n\n// In your test setup\nStove()\n    .with {\n        bridge()\n        ktor(\n            runner = { params ->\n                MyApp.run(\n                    params,\n                    testModules = listOf(\n                        module {\n                            // Override production beans with test doubles\n                            single<TimeProvider>(override = true) { FixedTimeProvider() }\n                            single<EmailService>(override = true) { MockEmailService() }\n                        }\n                    )\n                )\n            }\n        )\n    }\n    .run()\n```\n\n**Ktor-DI - Using Dependencies Block:**\n\n```kotlin\nobject MyApp {\n    fun run(\n        args: Array<String>,\n        testDependencies: (DependencyRegistrar.() -> Unit)? = null  // Accept test registrations\n    ): Application {\n        return embeddedServer(Netty, port = args.getPort()) {\n            install(DI) {\n                dependencies {\n                    // Production dependencies\n                    provide<UserService> { UserServiceImpl() }\n                    provide<TimeProvider> { SystemTimeProvider() }\n                    \n                    // Apply test overrides if provided\n                    testDependencies?.invoke(this)\n                }\n            }\n            configureRouting()\n        }.start(wait = false).application\n    }\n}\n\n// In your test setup\nStove()\n    .with {\n        bridge()\n        ktor(\n            runner = { params ->\n                MyApp.run(params) {\n                    // Override production beans with test doubles\n                    provide<TimeProvider> { FixedTimeProvider() }\n                    provide<EmailService> { MockEmailService() }\n                }\n            }\n        )\n    }\n    .run()\n```\n\n!!! tip \"Test Dependency Patterns\"\n    - **Koin**: Use `override = true` in test modules to replace production beans\n    - **Ktor-DI**: Later `provide<T>` calls override earlier ones\n    - Both frameworks support the pattern of passing test-specific configuration to your app's run function\n\n## Usage\n\n### Single Bean Access\n\nAccess a single bean and perform operations:\n\n```kotlin\nstove {\n    using<UserService> {\n        // 'this' refers to UserService\n        val user = findById(123)\n        user.name shouldBe \"John Doe\"\n        user.email shouldBe \"john@example.com\"\n    }\n}\n```\n\n### Multiple Bean Access\n\nAccess multiple beans in a single block (up to 5 beans supported):\n\n```kotlin\nstove {\n    // Two beans\n    using<UserService, OrderService> { userService, orderService ->\n        val user = userService.findById(123)\n        val orders = orderService.findByUserId(123)\n        orders.size shouldBeGreaterThan 0\n    }\n    \n    // Three beans\n    using<UserService, ProductService, InventoryService> { users, products, inventory ->\n        val product = products.findById(\"SKU-123\")\n        val stock = inventory.getStock(product.id)\n        stock shouldBeGreaterThan 0\n    }\n    \n    // Four beans\n    using<A, B, C, D> { a, b, c, d ->\n        // Work with all four services\n    }\n    \n    // Five beans\n    using<A, B, C, D, E> { a, b, c, d, e ->\n        // Work with all five services\n    }\n}\n```\n\n### Capturing Values for Later Use\n\nWhen you need to capture a value from inside the `using` block for later use, declare a variable outside the block and assign it inside:\n\n```kotlin\nstove {\n    // Declare variable outside, assign inside\n    var userId: Long = 0\n    using<UserService> {\n        userId = createUser(CreateUserRequest(name = \"John\", email = \"john@example.com\")).id\n    }\n    \n    // Use the captured value in subsequent operations\n    http {\n        get<UserResponse>(\"/users/$userId\") { user ->\n            user.name shouldBe \"John\"\n        }\n    }\n    \n    // Capture multiple values\n    var user: User? = null\n    var token: String? = null\n    using<AuthService> {\n        user = register(email = \"test@example.com\", password = \"secret\")\n        token = generateToken(user!!)\n    }\n    \n    // Or use lateinit for non-nullable types\n    lateinit var order: Order\n    using<OrderService> {\n        order = findById(orderId)\n    }\n    \n    // Use captured values\n    http {\n        getResponse(\"/orders/${order.id}\", headers = mapOf(\"Authorization\" to \"Bearer $token\")) { response ->\n            response.status shouldBe 200\n        }\n    }\n}\n```\n\n!!! tip \"Variable Capture Pattern\"\n    Since `using` blocks don't return values, use the pattern of declaring variables outside and assigning inside when you need to pass data between blocks.\n\n## Use Cases\n\n### 1. Setting Up Test Data\n\nUse application repositories to set up test data:\n\n```kotlin\ntest(\"should return user orders\") {\n    stove {\n        // Create test data using application's repository\n        var userId: Long = 0\n        using<UserRepository> {\n            userId = save(User(name = \"Test User\", email = \"test@example.com\")).id\n        }\n        \n        using<OrderRepository> {\n            save(Order(userId = userId, amount = 100.0))\n            save(Order(userId = userId, amount = 250.0))\n        }\n        \n        // Test the API\n        http {\n            get<List<OrderResponse>>(\"/users/$userId/orders\") { orders ->\n                orders.size shouldBe 2\n                orders.sumOf { it.amount } shouldBe 350.0\n            }\n        }\n    }\n}\n```\n\n### 2. Verifying Internal State\n\nVerify state that isn't exposed through APIs:\n\n```kotlin\ntest(\"should update inventory after order\") {\n    stove {\n        val productId = \"PROD-123\"\n        \n        // Check initial inventory\n        var initialStock = 0\n        using<InventoryService> {\n            initialStock = getStock(productId)\n        }\n        \n        // Place an order via API\n        http {\n            postAndExpectBodilessResponse(\n                uri = \"/orders\",\n                body = CreateOrderRequest(productId = productId, quantity = 5).some()\n            ) { response ->\n                response.status shouldBe 201\n            }\n        }\n        \n        // Verify inventory was reduced (internal side effect)\n        using<InventoryService> {\n            getStock(productId) shouldBe (initialStock - 5)\n        }\n    }\n}\n```\n\n### 3. Testing Domain Services Directly\n\nTest business logic that may be complex to trigger through APIs:\n\n```kotlin\ntest(\"should calculate shipping cost correctly\") {\n    stove {\n        using<ShippingCalculator> {\n            // Test various scenarios directly\n            calculate(weight = 1.0, destination = \"US\") shouldBe 5.99\n            calculate(weight = 5.0, destination = \"US\") shouldBe 12.99\n            calculate(weight = 1.0, destination = \"EU\") shouldBe 15.99\n        }\n    }\n}\n```\n\n### 4. Triggering Scheduled Jobs\n\nManually trigger scheduled jobs for testing:\n\n```kotlin\ntest(\"should process pending orders when scheduler runs\") {\n    stove {\n        // Setup: Create pending orders\n        using<OrderRepository> {\n            save(Order(status = \"PENDING\", createdAt = Instant.now().minusHours(2)))\n            save(Order(status = \"PENDING\", createdAt = Instant.now().minusHours(3)))\n        }\n        \n        // Trigger the scheduled job manually\n        using<OrderProcessingScheduler> {\n            processPendingOrders()\n        }\n        \n        // Verify orders were processed\n        using<OrderRepository> {\n            findByStatus(\"PENDING\").size shouldBe 0\n            findByStatus(\"PROCESSED\").size shouldBe 2\n        }\n    }\n}\n```\n\n### 5. Time Control\n\nControl time-dependent behavior:\n\n```kotlin hl_lines=\"9 18 34\"\n// First, create a testable time provider interface\ninterface TimeProvider {\n    fun now(): Instant\n}\n\n// Production implementation\nclass SystemTimeProvider : TimeProvider {\n    override fun now(): Instant = Instant.now()\n}\n\n// Test implementation\nclass FixedTimeProvider(private var time: Instant) : TimeProvider {\n    override fun now(): Instant = time\n    fun advance(duration: Duration) { time = time.plus(duration) }\n}\n\n// Register test implementation in your Stove setup\naddTestDependencies {\n    bean<TimeProvider>(isPrimary = true) { FixedTimeProvider(Instant.parse(\"2024-01-01T00:00:00Z\")) }\n}\n\n// Use in tests\ntest(\"should expire session after timeout\") {\n    stove {\n        // Create session and capture the session ID\n        var sessionId: String = \"\"\n        http {\n            postAndExpectBody<SessionResponse>(\"/login\", body = credentials.some()) { response ->\n                sessionId = response.body().sessionId\n            }\n        }\n        \n        // Advance time past session timeout\n        using<FixedTimeProvider> {\n            advance(Duration.ofHours(2))\n        }\n        \n        // Session should be expired\n        http {\n            getResponse(\"/protected\", headers = mapOf(\"Session-ID\" to sessionId)) { response ->\n                response.status shouldBe 401\n            }\n        }\n    }\n}\n```\n\n### 6. Event Verification\n\nCapture and verify domain events:\n\n```kotlin\n// Test event listener (registered via addTestDependencies)\nclass TestEventCapture {\n    private val events = ConcurrentLinkedQueue<Any>()\n    \n    @EventListener\n    fun capture(event: Any) {\n        events.add(event)\n    }\n    \n    inline fun <reified T> getEvents(): List<T> = events.filterIsInstance<T>()\n    fun clear() = events.clear()\n}\n\ntest(\"should publish UserCreatedEvent when user registers\") {\n    stove {\n        // Clear previous events\n        using<TestEventCapture> { clear() }\n        \n        // Perform action\n        http {\n            postAndExpectBodilessResponse(\"/users\", body = newUser.some()) { \n                it.status shouldBe 201 \n            }\n        }\n        \n        // Verify event was published\n        using<TestEventCapture> {\n            val events = getEvents<UserCreatedEvent>()\n            events.size shouldBe 1\n            events.first().email shouldBe newUser.email\n        }\n    }\n}\n```\n\n## Test Bean Registration\n\nRegister test-specific beans using `addTestDependencies`:\n\n**Spring Boot 2.x / 3.x:**\n\n```kotlin\nimport com.trendyol.stove.addTestDependencies\n\nStove()\n    .with {\n        bridge()\n        springBoot(\n            runner = { params -> \n                runApplication<MyApp>(*params) {\n                    addTestDependencies {\n                        // Replace production beans with test doubles\n                        bean<TimeProvider>(isPrimary = true) { FixedTimeProvider(Instant.now()) }\n                        bean<EmailService>(isPrimary = true) { MockEmailService() }\n                        \n                        // Add test utilities\n                        bean<TestEventCapture>()\n                        bean<TestDataBuilder>()\n                    }\n                }\n            }\n        )\n    }\n    .run()\n```\n\n**Spring Boot 4.x:**\n\n```kotlin\nimport com.trendyol.stove.addTestDependencies4x\n\nStove()\n    .with {\n        bridge()\n        springBoot(\n            runner = { params -> \n                runApplication<MyApp>(*params) {\n                    addTestDependencies4x {\n                        // Replace production beans with test doubles\n                        registerBean<TimeProvider>(primary = true) { FixedTimeProvider(Instant.now()) }\n                        registerBean<EmailService>(primary = true) { MockEmailService() }\n                        \n                        // Add test utilities\n                        registerBean<TestEventCapture>()\n                        registerBean<TestDataBuilder>()\n                    }\n                }\n            }\n        )\n    }\n    .run()\n```\n\n### Alternative: Using `addInitializers` Directly\n\nFor more control, you can use `addInitializers` with `stoveSpringRegistrar`:\n\n```kotlin\n// Spring Boot 2.x / 3.x\naddInitializers(stoveSpringRegistrar {\n    bean<TimeProvider>(isPrimary = true) { FixedTimeProvider(Instant.now()) }\n    bean<TestEventCapture>()\n})\n\n// Spring Boot 4.x\naddInitializers(stoveSpring4xRegistrar {\n    registerBean<TimeProvider>(primary = true) { FixedTimeProvider(Instant.now()) }\n    registerBean<TestEventCapture>()\n})\n```\n\n## Integration with Other Systems\n\nBridge works seamlessly with other Stove systems:\n\n```kotlin\ntest(\"should process order end-to-end\") {\n    stove {\n        val orderId = UUID.randomUUID().toString()\n        \n        // Mock external payment service\n        wiremock {\n            mockPost(\"/payments/charge\", statusCode = 200, responseBody = PaymentResult(success = true).some())\n        }\n        \n        // Create order via API\n        http {\n            postAndExpectBody<OrderResponse>(\n                uri = \"/orders\",\n                body = CreateOrderRequest(id = orderId, amount = 99.99).some()\n            ) { response ->\n                response.status shouldBe 201\n            }\n        }\n        \n        // Verify in database using application's repository\n        using<OrderRepository> {\n            val order = findById(orderId)\n            order.status shouldBe \"PAID\"\n            order.paymentId shouldNotBe null\n        }\n        \n        // Verify Kafka event\n        kafka {\n            shouldBePublished<OrderPaidEvent>(atLeastIn = 10.seconds) {\n                actual.orderId == orderId\n            }\n        }\n        \n        // Verify in Couchbase (if using)\n        couchbase {\n            shouldGet<Order>(\"orders\", orderId) { order ->\n                order.status shouldBe \"PAID\"\n            }\n        }\n        \n        // Access domain service for additional verification\n        using<OrderAnalytics> {\n            getTodaysTotalRevenue() shouldBeGreaterThanOrEqual 99.99\n        }\n  }\n}\n```\n\n## Best Practices\n\n### 1. Use Bridge for Setup, HTTP for Actions\n\n```kotlin\n// ✅ Good: Use bridge for setup, HTTP for testing\nusing<ProductRepository> {\n    save(Product(id = \"123\", name = \"Test\", price = 99.99))\n}\nhttp {\n    get<ProductResponse>(\"/products/123\") { product ->\n        product.name shouldBe \"Test\"\n    }\n}\n\n// ❌ Avoid: Using bridge for everything\nusing<ProductService> {\n    create(product)\n    val retrieved = findById(\"123\")  // Not testing actual API\n    retrieved.name shouldBe \"Test\"\n}\n```\n\n### 2. Prefer Application Services Over Direct Repository Access\n\n```kotlin\n// ✅ Good: Use application services that encapsulate business logic\nusing<OrderService> {\n    createOrder(CreateOrderRequest(...))  // Triggers all business logic\n}\n\n// ⚠️ Be careful: Direct repository access bypasses business logic\nusing<OrderRepository> {\n    save(Order(...))  // No validation, no events, no side effects\n}\n```\n\n### 3. Clean Up Test Data\n\n```kotlin\n// Use cleanup functions or explicit cleanup in tests\nstove {\n    var userId: Long = 0\n    using<UserRepository> {\n        userId = save(user).id\n    }\n    \n    try {\n        // Test logic\n        http { /* ... */ }\n    } finally {\n        // Cleanup\n        using<UserRepository> {\n            deleteById(userId)\n        }\n    }\n}\n```\n\n### 4. Keep Test Beans Minimal\n\nOnly replace what's necessary:\n\n```kotlin\n// ✅ Good: Replace only time-sensitive components\naddTestDependencies {\n    bean<Clock>(isPrimary = true) { Clock.fixed(fixedInstant, ZoneId.UTC) }\n}\n\n// ❌ Avoid: Replacing too many components (reduces test value)\naddTestDependencies {\n    bean<UserService>(isPrimary = true) { MockUserService() }\n    bean<OrderService>(isPrimary = true) { MockOrderService() }\n    bean<PaymentService>(isPrimary = true) { MockPaymentService() }\n}\n```\n\n## Summary\n\nThe Bridge component enables:\n\n| Capability | Example Use Case |\n|------------|-----------------|\n| **Bean Access** | Resolve any bean from DI container |\n| **State Verification** | Check internal state not exposed by APIs |\n| **Test Setup** | Create test data using application services |\n| **Time Control** | Replace time providers for deterministic tests |\n| **Event Capture** | Verify domain events were published |\n| **Job Triggering** | Manually trigger scheduled tasks |\n| **Service Testing** | Test domain services directly |\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">Bridge is essential for comprehensive e2e testing</span>, allowing you to verify and control aspects of your application that aren't accessible through external interfaces alone.\n"
  },
  {
    "path": "docs/Components/11-provided-instances.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Provided Instances</span> (Testcontainer-less Mode)\n\nStove supports using <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">externally provided infrastructure instances instead of testcontainers</span>. This is particularly useful for:\n\n- **CI/CD pipelines** with shared infrastructure\n- **Reducing startup time** by reusing existing instances\n- **Lower memory/CPU usage** by avoiding container overhead\n- **Working with pre-configured environments**\n\n## Overview\n\nInstead of starting a testcontainer, you can configure Stove to connect to an existing instance using the `.provided(...)` companion function on the options class itself.\n\n## Core Concept\n\nEach system's options class (e.g., `CouchbaseSystemOptions`, `PostgresqlOptions`) has a companion function called `provided(...)` that returns a specialized options subclass configured for external instances.\n\n## Usage Pattern\n\nAll systems follow the same pattern:\n\n```kotlin hl_lines=\"5 15\"\nStove()\n  .with {\n    // Option 1: Container-based (default)\n    systemName {\n      SystemOptions(\n        // System-specific options\n        cleanup = { client -> /* cleanup logic */ },\n        configureExposedConfiguration = { cfg -> listOf(\"property=${cfg.value}\") }\n      )\n    }\n\n    // Option 2: Provided instance using .provided() companion function\n    systemName {\n      SystemOptions.provided(\n        // Connection parameters for external instance\n        runMigrations = true,\n        cleanup = { client -> /* cleanup logic */ },\n        configureExposedConfiguration = { cfg -> listOf(\"property=${cfg.value}\") }\n      )\n    }\n  }\n  .run()\n```\n\n## Supported Systems\n\n### Couchbase\n\n```kotlin hl_lines=\"24-25\"\n// Container-based with cleanup\nStove()\n  .with {\n    couchbase {\n      CouchbaseSystemOptions(\n        defaultBucket = \"myBucket\",\n        cleanup = { cluster ->\n          cluster.query(\"DELETE FROM `myBucket` WHERE type = 'test'\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"couchbase.hosts=${cfg.hostsWithPort}\",\n            \"couchbase.username=${cfg.username}\",\n            \"couchbase.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n\n// Provided instance\nStove()\n  .with {\n    couchbase {\n      CouchbaseSystemOptions.provided(\n        connectionString = \"couchbase://localhost:8091\",\n        username = \"admin\",\n        password = \"password\",\n        defaultBucket = \"myBucket\",\n        runMigrations = true,\n        cleanup = { cluster ->\n          cluster.query(\"DELETE FROM `myBucket` WHERE type = 'test'\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"couchbase.hosts=${cfg.hostsWithPort}\",\n            \"couchbase.username=${cfg.username}\",\n            \"couchbase.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Cassandra\n\n```kotlin\n// Container-based\nStove()\n  .with {\n    cassandra {\n      CassandraSystemOptions(\n        keyspace = \"my_keyspace\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\",\n            \"spring.cassandra.local-datacenter=${cfg.datacenter}\",\n            \"spring.cassandra.keyspace-name=${cfg.keyspace}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n\n// Provided instance\nStove()\n  .with {\n    cassandra {\n      CassandraSystemOptions.provided(\n        host = \"cassandra-host\",\n        port = 9042,\n        datacenter = \"datacenter1\",\n        keyspace = \"my_keyspace\",\n        runMigrations = true,\n        cleanup = { session ->\n          session.execute(\"TRUNCATE my_keyspace.users\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\",\n            \"spring.cassandra.local-datacenter=${cfg.datacenter}\",\n            \"spring.cassandra.keyspace-name=${cfg.keyspace}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Kafka\n\n```kotlin\n// Container-based\nStove()\n  .with {\n    kafka {\n      KafkaSystemOptions(\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"kafka.bootstrapServers=${cfg.bootstrapServers}\",\n            \"kafka.interceptorClasses=${cfg.interceptorClass}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n\n// Provided instance\nStove()\n  .with {\n    kafka {\n      KafkaSystemOptions.provided(\n        bootstrapServers = \"localhost:9092\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"kafka.bootstrapServers=${cfg.bootstrapServers}\",\n            \"kafka.interceptorClasses=${cfg.interceptorClass}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Redis\n\n```kotlin\n// Container-based\nStove()\n  .with {\n    redis {\n      RedisOptions(\n        cleanup = { client ->\n          client.connect().sync().flushdb()\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"redis.host=${cfg.host}\",\n            \"redis.port=${cfg.port}\",\n            \"redis.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n\n// Provided instance\nStove()\n  .with {\n    redis {\n      RedisOptions.provided(\n        host = \"localhost\",\n        port = 6379,\n        password = \"password\",\n        database = 8,\n        cleanup = { client ->\n          client.connect().sync().flushdb()\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"redis.host=${cfg.host}\",\n            \"redis.port=${cfg.port}\",\n            \"redis.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### PostgreSQL\n\n```kotlin\n// Container-based\nStove()\n  .with {\n    postgresql {\n      PostgresqlOptions(\n        databaseName = \"testdb\",\n        cleanup = { operations ->\n          operations.execute(\"DELETE FROM users WHERE email LIKE '%@test.com'\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n\n// Provided instance\nStove()\n  .with {\n    postgresql {\n      PostgresqlOptions.provided(\n        jdbcUrl = \"jdbc:postgresql://localhost:5432/testdb\",\n        host = \"localhost\",\n        port = 5432,\n        databaseName = \"testdb\",\n        username = \"postgres\",\n        password = \"postgres\",\n        runMigrations = true,\n        cleanup = { operations ->\n          operations.execute(\"DELETE FROM users WHERE email LIKE '%@test.com'\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### MySQL\n\n```kotlin\n// Container-based\nStove()\n  .with {\n    mysql {\n      MySqlOptions(\n        databaseName = \"testdb\",\n        cleanup = { operations ->\n          operations.execute(\"DELETE FROM users WHERE email LIKE '%@test.com'\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n\n// Provided instance\nStove()\n  .with {\n    mysql {\n      MySqlOptions.provided(\n        jdbcUrl = \"jdbc:mysql://localhost:3306/testdb\",\n        host = \"localhost\",\n        port = 3306,\n        databaseName = \"testdb\",\n        username = \"root\",\n        password = \"password\",\n        runMigrations = true,\n        cleanup = { operations ->\n          operations.execute(\"DELETE FROM users WHERE email LIKE '%@test.com'\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### MSSQL\n\n```kotlin\n// Container-based\nStove()\n  .with {\n    mssql {\n      MsSqlOptions(\n        applicationName = \"stove-tests\",\n        databaseName = \"testdb\",\n        userName = \"sa\",\n        password = \"YourStrong@Passw0rd\",\n        cleanup = { operations ->\n          operations.execute(\"DELETE FROM Orders WHERE OrderDate < GETDATE() - 1\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n\n// Provided instance\nStove()\n  .with {\n    mssql {\n      MsSqlOptions.provided(\n        jdbcUrl = \"jdbc:sqlserver://localhost:1433;databaseName=testdb\",\n        host = \"localhost\",\n        port = 1433,\n        databaseName = \"testdb\",\n        username = \"sa\",\n        password = \"YourStrong@Passw0rd\",\n        runMigrations = true,\n        cleanup = { operations ->\n          operations.execute(\"DELETE FROM Orders WHERE OrderDate < GETDATE() - 1\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### MongoDB\n\n```kotlin\n// Container-based\nStove()\n  .with {\n    mongodb {\n      MongodbSystemOptions(\n        cleanup = { client ->\n          client.getDatabase(\"testdb\").drop()\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"mongodb.uri=${cfg.connectionString}\",\n            \"mongodb.host=${cfg.host}\",\n            \"mongodb.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n\n// Provided instance\nStove()\n  .with {\n    mongodb {\n      MongodbSystemOptions.provided(\n        connectionString = \"mongodb://localhost:27017\",\n        host = \"localhost\",\n        port = 27017,\n        cleanup = { client ->\n          client.getDatabase(\"testdb\").drop()\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"mongodb.uri=${cfg.connectionString}\",\n            \"mongodb.host=${cfg.host}\",\n            \"mongodb.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n### Elasticsearch\n\n```kotlin\n// Container-based\nStove()\n  .with {\n    elasticsearch {\n      ElasticsearchSystemOptions(\n        cleanup = { esClient ->\n          esClient.indices().delete { it.index(\"test-*\") }\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"elasticsearch.host=${cfg.host}\",\n            \"elasticsearch.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n\n// Provided instance\nStove()\n  .with {\n    elasticsearch {\n      ElasticsearchSystemOptions.provided(\n        host = \"localhost\",\n        port = 9200,\n        password = \"\", // Leave empty if security is disabled\n        runMigrations = true,\n        cleanup = { esClient ->\n          esClient.indices().delete { it.index(\"test-*\") }\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"elasticsearch.host=${cfg.host}\",\n            \"elasticsearch.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\n## Cleanup Function\n\nThe `cleanup` parameter is available for both container-based and provided instance modes. It executes during `close()` before the system is stopped - this ensures cleanup runs after all tests have completed.\n\n### Use Cases\n\n1. **Clear test data** from previous runs\n2. **Reset state** to a known baseline\n3. **Delete test-specific records** that shouldn't persist\n\n### Example with Container Mode and keepDependenciesRunning\n\nThe cleanup function is especially useful when using containers with `keepDependenciesRunning`:\n\n```kotlin\nStove {\n  keepDependenciesRunning()\n}.with {\n  couchbase {\n    CouchbaseSystemOptions(\n      defaultBucket = \"myBucket\",\n      cleanup = { cluster ->\n        // Clean test data between runs when reusing containers\n        cluster.query(\"DELETE FROM `myBucket` WHERE type = 'test'\")\n      },\n      configureExposedConfiguration = { cfg ->\n        listOf(\n          \"couchbase.hosts=${cfg.hostsWithPort}\",\n          \"couchbase.username=${cfg.username}\",\n          \"couchbase.password=${cfg.password}\"\n        )\n      }\n    )\n  }\n}.run()\n```\n\n## Migration Handling\n\nWhen using provided instances, migrations are controlled by the `runMigrations` parameter in the `.provided()` function:\n\n- **`runMigrations = true` (default for databases)**: Migrations will run on every test execution\n- **`runMigrations = false` (default for Kafka/Redis)**: Migrations are skipped\n\n```kotlin hl_lines=\"4 11\"\nStove()\n  .with {\n    postgresql {\n      PostgresqlOptions.provided(\n        jdbcUrl = \"jdbc:postgresql://localhost:5432/mydb\",\n        host = \"localhost\",\n        port = 5432,\n        databaseName = \"mydb\",\n        username = \"user\",\n        password = \"pass\",\n        runMigrations = false, // Schema already exists\n        configureExposedConfiguration = { cfg -> listOf(/* ... */) }\n      )\n    }\n  }\n  .run()\n```\n\n## Limitations\n\nWhen using provided instances, some operations are not available:\n\n- **`pause()`** - Cannot pause an external instance\n- **`unpause()`** - Cannot unpause an external instance\n- **`inspect()`** - Container inspection not available\n\nThese methods will log a warning and return without effect when called on a provided instance.\n\n## Complete Example\n\nHere's a complete setup for a CI/CD pipeline using provided instances:\n\n```kotlin\nclass TestSetup : AbstractProjectConfig() {\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n        }\n        bridge()\n        couchbase {\n          CouchbaseSystemOptions.provided(\n            connectionString = System.getenv(\"COUCHBASE_CONNECTION_STRING\"),\n            username = System.getenv(\"COUCHBASE_USERNAME\"),\n            password = System.getenv(\"COUCHBASE_PASSWORD\"),\n            defaultBucket = \"app-bucket\",\n            runMigrations = true,\n            cleanup = { cluster ->\n              cluster.query(\"DELETE FROM `app-bucket` WHERE _type = 'test'\")\n            },\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"couchbase.hosts=${cfg.hostsWithPort}\",\n                \"couchbase.username=${cfg.username}\",\n                \"couchbase.password=${cfg.password}\"\n              )\n            }\n          )\n        }\n        kafka {\n          KafkaSystemOptions.provided(\n            bootstrapServers = System.getenv(\"KAFKA_BOOTSTRAP_SERVERS\"),\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"kafka.bootstrapServers=${cfg.bootstrapServers}\",\n                \"kafka.interceptorClasses=${cfg.interceptorClass}\"\n              )\n            }\n          )\n        }\n        springBoot(\n          runner = { params ->\n            com.example.Application.run(params)\n          }\n        )\n      }\n      .run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n}\n```\n\n## Test Isolation with Shared Infrastructure\n\n!!! warning \"Critical: Prevent Test Run Collisions\"\n\n    When using provided instances (shared infrastructure), **multiple test runs can interfere with each other** if they use the same resource names. This is especially important in CI/CD pipelines where parallel builds may run against the same infrastructure.\n\n### The Problem\n\nConsider this scenario:\n- Build #1 creates records in `orders` table\n- Build #2 starts while Build #1 is still running\n- Build #2 reads Build #1's test data → **Test failures!**\n- Both builds try to create the same Kafka topic → **Conflicts!**\n\n### The Solution: Unique Resource Prefixes\n\nGenerate unique prefixes for each test run and use them for all resource names:\n\n```kotlin\nobject TestRunContext {\n    // Unique prefix for this test run\n    val runId: String = System.getenv(\"CI_JOB_ID\") \n        ?: System.getenv(\"BUILD_NUMBER\")\n        ?: UUID.randomUUID().toString().take(8)\n    \n    // Resource names with unique prefixes\n    val databaseName = \"testdb_$runId\"\n    val topicPrefix = \"test_${runId}_\"\n    val indexPrefix = \"test_${runId}_\"\n    val bucketPrefix = \"test_${runId}_\"\n    val cacheKeyPrefix = \"test:$runId:\"\n}\n```\n\n### Implementation by System\n\n#### PostgreSQL / MSSQL - Unique Database\n\n```kotlin\nStove()\n    .with {\n        postgresql {\n            PostgresqlOptions.provided(\n                jdbcUrl = \"jdbc:postgresql://shared-db:5432/${TestRunContext.databaseName}\",\n                host = \"shared-db\",\n                port = 5432,\n                databaseName = TestRunContext.databaseName,\n                username = \"postgres\",\n                password = \"postgres\",\n                runMigrations = true,  // Creates tables in unique database\n                cleanup = { ops ->\n                    // Optional: cleanup is less critical with unique database\n                    ops.execute(\"DROP SCHEMA public CASCADE; CREATE SCHEMA public;\")\n                },\n                configureExposedConfiguration = { cfg ->\n                    listOf(\"spring.datasource.url=${cfg.jdbcUrl}\")\n                }\n            )\n        }\n        springBoot(\n            withParameters = listOf(\n                \"spring.datasource.url=jdbc:postgresql://shared-db:5432/${TestRunContext.databaseName}\"\n            )\n        )\n    }\n```\n\n!!! tip \"Database Creation\"\n    You can create the database using Stove's migration system:\n    ```kotlin\n    class CreateDatabaseMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n        override val order: Int = 0  // Run first\n        \n        override suspend fun execute(connection: PostgresSqlMigrationContext) {\n            connection.operations.execute(\n                \"CREATE DATABASE IF NOT EXISTS ${TestRunContext.databaseName}\"\n            )\n        }\n    }\n    ```\n\n!!! tip \"Multiple Databases\"\n    If your application uses multiple databases in production (e.g., separate databases for users, orders, analytics), you can create all of them via migrations and expose separate connection URLs:\n    \n    ```kotlin\n    configureExposedConfiguration = { cfg ->\n        val baseUrl = \"jdbc:postgresql://${cfg.host}:${cfg.port}\"\n        listOf(\n            \"db.users.url=$baseUrl/users_${TestRunContext.runId}\",\n            \"db.orders.url=$baseUrl/orders_${TestRunContext.runId}\",\n            \"db.analytics.url=$baseUrl/analytics_${TestRunContext.runId}\",\n            // ... common credentials\n        )\n    }\n    ```\n    \n    See [PostgreSQL - Multiple Databases](06-postgresql.md#multiple-databases) for a complete guide.\n\n#### Kafka - Unique Topic Prefix\n\n```kotlin\nStove()\n    .with {\n        kafka {\n            KafkaSystemOptions.provided(\n                bootstrapServers = \"shared-kafka:9092\",\n                topicSuffixes = TopicSuffixes(\n                    // These are suffixes for error/retry topics\n                    error = \".error\",\n                    retry = \".retry\"\n                ),\n                cleanup = { admin ->\n                    // Delete only topics with our prefix\n                    val ourTopics = admin.listTopics().names().get()\n                        .filter { it.startsWith(TestRunContext.topicPrefix) }\n                    if (ourTopics.isNotEmpty()) {\n                        admin.deleteTopics(ourTopics).all().get()\n                    }\n                },\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"kafka.bootstrapServers=${cfg.bootstrapServers}\",\n                        \"kafka.topicPrefix=${TestRunContext.topicPrefix}\"\n                    )\n                }\n            )\n        }\n        springBoot(\n            withParameters = listOf(\n                // Application uses this prefix for all topic names\n                \"kafka.topic.orders=${TestRunContext.topicPrefix}orders\",\n                \"kafka.topic.payments=${TestRunContext.topicPrefix}payments\",\n                \"kafka.topic.notifications=${TestRunContext.topicPrefix}notifications\"\n            )\n        )\n    }\n```\n\n#### Elasticsearch - Unique Index Prefix\n\n```kotlin\nStove()\n    .with {\n        elasticsearch {\n            ElasticsearchSystemOptions.provided(\n                host = \"shared-elasticsearch\",\n                port = 9200,\n                password = \"\",\n                runMigrations = true,\n                cleanup = { esClient ->\n                    // Delete only indices with our prefix\n                    esClient.indices().delete { \n                        it.index(\"${TestRunContext.indexPrefix}*\") \n                    }\n                },\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"elasticsearch.host=${cfg.host}\",\n                        \"elasticsearch.indexPrefix=${TestRunContext.indexPrefix}\"\n                    )\n                }\n            )\n        }\n        springBoot(\n            withParameters = listOf(\n                \"elasticsearch.index.products=${TestRunContext.indexPrefix}products\",\n                \"elasticsearch.index.orders=${TestRunContext.indexPrefix}orders\"\n            )\n        )\n    }\n```\n\n#### Couchbase - Unique Document Prefix or Scope\n\n```kotlin\nStove()\n    .with {\n        couchbase {\n            CouchbaseSystemOptions.provided(\n                connectionString = \"couchbase://shared-couchbase:8091\",\n                username = \"admin\",\n                password = \"password\",\n                defaultBucket = \"shared-bucket\",\n                runMigrations = true,\n                cleanup = { cluster ->\n                    // Delete only documents with our prefix\n                    cluster.query(\n                        \"DELETE FROM `shared-bucket` WHERE META().id LIKE '${TestRunContext.bucketPrefix}%'\"\n                    )\n                },\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"couchbase.documentPrefix=${TestRunContext.bucketPrefix}\"\n                    )\n                }\n            )\n        }\n        springBoot(\n            withParameters = listOf(\n                \"couchbase.documentPrefix=${TestRunContext.bucketPrefix}\"\n            )\n        )\n    }\n```\n\n#### MongoDB - Unique Database or Collection Prefix\n\n```kotlin\nStove()\n    .with {\n        mongodb {\n            MongodbSystemOptions.provided(\n                connectionString = \"mongodb://shared-mongo:27017\",\n                host = \"shared-mongo\",\n                port = 27017,\n                cleanup = { client ->\n                    // Drop our unique database\n                    client.getDatabase(TestRunContext.databaseName).drop()\n                },\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"mongodb.database=${TestRunContext.databaseName}\"\n                    )\n                }\n            )\n        }\n        springBoot(\n            withParameters = listOf(\n                \"spring.data.mongodb.database=${TestRunContext.databaseName}\"\n            )\n        )\n    }\n```\n\n#### Redis - Unique Key Prefix or Database Number\n\n```kotlin\nStove()\n    .with {\n        redis {\n            // Use unique database number (0-15) or key prefix\n            val redisDb = (TestRunContext.runId.hashCode() and 0xF)  // 0-15\n            \n            RedisOptions.provided(\n                host = \"shared-redis\",\n                port = 6379,\n                password = \"\",\n                database = redisDb,\n                cleanup = { client ->\n                    // Flush only our database\n                    client.connect().sync().flushdb()\n                },\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"spring.redis.database=$redisDb\"\n                    )\n                }\n            )\n        }\n    }\n```\n\n### Complete CI/CD Example\n\n```kotlin\nobject TestRunContext {\n    val runId: String = System.getenv(\"CI_JOB_ID\") \n        ?: System.getenv(\"GITHUB_RUN_ID\")\n        ?: System.getenv(\"BUILD_NUMBER\")\n        ?: UUID.randomUUID().toString().take(8)\n    \n    val databaseName = \"test_$runId\"\n    val topicPrefix = \"test_${runId}_\"\n    val indexPrefix = \"test_${runId}_\"\n    val keyPrefix = \"test:$runId:\"\n    \n    init {\n        println(\"Test Run ID: $runId\")\n        println(\"Database: $databaseName\")\n        println(\"Topic Prefix: $topicPrefix\")\n    }\n}\n\nclass TestConfig : AbstractProjectConfig() {\n    override suspend fun beforeProject() {\n        Stove()\n            .with {\n                postgresql {\n                    PostgresqlOptions.provided(\n                        jdbcUrl = \"jdbc:postgresql://db:5432/${TestRunContext.databaseName}\",\n                        databaseName = TestRunContext.databaseName,\n                        // ... other config\n                    )\n                }\n                kafka {\n                    KafkaSystemOptions.provided(\n                        bootstrapServers = \"kafka:9092\",\n                        cleanup = { admin ->\n                            val topics = admin.listTopics().names().get()\n                                .filter { it.startsWith(TestRunContext.topicPrefix) }\n                            if (topics.isNotEmpty()) admin.deleteTopics(topics).all().get()\n                        },\n                        // ... other config\n                    )\n                }\n                elasticsearch {\n                    ElasticsearchSystemOptions.provided(\n                        host = \"elasticsearch\",\n                        port = 9200,\n                        cleanup = { es ->\n                            es.indices().delete { it.index(\"${TestRunContext.indexPrefix}*\") }\n                        },\n                        // ... other config\n                    )\n                }\n                springBoot(\n                    runner = { params -> myApp.run(params) },\n                    withParameters = listOf(\n                        \"spring.datasource.url=jdbc:postgresql://db:5432/${TestRunContext.databaseName}\",\n                        \"kafka.topic.orders=${TestRunContext.topicPrefix}orders\",\n                        \"elasticsearch.index.products=${TestRunContext.indexPrefix}products\"\n                    )\n                )\n            }\n            .run()\n    }\n    \n    override suspend fun afterProject() {\n        Stove.stop()\n        // Resources cleaned up by cleanup functions\n    }\n}\n```\n\n### Best Practices for Test Isolation\n\n| Practice | Description |\n|----------|-------------|\n| **Use CI Job ID** | Most CI systems provide unique job/build IDs - use them |\n| **Prefix everything** | <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Database names, topics, indices, keys</span> - all should be unique |\n| **Clean up after** | Use cleanup functions to remove test data |\n| **Short prefixes** | Keep prefixes short but unique (8 chars usually enough) |\n| **Log the prefix** | Print the run ID at test start for debugging |\n| **Application support** | Your app must read resource names from configuration |\n\n### Debugging Isolation Issues\n\nIf tests fail intermittently in CI:\n\n1. **Check for hardcoded names:**\n   ```kotlin\n   // ❌ Bad - hardcoded\n   val topic = \"orders\"\n   \n   // ✅ Good - configurable\n   val topic = config.getString(\"kafka.topic.orders\")\n   ```\n\n2. **Verify cleanup runs:**\n   ```kotlin\n   cleanup = { admin ->\n       println(\"Cleaning up topics with prefix: ${TestRunContext.topicPrefix}\")\n       // ... cleanup code\n   }\n   ```\n\n3. **Check parallel job interference:**\n   ```bash\n   # In CI logs, look for overlapping run IDs\n   grep \"Test Run ID\" build-*.log\n   ```\n"
  },
  {
    "path": "docs/Components/12-grpc.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">gRPC</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(\"com.trendyol:stove-grpc:$version\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you'll have access to the `grpc` function when configuring Stove:\n\n```kotlin hl_lines=\"3 5-6\"\nStove()\n  .with {\n    grpc {\n      GrpcSystemOptions(\n        host = \"localhost\",\n        port = 50051\n      )\n    }\n  }\n  .run()\n```\n\n### Configuration Options\n\n```kotlin\ndata class GrpcSystemOptions(\n  /**\n   * The gRPC server host.\n   */\n  val host: String,\n\n  /**\n   * The gRPC server port.\n   */\n  val port: Int,\n\n  /**\n   * Whether to use plaintext (no TLS). Default is true for testing.\n   */\n  val usePlaintext: Boolean = true,\n\n  /**\n   * Request timeout duration (default: 30 seconds).\n   */\n  val timeout: Duration = 30.seconds,\n\n  /**\n   * List of client interceptors for logging, auth, tracing, etc.\n   */\n  val interceptors: List<ClientInterceptor> = emptyList(),\n\n  /**\n   * Default metadata (headers) to send with every request.\n   */\n  val metadata: Map<String, String> = emptyMap(),\n\n  /**\n   * Factory function for creating the underlying ManagedChannel.\n   */\n  val createChannel: (host: String, port: Int) -> ManagedChannel = { h, p ->\n    defaultChannelBuilder(h, p, usePlaintext, timeout, interceptors, metadata)\n  },\n\n  /**\n   * Factory function for creating Wire's GrpcClient with resources.\n   */\n  val createWireClient: (host: String, port: Int) -> WireClientResources = { h, p ->\n    defaultWireGrpcClient(h, p, timeout, metadata)\n  }\n)\n```\n\n### With Authentication\n\n```kotlin\ngrpc {\n  GrpcSystemOptions(\n    host = \"localhost\",\n    port = 50051,\n    metadata = mapOf(\"authorization\" to \"Bearer $token\"),\n    interceptors = listOf(LoggingInterceptor())\n  )\n}\n```\n\n## Usage\n\nStove's gRPC module supports multiple gRPC providers through a <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">provider-agnostic design</span>:\n\n- **Wire clients** (`wireClient<T>`) - For Wire-generated clients\n- **Typed channel** (`channel<T>`) - For any stub with a Channel constructor\n- **Custom providers** (`withEndpoint`) - For any gRPC library\n- **Raw channel** (`rawChannel`) - For advanced scenarios\n\n### Wire Clients\n\nFor services generated with [Wire](https://github.com/square/wire):\n\n```kotlin hl_lines=\"3 5\"\nstove {\n  grpc {\n    wireClient<GreeterServiceClient> {\n      val response = SayHello().execute(HelloRequest(name = \"World\"))\n      response.message shouldBe \"Hello, World!\"\n    }\n  }\n}\n```\n\n### Typed Channel (grpc-kotlin and Wire stubs)\n\nFor any stub that takes a Channel constructor. This works with both grpc-kotlin generated stubs and Wire-generated stubs:\n\n```kotlin hl_lines=\"3 5\"\nstove {\n  grpc {\n    channel<GreeterServiceStub> {\n      // 'this' is the stub - direct method calls\n      val response = sayHello(HelloRequest(name = \"World\"))\n      response.message shouldBe \"Hello, World!\"\n    }\n  }\n}\n```\n\n#### With Per-Call Metadata\n\n```kotlin\nstove {\n  grpc {\n    channel<GreeterServiceStub>(\n      metadata = mapOf(\"authorization\" to \"Bearer custom-token\")\n    ) {\n      val response = sayHello(HelloRequest(name = \"Authenticated\"))\n      response.message shouldBe \"Hello, Authenticated!\"\n    }\n  }\n}\n```\n\n### Custom Providers\n\nFor any other gRPC library, use `withEndpoint` with a factory function:\n\n```kotlin\nstove {\n  grpc {\n    withEndpoint({ host, port -> \n      // Create your client however you want\n      MyCustomGrpcClient.connect(host, port)\n    }) {\n      // 'this' is your client\n      this.call() shouldBe expected\n    }\n  }\n}\n```\n\n### Raw Channel Access\n\nFor advanced scenarios where you need full control:\n\n```kotlin\nstove {\n  grpc {\n    rawChannel { channel ->\n      // Full control over channel\n      val stub = GreeterGrpc.newBlockingStub(channel)\n      val response = stub.sayHello(request)\n      response.message shouldBe \"Hello!\"\n    }\n  }\n}\n```\n\n## Streaming\n\nAll streaming types work naturally with Kotlin coroutines.\n\n### Server Streaming\n\n```kotlin\nstove {\n  grpc {\n    channel<StreamServiceStub> {\n      val responses = serverStream(request).toList()\n      \n      responses.size shouldBe 5\n      responses[0].message shouldBe \"Item 0\"\n      responses[4].message shouldBe \"Item 4\"\n    }\n  }\n}\n```\n\n### Client Streaming\n\n```kotlin\nstove {\n  grpc {\n    channel<StreamServiceStub> {\n      val requestFlow = flow {\n        emit(Request(message = \"First\"))\n        emit(Request(message = \"Second\"))\n        emit(Request(message = \"Third\"))\n      }\n      \n      val response = clientStream(requestFlow)\n      response.message shouldBe \"Received: First, Second, Third\"\n      response.count shouldBe 3\n    }\n  }\n}\n```\n\n### Bidirectional Streaming\n\n```kotlin\nstove {\n  grpc {\n    channel<StreamServiceStub> {\n      val requestFlow = flow {\n        emit(Request(message = \"A\"))\n        emit(Request(message = \"B\"))\n      }\n      \n      val responses = bidiStream(requestFlow).toList()\n      responses.size shouldBe 2\n      responses[0].message shouldBe \"Echo: A\"\n      responses[1].message shouldBe \"Echo: B\"\n    }\n  }\n}\n```\n\n## Wire Client Details\n\n### Direct GrpcClient Access\n\n```kotlin\nstove {\n  grpc {\n    rawWireClient { client ->\n      val service = client.create(GreeterServiceClient::class)\n      val response = service.SayHello().execute(HelloRequest(name = \"Direct\"))\n      response.message shouldBe \"Hello, Direct!\"\n    }\n  }\n}\n```\n\n### Wire Client with Custom OkHttp Configuration\n\n```kotlin\nstove {\n  grpc {\n    withEndpoint({ host, port ->\n      val okHttpClient = OkHttpClient.Builder()\n        .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE))\n        .addInterceptor { chain ->\n          val request = chain.request().newBuilder()\n            .addHeader(\"authorization\", \"Bearer my-token\")\n            .build()\n          chain.proceed(request)\n        }\n        .build()\n      \n      GrpcClient.Builder()\n        .client(okHttpClient)\n        .baseUrl(\"http://$host:$port\")\n        .build()\n        .create(GreeterServiceClient::class)\n    }) {\n      val response = SayHello().execute(HelloRequest(name = \"Custom\"))\n      response.message shouldBe \"Hello, Custom!\"\n    }\n  }\n}\n```\n\n## Authentication & Interceptors\n\n### Global Interceptors\n\n```kotlin\nclass LoggingInterceptor : ClientInterceptor {\n  override fun <ReqT, RespT> interceptCall(\n    method: MethodDescriptor<ReqT, RespT>,\n    callOptions: CallOptions,\n    next: Channel\n  ): ClientCall<ReqT, RespT> {\n    println(\"Calling: ${method.fullMethodName}\")\n    return next.newCall(method, callOptions)\n  }\n}\n\nStove()\n  .with {\n    grpc {\n      GrpcSystemOptions(\n        host = \"localhost\",\n        port = 50051,\n        interceptors = listOf(LoggingInterceptor())\n      )\n    }\n  }\n```\n\n### Per-Call Metadata\n\n```kotlin\nstove {\n  grpc {\n    // Metadata is applied via interceptor automatically\n    channel<SecureServiceStub>(\n      metadata = mapOf(\n        \"authorization\" to \"Bearer jwt-token\",\n        \"x-request-id\" to \"12345\"\n      )\n    ) {\n      val response = secureEndpoint(request)\n      response.success shouldBe true\n    }\n  }\n}\n```\n\n## Error Handling\n\n### Testing Authentication Errors\n\n```kotlin\nstove {\n  grpc {\n    // Wire client - throws GrpcException\n    wireClient<SecureServiceClient> {\n      val exception = shouldThrow<GrpcException> {\n        SecureCall().execute(Request(message = \"Hello\"))\n      }\n      exception.grpcStatus shouldBe GrpcStatus.UNAUTHENTICATED\n    }\n    \n    // grpc-kotlin - throws StatusException\n    channel<SecureServiceStub> {\n      val exception = shouldThrow<StatusException> {\n        secureCall(request)\n      }\n      exception.status.code shouldBe Status.Code.UNAUTHENTICATED\n    }\n  }\n}\n```\n\n### Testing Not Found\n\n```kotlin\nstove {\n  grpc {\n    channel<UserServiceStub> {\n      val exception = shouldThrow<StatusException> {\n        getUser(GetUserRequest(id = 999999))\n      }\n      exception.status.code shouldBe Status.Code.NOT_FOUND\n    }\n  }\n}\n```\n\n## Complete Example\n\nHere's a complete test example with various gRPC operations:\n\n```kotlin\ntest(\"should perform gRPC operations\") {\n  stove {\n    // Test unary call\n    grpc {\n      channel<UserServiceStub> {\n        val response = createUser(CreateUserRequest(name = \"John\", email = \"john@example.com\"))\n        response.id shouldNotBe null\n        response.name shouldBe \"John\"\n      }\n    }\n\n    // Test with authentication\n    grpc {\n      channel<UserServiceStub>(\n        metadata = mapOf(\"authorization\" to \"Bearer admin-token\")\n      ) {\n        val users = listUsers(ListUsersRequest(limit = 10)).toList()\n        users.size shouldBeGreaterThan 0\n      }\n    }\n\n    // Test error handling\n    grpc {\n      channel<UserServiceStub> {\n        shouldThrow<StatusException> {\n          getUser(GetUserRequest(id = -1))\n        }.status.code shouldBe Status.Code.INVALID_ARGUMENT\n      }\n    }\n  }\n}\n```\n\n## Integration with Other Components\n\n### gRPC + Database\n\n```kotlin\nstove {\n  // Create via gRPC\n  var userId: Long = 0\n  grpc {\n    channel<UserServiceStub> {\n      val response = createUser(CreateUserRequest(name = \"John\"))\n      userId = response.id\n    }\n  }\n\n  // Verify in database\n  postgresql {\n    shouldQuery(\n      query = \"SELECT * FROM users WHERE id = $userId\",\n      mapper = { row -> User(row.long(\"id\"), row.string(\"name\")) }\n    ) { users ->\n      users.size shouldBe 1\n      users.first().name shouldBe \"John\"\n    }\n  }\n}\n```\n\n### gRPC + Kafka\n\n```kotlin\nstove {\n  // Trigger event via gRPC\n  grpc {\n    channel<OrderServiceStub> {\n      createOrder(CreateOrderRequest(amount = 100.0))\n    }\n  }\n\n  // Verify event was published\n  kafka {\n    shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {\n      actual.amount == 100.0\n    }\n  }\n}\n```\n\n## Provider Support\n\n| Provider | DSL Method | Notes |\n|----------|------------|-------|\n| Wire | `wireClient<T>` | For Wire-generated service clients |\n| grpc-kotlin | `channel<T>` | Works with any stub with Channel constructor |\n| Wire stubs | `channel<T>` | Works with Wire server stubs |\n| Custom | `withEndpoint` | Any library with factory function |\n| Advanced | `rawChannel` | Direct ManagedChannel access |\n| Advanced | `rawWireClient` | Direct Wire GrpcClient access |\n"
  },
  {
    "path": "docs/Components/13-reporting.md",
    "content": "# Reporting\n\nWhen tests fail, you want to know what went wrong. Stove's reporting system <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">tracks everything that happens during test execution</span>—every HTTP call, database query, Kafka message, and more. When something fails, you get a detailed report showing exactly what happened, making debugging much easier.\n\n## What You Get\n\n- **Automatic tracking** of all system interactions (HTTP requests, Kafka messages, database queries, etc.)\n- **Rich failure reports** that show what happened before the failure\n- **Multiple output formats** - human-readable console output or machine-readable JSON\n- **Framework integration** with Kotest and JUnit (optional extensions)\n\n## Quick Start\n\nThe reporting extensions are <span data-rn=\"underline\" data-rn-color=\"#009688\">optional but recommended</span>. They automatically enrich test failures with detailed execution reports, making debugging much easier.\n\n### Kotest Integration\n\nIf you're using Kotest, add the extension dependency:\n\n```kotlin hl_lines=\"3\"\ndependencies {\n    testImplementation(\"com.trendyol:stove-extensions-kotest\")\n}\n```\n\n!!! info \"Test Framework Extensions\"\n    `StoveKotestExtension` (`stove-extensions-kotest`) and `StoveJUnitExtension` (`stove-extensions-junit`) are separate packages. **Kotest** requires **6.1.3+**; **JUnit** requires **Jupiter 6.x** if possible. For Kotest, add a `kotest.properties` file with `kotest.framework.config.fqn=<your config class FQN>`. See the [Getting Started guide](../getting-started.md#step-3-create-test-configuration) for details.\n\nThen register it in your project config:\n\n```kotlin hl_lines=\"5\"\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\n\nclass TestConfig : AbstractProjectConfig() {\n    override val extensions: List<Extension> = listOf(StoveKotestExtension())\n    \n    override suspend fun beforeProject() {\n        Stove()\n            .with {\n                // your configuration\n            }\n            .run()\n    }\n    \n    override suspend fun afterProject() {\n        Stove.stop()\n    }\n}\n```\n\n### JUnit Integration\n\nFor JUnit, add the extension dependency:\n\n```kotlin hl_lines=\"3\"\ndependencies {\n    testImplementation(\"com.trendyol:stove-extensions-junit\")\n}\n```\n\nThen annotate your test class:\n\n```kotlin hl_lines=\"4 6\"\nimport com.trendyol.stove.extensions.junit.StoveJUnitExtension\nimport org.junit.jupiter.api.extension.ExtendWith\n\n@ExtendWith(StoveJUnitExtension::class)\nclass MyE2ETest {\n    // your tests\n}\n```\n\nThe JUnit extension works with both JUnit 5 and 6 since they share the same Jupiter API.\n\n## Configuration\n\nYou can configure reporting options when setting up Stove:\n\n```kotlin hl_lines=\"3-5\"\nStove {\n    reporting {\n        enabled()           // Enable reporting (default: true)\n        dumpOnFailure()     // Dump report when tests fail (default: true)\n        failureRenderer(PrettyConsoleRenderer)  // Set the renderer\n    }\n}.with {\n    // your configuration\n}.run()\n```\n\nOr use the direct methods if you prefer:\n\n```kotlin hl_lines=\"2-4\"\nStove {\n    reportingEnabled(true)\n    dumpReportOnTestFailure(true)\n    failureRenderer(PrettyConsoleRenderer)\n}.with {\n    // your configuration\n}.run()\n```\n\n## What Gets Reported\n\n### Actions\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">Every interaction with a Stove system is recorded:</span>\n\n<div data-rn-group>\n- **HTTP**: <span data-rn=\"highlight\" data-rn-color=\"#00968855\">All requests and responses</span> (GET, POST, PUT, DELETE, etc.)\n- **Kafka**: <span data-rn=\"underline\" data-rn-color=\"#009688\">Message publishing, consumption, and failure assertions</span>\n- **Database**: <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Queries, saves, deletes</span> (Couchbase, PostgreSQL, MongoDB, etc.)\n- **WireMock**: Stub registrations and verifications\n- **gRPC**: Client connections and calls\n</div>\n\n### Assertions\n\nBoth successful and failed assertions are tracked:\n\n- Expected vs. actual values\n- Assertion descriptions\n- Error messages\n\n## Example Output\n\nWhen a test fails, you'll see output like this:\n\n```\nexpected:<2> but was:<1>\n\n═══════════════════════════════════════════════════════════════════════════════\n                         STOVE EXECUTION REPORT\n═══════════════════════════════════════════════════════════════════════════════\n\n╔══════════════════════════════════════════════════════════════════════════════╗\n║                           STOVE TEST REPORT                                  ║\n║ Test: ExampleTest::should save the product                                   ║\n╠══════════════════════════════════════════════════════════════════════════════╣\n║ 14:47:38.215 ✓ PASSED [HTTP] POST /api/products                              ║\n║     Input: {\"id\":1234,\"name\":\"Test Product\"}                                 ║\n║     Output: 201 Created                                                      ║\n║                                                                              ║\n║ 14:47:38.341 ✗ FAILED [PostgreSQL] Query                                     ║\n║     Input: SELECT * FROM Products WHERE id=1234                              ║\n║     Output: 1 row(s) returned                                                ║\n║     Expected: 2                                                              ║\n║     Actual: 1                                                                ║\n║     Error: expected:<2> but was:<1>                                          ║\n╚══════════════════════════════════════════════════════════════════════════════╝\n```\n\n## Renderers\n\nStove provides two built-in renderers:\n\n### PrettyConsoleRenderer (Default)\n\nHuman-readable format with:\n\n- Colorized output (when terminal supports ANSI)\n- Box-drawing characters for structure\n- Timestamps for each action\n- Clear pass/fail indicators\n\n### JsonReportRenderer\n\nMachine-readable JSON format, useful for:\n\n- CI/CD integration\n- Log aggregation systems\n- Custom report processing\n\n```kotlin hl_lines=\"2\"\nStove {\n    failureRenderer(JsonReportRenderer)\n}\n```\n\nExample JSON output:\n\n```json\n{\n  \"testId\": \"ExampleTest::should save the product\",\n  \"testName\": \"should save the product\",\n  \"entries\": [\n    {\n      \"type\": \"action\",\n      \"system\": \"HTTP\",\n      \"action\": \"POST /api/products\",\n      \"timestamp\": \"2025-01-05T14:47:38.215\",\n      \"result\": \"PASSED\",\n      \"input\": {\"id\": 1234, \"name\": \"Test Product\"},\n      \"output\": \"201 Created\"\n    },\n    {\n      \"type\": \"action_with_result\",\n      \"system\": \"PostgreSQL\",\n      \"action\": \"Query\",\n      \"timestamp\": \"2025-01-05T14:47:38.341\",\n      \"result\": \"FAILED\",\n      \"expected\": 2,\n      \"actual\": 1,\n      \"error\": \"expected:<2> but was:<1>\"\n    }\n  ],\n  \"summary\": {\n    \"totalActions\": 2,\n    \"totalAssertions\": 0,\n    \"passedAssertions\": 0,\n    \"failedAssertions\": 1\n  }\n}\n```\n\nTo use the JSON renderer:\n\n```kotlin hl_lines=\"2\"\nStove {\n    failureRenderer(JsonReportRenderer)\n}\n```\n\n## System Snapshots\n\nSome systems provide state snapshots when tests fail, giving you <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">visibility into the system's internal state</span>:\n\n### Kafka Snapshot\n\nShows all messages in the message store:\n\n```\n╔══════════════════════════════════════════════════════════════════════════════╗\n║ ┌─ KAFKA ────────────────────────────────────────────────────────────────────║\n║                                                                              ║\n║   Consumed: 1                                                                ║\n║   Produced: 1                                                                ║\n║   Failed: 0                                                                  ║\n║                                                                              ║\n║   State Details:                                                             ║\n║     produced: 1 item(s)                                                      ║\n║       [0]                                                                    ║\n║         topic: product-events                                                ║\n║         key: 1234                                                            ║\n║         value: {\"id\":1234,\"name\":\"Test Product\"}                             ║\n║     consumed: 1 item(s)                                                      ║\n║       [0]                                                                    ║\n║         topic: product-events                                                ║\n║         value: {\"id\":1234,\"name\":\"Test Product\"}                             ║\n║     failed: 0 item(s)                                                        ║\n╚══════════════════════════════════════════════════════════════════════════════╝\n```\n\n### WireMock Snapshot\n\nShows registered stubs and unmatched requests:\n\n```\n╔══════════════════════════════════════════════════════════════════════════════╗\n║ ┌─ WIREMOCK ─────────────────────────────────────────────────────────────────║\n║                                                                              ║\n║   Registered stubs: 2                                                        ║\n║   Served requests: 1 (matched: 1)                                            ║\n║   Unmatched requests: 0                                                      ║\n║                                                                              ║\n║   State Details:                                                             ║\n║     registeredStubs: 2 item(s)                                               ║\n║     servedRequests: 1 item(s)                                                ║\n║     unmatchedRequests: 0 item(s)                                             ║\n╚══════════════════════════════════════════════════════════════════════════════╝\n```\n\n## Disabling Reporting\n\nIf you need to disable reporting (e.g., for performance-sensitive test runs):\n\n```kotlin\nStove {\n    reporting {\n        disabled()\n    }\n}\n```\n\nOr:\n\n```kotlin\nStove {\n    reportingEnabled(false)\n}\n```\n\n## Best Practices\n\n### 1. Use the Extension for Better Debugging\n\nWhile optional, the extensions make debugging much easier by <span data-rn=\"underline\" data-rn-color=\"#009688\">automatically tracking test context and enriching failures with detailed reports</span>. Just add the dependency for your test framework:\n\n- Kotest: `testImplementation(\"com.trendyol:stove-extensions-kotest\")`\n- JUnit: `testImplementation(\"com.trendyol:stove-extensions-junit\")`\n\n### 2. Use Descriptive Actions\n\nWhen writing custom assertions, provide meaningful descriptions:\n\n```kotlin\nshouldQuery<Product>(\"SELECT * FROM products WHERE active = true\") { products ->\n    products.size shouldBe expectedCount\n}\n```\n\n### 3. Review Reports on CI\n\nThe JSON renderer is particularly useful for CI/CD pipelines. You can:\n\n- Parse the JSON output for custom reporting\n- Store reports as build artifacts\n- Integrate with test management tools\n\n## Troubleshooting\n\n### Reports Not Showing\n\nIf you're not seeing reports when tests fail, check these:\n\n1. **Extension dependency added?** (optional but recommended)\n   - Kotest: `testImplementation(\"com.trendyol:stove-extensions-kotest\")`\n   - JUnit: `testImplementation(\"com.trendyol:stove-extensions-junit\")`\n\n2. **Extension registered?**\n   - Kotest: `override val extensions = listOf(StoveKotestExtension())`\n   - JUnit: `@ExtendWith(StoveJUnitExtension::class)`\n\n3. **Reporting enabled?**\n   ```kotlin\n   Stove {\n       reportingEnabled(true)\n       dumpReportOnTestFailure(true)\n   }\n   ```\n\n4. **Stove initialized?** Make sure <span data-rn=\"box\" data-rn-color=\"#ef5350\">`Stove().run()` is called before your tests execute</span>.\n\n### Truncated Output\n\nIf output appears truncated in your console, try:\n\n- Using a wider terminal window\n- Switching to `JsonReportRenderer` for full output\n- Checking your logging configuration\n\n### MCP Endpoint Unavailable\n\nIf an AI agent cannot connect to Stove MCP, first confirm that `stove` is running and check the startup banner for the actual `http://localhost:<port>/mcp` endpoint. You can also call `GET /api/v1/meta` and verify `mcp.enabled` is `true`.\n\nMCP is optional. If it is unavailable, ambiguous, or missing data for a run, agents should fall back to the normal failure report, test output, and logs.\n"
  },
  {
    "path": "docs/Components/14-grpc-mock.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">gRPC Mock</span>\n\n`stove-grpc-mock` provides a native gRPC mock server for testing gRPC service integrations. Unlike WireMock-based solutions, this implementation provides <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">**full support for all gRPC RPC types**</span> without external dependency conflicts.\n\n## Features\n\n| Feature | Support |\n|---------|---------|\n| Unary RPC | ✅ Full support |\n| Server Streaming | ✅ Full support |\n| Client Streaming | ✅ Full support |\n| Bidirectional Streaming | ✅ Full support |\n| Error responses | ✅ Full support |\n| Request matching | ✅ Full support |\n| **Authentication** | ✅ Full support |\n| Multiple services | ✅ Same port |\n\n## Installation\n\n```kotlin\ndependencies {\n  testImplementation(\"com.trendyol:stove-grpc-mock:$stoveVersion\")\n}\n```\n\n## Configuration\n\nBy default, gRPC Mock uses a **dynamic port** (port = 0), which lets the system pick an available port automatically. This avoids port conflicts, especially in CI environments.\n\n```kotlin hl_lines=\"4-5 11-12\"\nStove()\n  .with {\n    grpcMock {\n      GrpcMockSystemOptions(\n        // port = 0 by default (dynamic port)\n        removeStubAfterRequestMatched = true, // optional, default false\n        configureExposedConfiguration = { cfg ->\n          // cfg.host = \"localhost\"\n          // cfg.port = <dynamic-port>\n          listOf(\n            \"grpcService.host=${cfg.host}\",\n            \"grpcService.port=${cfg.port}\"\n          )\n        }\n      )\n    }\n    // Your application configuration - gRPC settings are auto-injected\n    ktor(\n      runner = { parameters -> run(parameters) }\n    )\n  }\n```\n\n### Using Fixed Port (Not Recommended for CI)\n\nIf you need a specific port:\n\n```kotlin\ngrpcMock {\n  GrpcMockSystemOptions(\n    port = 9090  // Fixed port\n  )\n}\n```\n\n!!! tip \"Dynamic Ports Avoid CI Conflicts\"\n\n    Using `port = 0` (the default) lets the system pick an available port automatically. This is essential in CI environments where:\n    \n    - Multiple test runs may execute in parallel\n    - Other services might already be using common ports\n    - You get \"Address already in use\" errors with fixed ports\n    \n    The `configureExposedConfiguration` callback receives the actual port after the server starts.\n\n## Usage\n\n### Mocking Unary Calls\n\n```kotlin hl_lines=\"4-5 16\"\ntest(\"should mock unary gRPC call\") {\n  stove {\n    grpcMock {\n      mockUnary(\n        serviceName = \"greeting.GreeterService\",\n        methodName = \"SayHello\",\n        response = HelloResponse.newBuilder()\n          .setMessage(\"Hello from mock!\")\n          .build()\n      )\n    }\n    \n    // Your test that triggers the gRPC call\n    http {\n      get(\"/api/greet/World\") { response ->\n        response.body shouldContain \"Hello from mock!\"\n      }\n    }\n  }\n}\n```\n\n### Mocking with Request Matching\n\n```kotlin hl_lines=\"3 6 15 18\"\ngrpcMock {\n  // Match specific request\n  mockUnary(\n    serviceName = \"users.UserService\",\n    methodName = \"GetUser\",\n    requestMatcher = RequestMatcher.ExactMessage(\n      GetUserRequest.newBuilder().setUserId(\"123\").build()\n    ),\n    response = GetUserResponse.newBuilder()\n      .setName(\"John Doe\")\n      .build()\n  )\n  \n  // Custom matcher\n  mockUnary(\n    serviceName = \"users.UserService\",\n    methodName = \"GetUser\",\n    requestMatcher = RequestMatcher.Custom { bytes ->\n      // Parse and inspect request bytes\n      val request = GetUserRequest.parseFrom(bytes)\n      request.userId.startsWith(\"vip-\")\n    },\n    response = GetUserResponse.newBuilder()\n      .setName(\"VIP User\")\n      .build()\n  )\n}\n```\n\n### Mocking Server Streaming\n\n```kotlin\ngrpcMock {\n  mockServerStream(\n    serviceName = \"streaming.ItemService\",\n    methodName = \"ListItems\",\n    responses = listOf(\n      Item.newBuilder().setId(\"1\").setName(\"Item 1\").build(),\n      Item.newBuilder().setId(\"2\").setName(\"Item 2\").build(),\n      Item.newBuilder().setId(\"3\").setName(\"Item 3\").build()\n    )\n  )\n}\n```\n\n### Mocking Client Streaming\n\n```kotlin\ngrpcMock {\n  mockClientStream(\n    serviceName = \"upload.UploadService\",\n    methodName = \"UploadChunks\",\n    response = UploadResponse.newBuilder()\n      .setTotalSize(1024)\n      .setSuccess(true)\n      .build()\n  )\n}\n```\n\n> **Note:** For client streaming, the `requestMatcher` is evaluated against **only the first request** in the stream. This is because stub matching happens before the full stream is received. If you need to validate all requests in a client stream, use the bidirectional streaming mock with a custom handler instead.\n\n### Mocking Bidirectional Streaming\n\n```kotlin\ngrpcMock {\n  mockBidiStream(\n    serviceName = \"chat.ChatService\",\n    methodName = \"Chat\"\n  ) { requestFlow ->\n    // Transform each request into a response\n    requestFlow.map { requestBytes ->\n      val request = ChatMessage.parseFrom(requestBytes)\n      ChatMessage.newBuilder()\n        .setMessage(\"Echo: ${request.message}\")\n        .build()\n    }\n  }\n}\n```\n\n### Mocking Error Responses\n\n```kotlin hl_lines=\"2 4-5 11 16-17\"\ngrpcMock {\n  mockError(\n    serviceName = \"users.UserService\",\n    methodName = \"GetUser\",\n    status = Status.Code.NOT_FOUND,\n    message = \"User not found\"\n  )\n  \n  // With request matching\n  mockError(\n    serviceName = \"users.UserService\",\n    methodName = \"DeleteUser\",\n    requestMatcher = RequestMatcher.ExactMessage(\n      DeleteUserRequest.newBuilder().setUserId(\"admin\").build()\n    ),\n    status = Status.Code.PERMISSION_DENIED,\n    message = \"Cannot delete admin user\"\n  )\n}\n```\n\n## Authentication Support\n\n`stove-grpc-mock` provides full support for mocking authenticated gRPC calls.\n\n### Bearer Token Authentication\n\n```kotlin\ngrpcMock {\n  mockUnary(\n    serviceName = \"secure.SecureService\",\n    methodName = \"GetSecret\",\n    metadataMatcher = MetadataMatcher.BearerToken(\"valid-token-123\"),\n    response = SecretResponse.newBuilder()\n      .setData(\"confidential\")\n      .build()\n  )\n}\n\n// Call with proper token\ngrpc {\n  channel<SecureServiceGrpcKt.SecureServiceCoroutineStub>(\n    metadata = mapOf(\"authorization\" to \"Bearer valid-token-123\")\n  ) {\n    val response = getSecret(request)  // Works!\n  }\n}\n```\n\n### Custom Header Matching\n\n```kotlin\ngrpcMock {\n  mockUnary(\n    serviceName = \"api.ApiService\",\n    methodName = \"GetData\",\n    metadataMatcher = MetadataMatcher.HasHeader(\"x-api-key\", \"secret-key\"),\n    response = DataResponse.newBuilder().build()\n  )\n}\n```\n\n### Require Any Authentication\n\n```kotlin\ngrpcMock {\n  // Matches any request with a non-empty authorization header\n  mockUnary(\n    serviceName = \"auth.AuthService\",\n    methodName = \"GetProfile\",\n    metadataMatcher = MetadataMatcher.RequiresAuth,\n    response = ProfileResponse.newBuilder().build()\n  )\n}\n```\n\n### Combined Matchers\n\n```kotlin\ngrpcMock {\n  mockUnary(\n    serviceName = \"multi.MultiAuthService\",\n    methodName = \"GetResource\",\n    metadataMatcher = MetadataMatcher.All(\n      MetadataMatcher.BearerToken(\"valid-token\"),\n      MetadataMatcher.HasHeader(\"x-tenant-id\", \"tenant-123\")\n    ),\n    response = ResourceResponse.newBuilder().build()\n  )\n}\n```\n\n### Authenticated Streaming\n\n```kotlin\ngrpcMock {\n  mockServerStream(\n    serviceName = \"secure.DataService\",\n    methodName = \"StreamData\",\n    metadataMatcher = MetadataMatcher.BearerToken(\"stream-token\"),\n    responses = listOf(data1, data2, data3)\n  )\n  \n  mockClientStream(\n    serviceName = \"secure.UploadService\",\n    methodName = \"Upload\",\n    metadataMatcher = MetadataMatcher.BearerToken(\"upload-token\"),\n    response = UploadResponse.newBuilder().setSuccess(true).build()\n  )\n  \n  mockBidiStream(\n    serviceName = \"secure.ChatService\",\n    methodName = \"Chat\",\n    metadataMatcher = MetadataMatcher.BearerToken(\"chat-token\")\n  ) { requestFlow ->\n    requestFlow.map { parseAndRespond(it) }\n  }\n}\n```\n\n### Testing Auth Failures\n\n```kotlin\ntest(\"should reject unauthenticated request\") {\n  stove {\n    grpcMock {\n      // Only accepts valid token\n      mockUnary(\n        serviceName = \"secure.SecureService\",\n        methodName = \"GetSecret\",\n        metadataMatcher = MetadataMatcher.BearerToken(\"valid-token\"),\n        response = SecretResponse.newBuilder().build()\n      )\n    }\n    \n    grpc {\n      // Call WITHOUT token - fails with UNIMPLEMENTED (no matching stub)\n      channel<SecureServiceGrpcKt.SecureServiceCoroutineStub> {\n        val exception = shouldThrow<StatusException> {\n          getSecret(request)\n        }\n        exception.status.code shouldBe Status.Code.UNIMPLEMENTED\n      }\n      \n      // Call WITH wrong token - also fails\n      channel<SecureServiceGrpcKt.SecureServiceCoroutineStub>(\n        metadata = mapOf(\"authorization\" to \"Bearer wrong-token\")\n      ) {\n        val exception = shouldThrow<StatusException> {\n          getSecret(request)\n        }\n        exception.status.code shouldBe Status.Code.UNIMPLEMENTED\n      }\n    }\n  }\n}\n```\n\n## Multiple gRPC Services\n\nThe mock server can handle **multiple services on the same port**. Simply register stubs for different services:\n\n```kotlin\nStove()\n  .with {\n    grpcMock {\n      GrpcMockSystemOptions(port = 9090)\n    }\n    ktor(\n      withParameters = listOf(\n        // All services point to the same mock server\n        \"featureToggle.host=localhost\",\n        \"featureToggle.port=9090\",\n        \"pricing.host=localhost\", \n        \"pricing.port=9090\",\n        \"inventory.host=localhost\",\n        \"inventory.port=9090\"\n      ),\n      runner = { parameters -> run(parameters) }\n    )\n  }\n```\n\nThen mock each service in your tests:\n\n```kotlin\ntest(\"should handle multiple gRPC services\") {\n  stove {\n    grpcMock {\n      // Service 1: Feature Toggle\n      mockUnary(\n        serviceName = \"featuretoggle.FeatureToggleService\",\n        methodName = \"IsFeatureEnabled\",\n        response = IsFeatureEnabledResponse.newBuilder()\n          .setEnabled(true)\n          .build()\n      )\n      \n      // Service 2: Pricing\n      mockUnary(\n        serviceName = \"pricing.PricingService\",\n        methodName = \"CalculatePrice\",\n        response = CalculatePriceResponse.newBuilder()\n          .setFinalPrice(29.99)\n          .build()\n      )\n      \n      // Service 3: Inventory (error case)\n      mockError(\n        serviceName = \"inventory.InventoryService\",\n        methodName = \"CheckStock\",\n        status = Status.Code.UNAVAILABLE,\n        message = \"Inventory service is down\"\n      )\n    }\n    \n    // Test your application logic\n    http {\n      post(\"/api/checkout\", body = checkoutRequest.some()) { response ->\n        // Assert based on mocked responses\n      }\n    }\n  }\n}\n```\n\n## Stub Removal Options\n\nBy default, stubs persist across requests. You can configure automatic removal:\n\n```kotlin\ngrpcMock {\n  GrpcMockSystemOptions(\n    port = 9090,\n    removeStubAfterRequestMatched = true // Remove stub after first match\n  )\n}\n```\n\nThis is useful when testing retry logic or different responses for sequential calls.\n\n## Direct gRPC Client Testing\n\nYou can also test gRPC calls directly using the `grpc` system:\n\n```kotlin\ntest(\"should call mocked gRPC service directly\") {\n  stove {\n    grpcMock {\n      mockUnary(\n        serviceName = \"greeting.GreeterService\",\n        methodName = \"SayHello\",\n        response = HelloResponse.newBuilder()\n          .setMessage(\"Hello!\")\n          .build()\n      )\n    }\n    \n    grpc {\n      channel<GreeterServiceGrpcKt.GreeterServiceCoroutineStub> {\n        val response = sayHello(\n          HelloRequest.newBuilder().setName(\"Test\").build()\n        )\n        response.message shouldBe \"Hello!\"\n      }\n    }\n  }\n}\n```\n\n## Architecture\n\n```\n┌─────────────────────────────────────────────────────────────┐\n│                    Your Application                          │\n├─────────────────────┬───────────────────────────────────────┤\n│  ServiceA Client    │      ServiceB Client                  │\n│  (port 9090)        │      (port 9090)                      │\n└────────────┬────────┴───────────────┬───────────────────────┘\n             │                        │\n             ▼                        ▼\n┌─────────────────────────────────────────────────────────────┐\n│              stove-grpc-mock Server (port 9090)             │\n│  ┌────────────────────────────────────────────────────────┐ │\n│  │              Dynamic Handler Registry                   │ │\n│  │  Routes by: serviceName/methodName                      │ │\n│  ├─────────────────────┬──────────────────────────────────┤ │\n│  │ serviceA.*          │    serviceB.*                     │ │\n│  │ → stub responses    │    → stub responses               │ │\n│  └─────────────────────┴──────────────────────────────────┘ │\n└─────────────────────────────────────────────────────────────┘\n```\n\n## Comparison with WireMock gRPC\n\n| Feature | stove-grpc-mock | WireMock gRPC |\n|---------|-----------------|---------------|\n| Unary RPC | ✅ | ✅ |\n| Server Streaming | ✅ Full | ⚠️ First response only |\n| Client Streaming | ✅ | ❌ Not supported |\n| Bidi Streaming | ✅ | ❌ Not supported |\n| Proto descriptors | Not needed | Required |\n| Dependency conflicts | None | Shaded protobuf issues |\n| Setup complexity | Simple | Requires descriptor generation |\n\n## Best Practices\n\n1. **Register stubs before triggering calls** - Stubs must be registered before your application makes gRPC calls.\n\n2. **Use specific request matchers** - When testing different scenarios, use `RequestMatcher.ExactMessage` to ensure the right stub is matched.\n\n3. **Test error scenarios** - Use `mockError()` to test how your application handles gRPC failures.\n\n4. **Multiple services, single port** - <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">Point all gRPC clients to the same mock server port</span> for simpler configuration.\n\n5. **Use `removeStubAfterRequestMatched`** - Enable this when testing retry logic or sequential calls with different responses.\n"
  },
  {
    "path": "docs/Components/15-tracing.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Tracing</span>\n\nYour end-to-end test just failed. Now what?\n\nYou stare at a stack trace that says *\"expected message not found within timeout\"*. You dig through application logs. You check Kafka topics. You wonder if the HTTP request even reached the controller. Was it a database error? A serialization issue? A Kafka consumer that silently died?\n\n**What if your test failure told you exactly what happened inside your application?**\n\n```\n═══════════════════════════════════════════════════════════════════════════════\nEXECUTION TRACE (Call Chain)\n═══════════════════════════════════════════════════════════════════════════════\n✓ POST (377ms)\n  ✓ POST /api/product/create (361ms)\n    ✓ ProductController.create (141ms)\n      ✓ ProductCreator.create (0ms)\n      ✓ KafkaProducer.send (137ms)\n        ✓ orders.created publish (81ms)\n          ✗ orders.created process (82ms)  ← FAILURE POINT\n```\n\nThat's Stove tracing. When a test fails, you see the <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">entire call chain</span> of your application, powered by <span data-rn=\"underline\" data-rn-color=\"#ff9800\">OpenTelemetry</span>: every controller method, every database query, every Kafka message, every HTTP call, with timing and the exact point of failure. It's a unique feature.\n\n## What You Get\n\nWhen tracing is enabled, every test failure comes with the full story:\n\n```\nSTOVE EXECUTION REPORT\n═══════════════════════════════════════════════════════════════════════════════\n\nTIMELINE\n────────\n14:45:38.439 ✓ PASSED [HTTP] POST /api/product/create\n14:45:38.472 ✗ FAILED [Kafka] shouldBePublished<ProductCreatedEvent>\n\nSYSTEM SNAPSHOTS\n────────────────\nKAFKA\n  Consumed: 0\n  Produced: 1\n  Failed: 1\n    [0] topic: orders.created\n        reason: Something went wrong\n\n═══════════════════════════════════════════════════════════════════════════════\nEXECUTION TRACE (Call Chain)\n═══════════════════════════════════════════════════════════════════════════════\n✓ POST (377ms)\n  ✓ POST /api/product/create (361ms)\n    ✓ ProductController.create (141ms)\n      ✓ ProductCreator.create (0ms)\n      ✓ KafkaProducer.send (137ms)\n        ✓ orders.created publish (81ms)\n          ✗ orders.created process (82ms)  ← FAILURE POINT\n```\n\n<span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">Everything is automatic:</span>\n\n- Traces **start and end** with each test\n- W3C `traceparent` headers are **injected into HTTP requests**\n- Trace headers are **injected into Kafka messages**\n- Trace metadata is **injected into gRPC calls**\n- All spans are **correlated back to the originating test**\n- Failure reports are **enriched with the execution trace**\n\nWhen failures include exceptions, you see those too:\n\n```\n✗ PaymentGateway.charge [80ms] ⚠ FAILURE POINT\n├── Exception: PaymentDeclinedException\n│   Message: Card declined\n│   at PaymentGateway.charge(PaymentGateway.kt:42)\n```\n\nSuccessful traces render as clean hierarchical trees:\n\n```\n✓ OrderController.createOrder [100ms]\n├── ✓ OrderService.processOrder [95ms]\n│   ├── ✓ UserRepository.findById [10ms]\n│   │   └── db.system: postgresql\n│   └── ✓ PaymentClient.charge [65ms]\n│       └── http.url: https://payment.api/charge\n\nSummary: 4 spans, 0 failures, total: 100ms\n```\n\n## Setup\n\nTwo steps. That's it.\n\n### Step 1: Enable tracing in your Stove config\n\n```kotlin hl_lines=\"3-4\"\nStove()\n    .with {\n        tracing {\n            enableSpanReceiver()\n        }\n        // ... your other systems (http, kafka, etc.)\n    }\n    .run()\n```\n\n### Step 2: Attach the OpenTelemetry agent in your build\n\n=== \"Gradle Plugin (Recommended)\"\n\n    ```kotlin hl_lines=\"2 6-7\"\n    plugins {\n        id(\"com.trendyol.stove.tracing\") version \"<stove-version>\"\n    }\n\n    stoveTracing {\n        serviceName.set(\"my-service\")\n    }\n    ```\n\n    The plugin is published to [Maven Central](https://central.sonatype.com/artifact/com.trendyol/stove-tracing-gradle-plugin). Add `mavenCentral()` to your `pluginManagement` repositories if not already present.\n\n=== \"buildSrc (Copy-Paste)\"\n\n    Copy [StoveTracingConfiguration.kt](https://github.com/Trendyol/stove/blob/main/buildSrc/src/main/kotlin/com/trendyol/stove/gradle/StoveTracingConfiguration.kt) to your project's `buildSrc/src/main/kotlin/` directory, then add to your `build.gradle.kts`:\n\n    ```kotlin hl_lines=\"3-4\"\n    import com.trendyol.stove.gradle.stoveTracing\n\n    stoveTracing {\n        serviceName = \"my-service\"\n    }\n    ```\n\nBoth approaches handle everything: downloading the OpenTelemetry Java Agent, configuring JVM arguments, attaching the agent to your test tasks, and dynamically assigning ports so parallel test runs don't conflict.\n\n!!! tip \"That's all you need\"\n    Now write your tests as usual. When a test fails, you'll see the execution trace automatically. <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">No code changes to your application required.</span> The OpenTelemetry agent instruments 100+ libraries (Spring, JDBC, Kafka, gRPC, HTTP clients, Redis, MongoDB, and more) with zero code changes.\n\n### Dependencies\n\n```kotlin hl_lines=\"2\"\ndependencies {\n    testImplementation(\"com.trendyol:stove-tracing:$stoveVersion\")\n    testImplementation(\"com.trendyol:stove-extensions-kotest:$stoveVersion\")\n    // or\n    testImplementation(\"com.trendyol:stove-extensions-junit:$stoveVersion\")\n}\n```\n\n!!! info \"Test Framework Extensions\"\n    `StoveKotestExtension` (`stove-extensions-kotest`) and `StoveJUnitExtension` (`stove-extensions-junit`) are separate packages that must be on your classpath. **Kotest** requires **6.1.3+**; **JUnit** requires **Jupiter 6.x** if possible. For Kotest, add a `kotest.properties` file with `kotest.framework.config.fqn=<your config class FQN>`. See the [Getting Started guide](../getting-started.md#step-3-create-test-configuration) for details.\n\n## Zero-Effort Trace Propagation\n\nYou don't need to do anything special in your test code. Stove injects trace headers into every interaction automatically:\n\n=== \"HTTP\"\n\n    ```kotlin\n    http {\n        get<UserResponse>(\"/users/123\") { user ->\n            user.name shouldBe \"John\"\n        }\n    }\n    ```\n\n=== \"Kafka\"\n\n    ```kotlin\n    kafka {\n        publish(\"orders.created\", OrderCreatedEvent(orderId = \"123\"))\n    }\n    ```\n\n=== \"gRPC\"\n\n    ```kotlin\n    grpc {\n        channel<GreeterServiceStub> {\n            sayHello(HelloRequest(name = \"World\"))\n        }\n    }\n    ```\n\nEvery HTTP request gets a `traceparent` header. Every Kafka message gets trace headers. Every gRPC call gets trace metadata. Your application picks these up through the OpenTelemetry agent, and Stove collects the resulting spans, all without you writing a single line of tracing code.\n\n## Trace Validation DSL\n\nBeyond automatic failure reports, you can actively query and assert on traces using the `tracing { }` DSL. This is useful when you want to verify *how* your application handled a request, not just *that* it did.\n\n```kotlin hl_lines=\"9 10 11 12\"\ntest(\"order processing should call payment service\") {\n    stove {\n        http {\n            post<OrderResponse>(\"/orders\", orderRequest) { response ->\n                response.status shouldBe \"created\"\n            }\n        }\n\n        tracing {\n            shouldContainSpan(\"OrderService.processOrder\")\n            shouldContainSpan(\"PaymentClient.charge\")\n            shouldNotHaveFailedSpans()\n            executionTimeShouldBeLessThan(500.milliseconds)\n        }\n    }\n}\n```\n\n### Span Assertions\n\nVerify which operations happened (or didn't) during a test:\n\n```kotlin hl_lines=\"2 3 6 9\"\ntracing {\n    shouldContainSpan(\"UserService.findById\")\n    shouldContainSpanMatching { it.operationName.contains(\"Repository\") }\n    shouldNotContainSpan(\"AdminService.delete\")\n\n    shouldNotHaveFailedSpans()\n    shouldHaveFailedSpan(\"PaymentGateway.charge\")\n\n    shouldHaveSpanWithAttribute(\"http.method\", \"GET\")\n    shouldHaveSpanWithAttributeContaining(\"http.url\", \"/api/users\")\n}\n```\n\n### Performance Assertions\n\nAssert on execution timing and span counts:\n\n```kotlin hl_lines=\"2 6\"\ntracing {\n    executionTimeShouldBeLessThan(500.milliseconds)\n    executionTimeShouldBeGreaterThan(10.milliseconds)\n\n    spanCountShouldBe(10)\n    spanCountShouldBeAtLeast(5)\n    spanCountShouldBeAtMost(20)\n}\n```\n\n### Debugging Helpers\n\nWhen you need to understand what happened during a test, render the trace:\n\n```kotlin\ntracing {\n    println(renderTree())    // Hierarchical tree view\n    println(renderSummary()) // Compact summary\n\n    val failedSpans = getFailedSpans()\n    val totalDuration = getTotalDuration()\n    val span = findSpanByName(\"OrderService.process\")\n\n    // Wait for spans to arrive before asserting (useful for async flows)\n    waitForSpans(expectedCount = 5, timeoutMs = 3000)\n}\n```\n\n## Real-World Example\n\nHere's a realistic scenario: an HTTP request triggers order processing, which publishes a Kafka event, which is consumed and writes to the database.\n\n```kotlin hl_lines=\"28-33\"\ntest(\"should create order and notify downstream services\") {\n    stove {\n        val orderId = UUID.randomUUID().toString()\n\n        // 1. Create order via HTTP\n        http {\n            post<OrderResponse>(\"/orders\", CreateOrderRequest(orderId, amount = 99.99)) { response ->\n                response.status shouldBe \"created\"\n            }\n        }\n\n        // 2. Verify Kafka event was published\n        kafka {\n            shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {\n                actual.orderId == orderId\n            }\n        }\n\n        // 3. Verify database state\n        postgresql {\n            shouldQuery<Order>(\"SELECT * FROM orders WHERE id = '$orderId'\") { orders ->\n                orders.size shouldBe 1\n                orders.first().status shouldBe \"CREATED\"\n            }\n        }\n\n        // 4. Verify the execution flow\n        tracing {\n            shouldContainSpan(\"OrderController.create\")\n            shouldContainSpan(\"OrderService.processOrder\")\n            shouldContainSpan(\"orders.created publish\")\n            shouldNotHaveFailedSpans()\n        }\n    }\n}\n```\n\nIf any step fails, the trace tree shows you <span data-rn=\"highlight\" data-rn-color=\"#ef535044\" data-rn-duration=\"800\">exactly where and why</span>:\n\n```\n✓ POST (250ms)\n  ✓ POST /orders (245ms)\n    ✓ OrderController.create [120ms]\n    ├── ✓ OrderService.processOrder [115ms]\n    │   ├── ✓ INSERT INTO orders [15ms]\n    │   │   └── db.system: postgresql\n    │   └── ✓ KafkaProducer.send [90ms]\n    │       └── ✓ orders.created publish [45ms]\n    │           └── ✓ orders.created process [40ms]\n    │               └── ✓ UPDATE orders SET status='CREATED' [8ms]\n\nSummary: 8 spans, 0 failures, total: 250ms\n```\n\n!!! note \"Working example\"\n    For a complete working project with tracing, see the [spring-showcase recipe](https://github.com/Trendyol/stove/tree/main/recipes/jvm/kotlin-recipes/spring-showcase).\n\n## Configuration Reference\n\n### Stove Test Config\n\nConfigure tracing behavior in your Stove setup:\n\n```kotlin hl_lines=\"2\"\ntracing {\n    enableSpanReceiver()              // Required: starts the span receiver\n    spanCollectionTimeout(10.seconds) // How long to wait for spans (default: 5s)\n    maxSpansPerTrace(2000)            // Cap spans per trace (default: 1000)\n    spanFilter { span ->              // Filter which spans are collected\n        !span.operationName.contains(\"health-check\")\n    }\n}\n```\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `enableSpanReceiver(port?)` | Port from `STOVE_TRACING_PORT` env or `4317` | Starts the OTLP gRPC receiver |\n| `spanCollectionTimeout` | `5.seconds` | How long to wait for spans when building failure reports |\n| `maxSpansPerTrace` | `1000` | Maximum spans stored per trace (prevents memory issues) |\n| `spanFilter` | Accept all | Predicate to filter which spans are collected |\n\n### Gradle Plugin\n\nThe Stove Tracing Gradle plugin configures the OpenTelemetry Java Agent for your test tasks. It is published to **Maven Central**.\n\nAdd `mavenCentral()` to your `pluginManagement` repositories:\n\n```kotlin\n// settings.gradle.kts\npluginManagement {\n    repositories {\n        mavenCentral()\n        gradlePluginPortal()\n    }\n}\n```\n\nThen apply the plugin:\n\n```kotlin\nplugins {\n    id(\"com.trendyol.stove.tracing\") version \"<stove-version>\"\n}\n```\n\nFor snapshot versions, also add the Maven Central snapshot repository:\n\n```kotlin\n// settings.gradle.kts\npluginManagement {\n    repositories {\n        mavenCentral()\n        maven(\"https://central.sonatype.com/repository/maven-snapshots\")\n        gradlePluginPortal()\n    }\n}\n```\n\nConfigure the plugin in your `build.gradle.kts`:\n\n```kotlin hl_lines=\"2-4\"\nstoveTracing {\n    serviceName.set(\"my-service\")\n    testTaskNames.set(listOf(\"integrationTest\")) // Only apply to specific tasks\n    disabledInstrumentations.set(listOf(\"jdbc\"))  // Exclude noisy instrumentations\n}\n```\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `serviceName` | `\"stove-traced-app\"` | Service name shown in traces |\n| `enabled` | `true` | Toggle tracing on/off |\n| `protocol` | `\"grpc\"` | OTLP protocol (currently only `grpc` is supported) |\n| `testTaskNames` | `[]` | Apply only to specific test tasks (empty = all) |\n| `otelAgentVersion` | `\"2.24.0\"` | OpenTelemetry Java Agent version |\n| `captureHttpHeaders` | `true` | Include HTTP headers in spans |\n| `captureExperimentalTelemetry` | `true` | Enable experimental HTTP telemetry |\n| `disabledInstrumentations` | `[]` | Instrumentations to disable (e.g., `jdbc`, `hibernate`) |\n| `additionalInstrumentations` | `[]` | Extra instrumentations to enable |\n| `customAnnotations` | `[]` | Custom annotation classes to instrument |\n| `bspScheduleDelay` | `100` | Batch span processor delay in ms (lower = faster export) |\n| `bspMaxBatchSize` | `1` | Batch size for span export (1 = immediate) |\n\n??? note \"Alternative: buildSrc copy-paste approach\"\n    If you prefer not to use the plugin, copy [`StoveTracingConfiguration.kt`](https://github.com/Trendyol/stove/blob/main/buildSrc/src/main/kotlin/com/trendyol/stove/gradle/StoveTracingConfiguration.kt) to your project's `buildSrc/src/main/kotlin/` directory and use `stoveTracing { ... }` in your build script.\n\n??? note \"Alternative: Manual OTel agent setup\"\n    If you prefer full control, you can configure the agent manually:\n\n    ```kotlin\n    // build.gradle.kts\n    val otelAgent by configurations.creating { isTransitive = false }\n\n    dependencies {\n        otelAgent(\"io.opentelemetry.javaagent:opentelemetry-javaagent:2.24.0\")\n    }\n\n    tasks.test {\n        doFirst {\n            jvmArgs(\n                \"-javaagent:${otelAgent.singleFile.absolutePath}\",\n                \"-Dotel.traces.exporter=otlp\",\n                \"-Dotel.exporter.otlp.protocol=grpc\",\n                \"-Dotel.exporter.otlp.endpoint=http://localhost:4317\",\n                \"-Dotel.metrics.exporter=none\",\n                \"-Dotel.logs.exporter=none\",\n                \"-Dotel.service.name=my-service\",\n                \"-Dotel.propagators=tracecontext,baggage\",\n                \"-Dotel.traces.sampler=always_on\",\n                \"-Dotel.bsp.schedule.delay=100\",\n                \"-Dotel.bsp.max.export.batch.size=1\",\n                \"-Dotel.instrumentation.grpc.enabled=false\"\n            )\n        }\n    }\n    ```\n\n## Best Practices\n\n1. <span data-rn=\"underline\" data-rn-color=\"#009688\">**Just enable it.**</span> Tracing is automatic and low-overhead; there's no reason not to use it\n2. **Use `tracing { }` sparingly.** The automatic failure reports cover most debugging needs; use the DSL only when you want to assert on the execution flow\n3. **Start with `shouldNotHaveFailedSpans()`.** The simplest assertion that catches unexpected errors\n4. **Filter noise.** If you see too many spans, use `disabledInstrumentations` to exclude verbose libraries like `jdbc` or `spring-scheduling`\n5. **CI just works.** <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">Ports are dynamically assigned</span>, so parallel test runs don't conflict\n\n!!! tip \"Works with Reporting\"\n    Tracing integrates seamlessly with Stove's [Reporting](13-reporting.md) system. When both are enabled, test failures include the execution report *and* the trace tree together, giving you <span data-rn=\"underline\" data-rn-color=\"#009688\">the complete picture</span>.\n\n## Troubleshooting\n\n### No trace in failure reports\n\n1. Ensure `stove-tracing` is in your dependencies\n2. Verify `enableSpanReceiver()` is called in your Stove config\n3. Verify the `com.trendyol.stove.tracing` plugin is applied in your `build.gradle.kts`\n4. Look for *\"Stove tracing: Attached OTel agent\"* in test output\n\n### Too many spans\n\nUse `disabledInstrumentations` to exclude noisy libraries:\n\n```kotlin\nstoveTracing {\n    serviceName.set(\"my-service\")\n    disabledInstrumentations.set(listOf(\"jdbc\", \"hibernate\", \"spring-scheduling\"))\n}\n```\n\n### Spans missing parent-child relationships\n\n1. Ensure trace context is propagated through async boundaries\n2. Check that the OTel agent version is compatible with your framework version\n"
  },
  {
    "path": "docs/Components/16-mysql.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">MySQL</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n            testImplementation(\"com.trendyol:stove-mysql\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you can <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">configure MySQL in your Stove setup</span>:\n\n```kotlin hl_lines=\"4 7-8\"\nStove()\n  .with {\n    mysql {\n      MySqlOptions {\n        listOf(\n          \"mysql.jdbcUrl=${it.jdbcUrl}\",\n          \"mysql.host=${it.host}\",\n          \"mysql.port=${it.port}\",\n          \"mysql.username=${it.username}\",\n          \"mysql.password=${it.password}\"\n        )\n      }\n    }\n  }.run()\n```\n\nThe `it` reference gives you access to the MySQL container's connection details, which you can pass to your application.\n\n## Migrations\n\nStove provides a way to run database migrations before tests start:\n\n```kotlin\nclass InitialMigration : DatabaseMigration<MySqlMigrationContext> {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: MySqlMigrationContext) {\n    connection.operations.execute(\n      \"\"\"\n      CREATE TABLE IF NOT EXISTS users (\n        id INT AUTO_INCREMENT PRIMARY KEY,\n        name VARCHAR(100) NOT NULL,\n        email VARCHAR(100) NOT NULL UNIQUE,\n        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n      );\n      \"\"\".trimIndent()\n    )\n  }\n}\n```\n\nRegister migrations in your Stove configuration:\n\n```kotlin\nStove()\n  .with {\n    mysql {\n      MySqlOptions(\n        databaseName = \"testing\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      ).migrations {\n        register<InitialMigration>()\n      }\n    }\n  }\n  .run()\n```\n\n## Usage\n\n### Executing SQL\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">Execute DDL and DML statements with `shouldExecute`:</span>\n\n```kotlin hl_lines=\"4 11 19 22\"\nstove {\n  mysql {\n    // Create tables\n    shouldExecute(\n      \"\"\"\n      DROP TABLE IF EXISTS products;\n      CREATE TABLE IF NOT EXISTS products (\n        id INT AUTO_INCREMENT PRIMARY KEY,\n        name VARCHAR(100) NOT NULL,\n        price DECIMAL(10, 2) NOT NULL,\n        stock INT DEFAULT 0\n      );\n      \"\"\".trimIndent()\n    )\n\n    // Insert data\n    shouldExecute(\n      \"\"\"\n      INSERT INTO products (name, price, stock)\n      VALUES ('Laptop', 999.99, 10)\n      \"\"\".trimIndent()\n    )\n\n    // Update data\n    shouldExecute(\"UPDATE products SET stock = 5 WHERE name = 'Laptop'\")\n\n    // Delete data\n    shouldExecute(\"DELETE FROM products WHERE stock = 0\")\n  }\n}\n```\n\n### Querying Data\n\nQuery data with type-safe mappers:\n\n```kotlin hl_lines=\"4 12 17\"\ndata class Product(\n  val id: Long,\n  val name: String,\n  val price: Double,\n  val stock: Int\n)\n\nstove {\n  mysql {\n    shouldQuery<Product>(\n      query = \"SELECT * FROM products WHERE price > 500\",\n      mapper = { row ->\n        Product(\n          id = row.long(\"id\"),\n          name = row.string(\"name\"),\n          price = row.double(\"price\"),\n          stock = row.int(\"stock\")\n        )\n      }\n    ) { products ->\n      products.size shouldBeGreaterThan 0\n      products.all { it.price > 500 } shouldBe true\n    }\n  }\n}\n```\n\n### Query with Parameters\n\nUse parameterized queries for safety:\n\n```kotlin\nstove {\n  mysql {\n    val minPrice = 100.0\n    shouldQuery<Product>(\n      query = \"SELECT * FROM products WHERE price >= ?\",\n      mapper = { row ->\n        Product(\n          id = row.long(\"id\"),\n          name = row.string(\"name\"),\n          price = row.double(\"price\"),\n          stock = row.int(\"stock\")\n        )\n      }\n    ) { products ->\n      products.all { it.price >= minPrice } shouldBe true\n    }\n  }\n}\n```\n\n### Working with Nullable Fields\n\nHandle nullable columns:\n\n```kotlin\ndata class User(\n  val id: Long,\n  val name: String,\n  val email: String?,\n  val phone: String?\n)\n\nstove {\n  mysql {\n    shouldQuery<User>(\n      query = \"SELECT * FROM users\",\n      mapper = { row ->\n        User(\n          id = row.long(\"id\"),\n          name = row.string(\"name\"),\n          email = row.stringOrNull(\"email\"),\n          phone = row.stringOrNull(\"phone\")\n        )\n      }\n    ) { users ->\n      users.size shouldBeGreaterThan 0\n    }\n  }\n}\n```\n\n## Provided Instance (External MySQL)\n\n<span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">For CI/CD pipelines or shared infrastructure</span>, connect to an existing MySQL instance instead of starting a container:\n\n```kotlin\nStove()\n  .with {\n    mysql {\n      MySqlOptions.provided(\n        jdbcUrl = \"jdbc:mysql://localhost:3306/testdb\",\n        host = \"localhost\",\n        port = 3306,\n        databaseName = \"testdb\",\n        username = \"root\",\n        password = \"password\",\n        runMigrations = true,\n        cleanup = { operations ->\n          operations.execute(\"DELETE FROM users WHERE email LIKE '%@test.com'\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.datasource.url=${cfg.jdbcUrl}\",\n            \"spring.datasource.username=${cfg.username}\",\n            \"spring.datasource.password=${cfg.password}\"\n          )\n        }\n      )\n    }\n  }\n  .run()\n```\n\nSee [Provided Instances](11-provided-instances.md) for detailed documentation on all supported systems and test isolation strategies.\n"
  },
  {
    "path": "docs/Components/17-cassandra.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Cassandra</span>\n\n=== \"Gradle\"\n\n    ``` kotlin\n        dependencies {\n            testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n            testImplementation(\"com.trendyol:stove-cassandra\")\n        }\n    ```\n\n## Configure\n\nOnce you've added the dependency, you'll have access to the `cassandra` function when configuring Stove.\nThis function configures the Cassandra Docker container that will be started for tests.\n\n```kotlin hl_lines=\"4 6-10\"\nStove()\n  .with {\n    cassandra {\n      CassandraSystemOptions(\n        keyspace = \"my_keyspace\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\",\n            \"spring.cassandra.local-datacenter=${cfg.datacenter}\",\n            \"spring.cassandra.keyspace-name=${cfg.keyspace}\"\n          )\n        }\n      )\n    }\n  }.run()\n```\n\nThe `cfg` reference gives you access to the Cassandra container's connection details, which you can pass to your application.\n\n### Container Options\n\nCustomize the Cassandra container version and configuration:\n\n```kotlin\nStove()\n  .with {\n    cassandra {\n      CassandraSystemOptions(\n        keyspace = \"my_keyspace\",\n        datacenter = \"datacenter1\",\n        container = CassandraContainerOptions(\n          registry = \"docker.io\",\n          image = \"cassandra\",\n          tag = \"4.1\",\n          containerFn = { container ->\n            // Additional container configuration\n            container.withEnv(\"CASSANDRA_CLUSTER_NAME\", \"test-cluster\")\n          }\n        ),\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\",\n            \"spring.cassandra.local-datacenter=${cfg.datacenter}\",\n            \"spring.cassandra.keyspace-name=${cfg.keyspace}\"\n          )\n        }\n      )\n    }\n  }.run()\n```\n\n### Cleanup\n\nUse the `cleanup` lambda to truncate tables or delete data between test runs:\n\n```kotlin\nStove()\n  .with {\n    cassandra {\n      CassandraSystemOptions(\n        keyspace = \"my_keyspace\",\n        cleanup = { session ->\n          session.execute(\"TRUNCATE my_keyspace.users\")\n          session.execute(\"TRUNCATE my_keyspace.events\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\",\n            \"spring.cassandra.local-datacenter=${cfg.datacenter}\",\n            \"spring.cassandra.keyspace-name=${cfg.keyspace}\"\n          )\n        }\n      )\n    }\n  }.run()\n```\n\n## Migrations\n\nStove provides a way to run CQL migrations before tests start.\nUse this to create keyspaces, tables, indexes, and seed data.\n\n```kotlin\nclass CreateKeyspaceMigration : CassandraMigration {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: CassandraMigrationContext) {\n    connection.session.execute(\n      \"\"\"\n      CREATE KEYSPACE IF NOT EXISTS ${connection.options.keyspace}\n        WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}\n      \"\"\".trimIndent()\n    )\n  }\n}\n\nclass CreateTablesMigration : CassandraMigration {\n  override val order: Int = 2\n\n  override suspend fun execute(connection: CassandraMigrationContext) {\n    connection.session.execute(\n      \"\"\"\n      CREATE TABLE IF NOT EXISTS ${connection.options.keyspace}.users (\n        id uuid PRIMARY KEY,\n        name text,\n        email text,\n        created_at timestamp\n      )\n      \"\"\".trimIndent()\n    )\n  }\n}\n```\n\nRegister migrations in your Stove configuration:\n\n```kotlin hl_lines=\"9-12\"\nStove()\n  .with {\n    cassandra {\n      CassandraSystemOptions(\n        keyspace = \"my_keyspace\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\")\n        }\n      ).migrations {\n        register<CreateKeyspaceMigration>()\n        register<CreateTablesMigration>()\n      }\n    }\n  }.run()\n```\n\nMigrations are executed in ascending `order` and are skipped on subsequent test runs (container reuse) unless `runMigrationsAlways` is enabled.\n\n## Usage\n\n### Executing CQL Statements\n\nExecute any DDL or DML statement:\n\n```kotlin hl_lines=\"3 8 13\"\nstove {\n  cassandra {\n    // Create a table\n    shouldExecute(\n      \"CREATE TABLE IF NOT EXISTS my_keyspace.products (id uuid PRIMARY KEY, name text, price decimal)\"\n    )\n\n    // Insert data\n    shouldExecute(\n      \"INSERT INTO my_keyspace.products (id, name, price) VALUES (uuid(), 'Laptop', 999.99)\"\n    )\n\n    // Delete data\n    shouldExecute(\"DELETE FROM my_keyspace.products WHERE name = 'Laptop'\")\n  }\n}\n```\n\n### Querying Data\n\nExecute a CQL query and assert on the returned `ResultSet`:\n\n```kotlin hl_lines=\"3 5\"\nstove {\n  cassandra {\n    shouldQuery(\"SELECT * FROM my_keyspace.products\") { resultSet ->\n      val rows = resultSet.all()\n      rows.isNotEmpty() shouldBe true\n      rows.first().getString(\"name\") shouldBe \"Laptop\"\n    }\n  }\n}\n```\n\n### Prepared Statements\n\nUse prepared (bound) statements for parameterized queries:\n\n```kotlin hl_lines=\"6 11\"\nstove {\n  cassandra {\n    // Prepare a statement using the raw session\n    val prepared = session().prepare(\n      \"INSERT INTO my_keyspace.users (id, name, email) VALUES (?, ?, ?)\"\n    )\n    val bound = prepared.bind(java.util.UUID.randomUUID(), \"Jane Doe\", \"jane@example.com\")\n\n    // Execute the bound statement\n    shouldExecute(bound)\n\n    // Query with a bound statement\n    val selectPrepared = session().prepare(\n      \"SELECT * FROM my_keyspace.users WHERE id = ?\"\n    )\n    shouldQuery(selectPrepared.bind(bound.getUuid(0))) { resultSet ->\n      val row = resultSet.one()\n      row?.getString(\"name\") shouldBe \"Jane Doe\"\n    }\n  }\n}\n```\n\n### Direct Session Access\n\nAccess the raw `CqlSession` for advanced operations not covered by the DSL:\n\n```kotlin hl_lines=\"4\"\nstove {\n  cassandra {\n    // Use session() for operations outside the DSL\n    val result = session().execute(\"SELECT release_version FROM system.local\")\n    val version = result.one()?.getString(\"release_version\")\n    version shouldNotBe null\n  }\n}\n```\n\n### Pause and Unpause Container\n\nTest resilience scenarios by pausing the Cassandra container:\n\n```kotlin hl_lines=\"5 10\"\nstove {\n  cassandra {\n    // Verify database is reachable\n    shouldQuery(\"SELECT * FROM my_keyspace.users\") { it.all().size shouldBeGreaterThanOrEqual 0 }\n\n    // Pause the container to simulate an outage\n    pause()\n\n    // Your application should handle the failure gracefully\n    // ...\n\n    // Restore the container\n    unpause()\n\n    // Verify recovery\n    shouldQuery(\"SELECT * FROM my_keyspace.users\") { it.all().size shouldBeGreaterThanOrEqual 0 }\n  }\n}\n```\n\n!!! note\n    `pause()` and `unpause()` are only supported in container mode. They are ignored (with a warning) when using a [provided instance](#provided-instances).\n\n## Complete Example\n\nHere's a <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">complete end-to-end test</span>:\n\n```kotlin hl_lines=\"7 12 22 30\"\ntest(\"should create user via API and verify in Cassandra\") {\n  stove {\n    val userName = \"John Doe\"\n    val userEmail = \"john@example.com\"\n\n    // Create user via API\n    http {\n      postAndExpectBody<UserResponse>(\n        uri = \"/users\",\n        body = CreateUserRequest(name = userName, email = userEmail).some()\n      ) { response ->\n        response.status shouldBe 201\n      }\n    }\n\n    // Verify user event was published\n    kafka {\n      shouldBePublished<UserCreatedEvent>(atLeastIn = 10.seconds) {\n        actual.name == userName && actual.email == userEmail\n      }\n    }\n\n    // Verify user was stored in Cassandra\n    cassandra {\n      shouldQuery(\n        \"SELECT * FROM my_keyspace.users WHERE email = '$userEmail' ALLOW FILTERING\"\n      ) { resultSet ->\n        val rows = resultSet.all()\n        rows shouldHaveSize 1\n        rows.first().getString(\"name\") shouldBe userName\n      }\n    }\n  }\n}\n```\n\n## Provided Instances\n\nConnect to an externally running Cassandra instance instead of a testcontainer.\nThis is useful when Docker is unavailable or you want to use a shared cluster.\n\n```kotlin hl_lines=\"4-11\"\nStove()\n  .with {\n    cassandra {\n      CassandraSystemOptions.provided(\n        host = \"localhost\",\n        port = 9042,\n        datacenter = \"datacenter1\",\n        keyspace = \"my_keyspace\",\n        runMigrations = true,\n        cleanup = { session ->\n          session.execute(\"TRUNCATE my_keyspace.users\")\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\",\n            \"spring.cassandra.local-datacenter=${cfg.datacenter}\",\n            \"spring.cassandra.keyspace-name=${cfg.keyspace}\"\n          )\n        }\n      ).migrations {\n        register<CreateKeyspaceMigration>()\n        register<CreateTablesMigration>()\n      }\n    }\n  }.run()\n```\n\nSee [Provided Instances](11-provided-instances.md) for more details on connecting to existing infrastructure.\n\n## Spring Boot Integration\n\nWhen using Spring Boot Data Cassandra, map the exposed configuration to Spring properties:\n\n```kotlin\nCassandraSystemOptions(\n  keyspace = \"my_keyspace\",\n  configureExposedConfiguration = { cfg ->\n    listOf(\n      \"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\",\n      \"spring.cassandra.local-datacenter=${cfg.datacenter}\",\n      \"spring.cassandra.keyspace-name=${cfg.keyspace}\",\n      \"spring.cassandra.schema-action=CREATE_IF_NOT_EXISTS\"\n    )\n  }\n)\n```\n\nFor `application.yml`-based configuration:\n\n```yaml\nspring:\n  cassandra:\n    contact-points: \"${CASSANDRA_HOST}:${CASSANDRA_PORT}\"\n    local-datacenter: datacenter1\n    keyspace-name: my_keyspace\n```\n"
  },
  {
    "path": "docs/Components/18-dashboard.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Dashboard</span>\n\nYour end-to-end tests pass. But do you *see* what they do?\n\nStove Dashboard is a <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">local observability dashboard</span> for your e2e test runs.\n\n- **Captures everything** — HTTP calls, Kafka messages, database queries, gRPC calls, distributed traces, system snapshots\n- **Real-time web UI** — updates live via SSE as your tests execute\n- **Single binary** — receives events via gRPC, persists in SQLite, serves an embedded SPA\n- **Persistent** — browse test runs after they complete, across sessions\n- **Agent API** — exposes a local read-only MCP endpoint for compact failed-test evidence\n\nUnlike [Reporting](13-reporting.md) (console output on failure) and [Tracing](15-tracing.md) (span collection for assertions), Dashboard gives you a <span data-rn=\"underline\" data-rn-color=\"#009688\">persistent, browsable view</span> of your test runs — including successful ones.\n\n## Install the CLI\n\n=== \"Homebrew (macOS)\"\n\n    ```bash\n    brew install Trendyol/trendyol-tap/stove\n    ```\n\n=== \"Shell Script (macOS & Linux)\"\n\n    ```bash\n    curl -fsSL https://raw.githubusercontent.com/Trendyol/stove/main/tools/stove-cli/install.sh | sh\n    ```\n\n    Options:\n\n    ```bash\n    # Install a specific version\n    curl -fsSL ... | sh -s -- --version 0.23.0\n\n    # Install to a custom directory\n    curl -fsSL ... | sh -s -- --dir /usr/local/bin\n    ```\n\n=== \"Manual Download\"\n\n    Download the binary for your platform from [GitHub Releases](https://github.com/Trendyol/stove/releases):\n\n    | Platform    | Archive                                      |\n    |-------------|----------------------------------------------|\n    | macOS arm64 | `stove-<version>-darwin-arm64.tar.gz` |\n    | macOS amd64 | `stove-<version>-darwin-amd64.tar.gz` |\n    | Linux amd64 | `stove-<version>-linux-amd64.tar.gz`  |\n\n    Each archive includes a `.sha256` checksum file.\n\nThe CLI is a single binary with no runtime dependencies. It embeds the web UI, so there's nothing else to install.\n\n!!! info \"Keep Versions Aligned\"\n    Keep `stove-cli`, the Stove BOM, and your Stove test dependencies on the same Stove version. The dashboard shows a warning when the runtime libraries and CLI drift apart, but matching versions avoids inconsistent dashboard data.\n\n## Quick Start\n\n**1. Start the dashboard**\n\n```bash\nstove\n```\n\nYou'll see:\n\n```\nStove CLI v0.23.0 running\nUI:   http://localhost:4040\nREST: http://localhost:4040/api/v1\nMCP:  http://localhost:4040/mcp\ngRPC: localhost:4041\n```\n\n**2. Add the dependency**\n\n=== \"Gradle\"\n\n    ```kotlin hl_lines=\"3-4\"\n    dependencies {\n        testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n        testImplementation(\"com.trendyol:stove-dashboard\")\n        testImplementation(\"com.trendyol:stove-tracing\")\n    }\n    ```\n\n=== \"Maven\"\n\n    ```xml hl_lines=\"3-6\"\n    <dependency>\n        <groupId>com.trendyol</groupId>\n        <artifactId>stove-dashboard</artifactId>\n        <scope>test</scope>\n    </dependency>\n    ```\n\n**3. Apply the tracing Gradle plugin**\n\nThe tracing Gradle plugin attaches the OpenTelemetry agent to your test tasks, which is required for the dashboard's trace view to receive spans.\n\n```kotlin\n// build.gradle.kts\nplugins {\n    id(\"com.trendyol.stove.tracing\") version \"<stove-version>\"\n}\n\nstoveTracing {\n    serviceName.set(\"product-api\")\n}\n```\n\nSee [Tracing](15-tracing.md) for the full plugin configuration reference.\n\n**4. Register in your Stove config**\n\n=== \"Kotest\"\n\n    ```kotlin hl_lines=\"2 6-7\"\n    class StoveConfig : AbstractProjectConfig() {\n      override val extensions = listOf(StoveKotestExtension())\n\n      override suspend fun beforeProject() =\n        Stove().with {\n          dashboard { DashboardSystemOptions(appName = \"product-api\") }\n          tracing { enableSpanReceiver() }  // recommended: enables distributed trace capture\n          // ... other systems\n        }.run()\n\n      override suspend fun afterProject() = Stove.stop()\n    }\n    ```\n\n=== \"JUnit\"\n\n    ```kotlin hl_lines=\"1\"\n    @ExtendWith(StoveJUnitExtension::class)\n    @TestInstance(TestInstance.Lifecycle.PER_CLASS)\n    abstract class BaseE2ETest {\n      companion object {\n        @JvmStatic @BeforeAll\n        fun setup() = runBlocking {\n          Stove().with {\n            dashboard { DashboardSystemOptions(appName = \"product-api\") }\n            tracing { enableSpanReceiver() }\n            // ... other systems\n          }.run()\n        }\n\n        @JvmStatic @AfterAll\n        fun teardown() = runBlocking { Stove.stop() }\n      }\n    }\n    ```\n\n**5. Run your tests and open the dashboard**\n\n```bash\n./gradlew test\n```\n\nNavigate to [http://localhost:4040](http://localhost:4040). The UI updates in real time as tests execute.\n\nIf the dashboard shows a version mismatch warning, align your Stove BOM and test dependencies with the `stove-cli` version, or upgrade `stove-cli` to the runtime version reported by the selected app.\n\nAI agents can connect to the local MCP endpoint shown in the startup output. See [MCP](21-mcp.md) for the tool list and fallback behavior.\n\n## What Gets Captured\n\nOnce `dashboard {}` is registered, Stove <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">automatically captures everything</span> — no code changes to your tests:\n\n| Event              | Data                                                         |\n|--------------------|--------------------------------------------------------------|\n| **Run lifecycle**  | Start/end timestamps, app name, active systems, pass/fail counts |\n| **Test lifecycle** | Test name, spec name, duration, status, error messages       |\n| **Entries**        | Every `http {}`, `kafka {}`, `postgresql {}` assertion — system, action, input/output, expected/actual, trace ID |\n| **Spans**          | Distributed traces via OpenTelemetry — operation, service, duration, attributes, exceptions |\n| **Snapshots**      | System state at test boundaries — database contents, Kafka offsets, WireMock stubs |\n\n## The Dashboard\n\nThe embedded SPA provides four views for each test:\n\n### Timeline\n\nChronological list of every action the test performed. Each entry shows timestamp, system badge (color-coded), action name, and pass/fail indicator. Click any entry to expand full detail: input, output, expected vs. actual, error, metadata.\n\nRecognized systems: <span data-rn=\"highlight\" data-rn-color=\"#00968855\">HTTP, Kafka, PostgreSQL, MongoDB, Couchbase, Redis, Elasticsearch, WireMock, gRPC, MySQL, MSSQL, Cassandra</span>.\n\n### Trace\n\nDistributed trace tree built from OpenTelemetry spans. Spans are linked to tests via two mechanisms:\n\n- **Entry-based**: spans sharing a `trace_id` with a test entry\n- **Attribute-based**: spans containing `x-stove-test-id` in their attributes\n\nThe tree shows operation name, service, duration, status, relevant attributes (`http.*`, `db.*`, `messaging.*`, `rpc.*`), and exception details with stack traces.\n\n!!! tip \"Combine with Tracing\"\n    Dashboard's trace view is the visual counterpart to the [Tracing](15-tracing.md) component's console output. Enable both for the best experience: Tracing gives you assertion DSL and failure reports in the terminal, Dashboard gives you a browsable trace tree in the browser.\n\n### Snapshots\n\nGrid of system state cards captured at test boundaries. Each card shows the system name with a color-coded icon and a summary of the captured state.\n\n### Kafka Explorer\n\nDedicated view filtering Kafka-specific entries. Shows consumed/published/failed message counts with expandable JSON payloads.\n\n## Configuration\n\n### DashboardSystemOptions\n\n```kotlin\nDashboardSystemOptions(\n  appName = \"product-api\",     // required: identifies the application under test\n  cliHost = \"localhost\",       // where the stove CLI is running\n  cliPort = 4041               // gRPC port of the stove CLI\n)\n```\n\n| Parameter | Type     | Default       | Description                                |\n|-----------|----------|---------------|--------------------------------------------|\n| `appName` | `String` | *(required)*  | Application name for grouping test runs    |\n| `cliHost` | `String` | `\"localhost\"` | Hostname where `stove` CLI is running      |\n| `cliPort` | `Int`    | `4041`        | gRPC port where `stove` CLI is listening   |\n\n### CLI Options\n\n```\nstove [OPTIONS]\n\nOptions:\n  --port <PORT>          HTTP port for the web UI and REST API [default: 4040]\n  --grpc-port <PORT>     gRPC port for receiving events [default: 4041]\n  --db <PATH>            Path to SQLite database file [default: ~/.stove-dashboard.db]\n  --clear                Clear all stored data and exit\n  --fresh-start          Back up and recreate the database, then start normally\n  -h, --help             Print help\n  -V, --version          Print version\n```\n\n```bash\n# Run on custom ports\nstove --port 8080 --grpc-port 8081\n\n# Use a project-specific database\nstove --db ./my-project-dashboard.db\n\n# Reset all data (exits after clearing)\nstove --clear\n\n# Drop and recreate the database (backs up first, then starts servers)\nstove --fresh-start\n```\n\n## Fault Tolerance\n\nThe dashboard emitter is designed to <span data-rn=\"underline\" data-rn-color=\"#009688\">never break your tests</span>:\n\n- Non-blocking event queue (capacity: 512)\n- Auto-disables after 5 consecutive gRPC failures\n- 3-second drain timeout on shutdown\n- If the dashboard CLI is not running, tests continue normally with zero overhead\n\nThis means you can add `dashboard {}` to your config permanently. When the CLI is running, you get the dashboard. When it's not, nothing changes.\n\n## REST API\n\nThe dashboard exposes a REST API at `/api/v1` for programmatic access:\n\n| Method | Path                                         | Description                    |\n|--------|----------------------------------------------|--------------------------------|\n| GET    | `/meta`                                      | CLI version and MCP discovery metadata |\n| GET    | `/apps`                                      | List applications with latest run info |\n| GET    | `/runs?app={name}`                           | List runs, optionally filtered by app  |\n| GET    | `/runs/{run_id}`                             | Get a specific run             |\n| GET    | `/runs/{run_id}/tests`                       | List tests in a run            |\n| GET    | `/runs/{run_id}/tests/{test_id}/entries`     | List entries for a test        |\n| GET    | `/runs/{run_id}/tests/{test_id}/spans`       | List spans linked to a test    |\n| GET    | `/runs/{run_id}/tests/{test_id}/snapshots`   | List snapshots for a test      |\n| GET    | `/traces/{trace_id}`                         | Get all spans in a trace       |\n| GET    | `/events/stream`                             | SSE stream for real-time events |\n\nFor AI agents, prefer the MCP endpoint at `/mcp` over the REST API when the task is failed-test triage. MCP returns compact, scoped evidence and ready-to-use follow-up tool calls. See [MCP](21-mcp.md) for setup and fallback behavior.\n\n### SSE Events\n\nThe `/events/stream` endpoint delivers server-sent events with JSON payloads:\n\n```json\n{\"run_id\": \"abc-123\", \"event_type\": \"test_ended\"}\n```\n\nEvent types: `run_started`, `run_ended`, `test_started`, `test_ended`, `entry_recorded`, `span_recorded`, `snapshot`.\n\n## Complete Example\n\n```kotlin hl_lines=\"7-8\"\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject() =\n    Stove()\n      .with {\n        dashboard { DashboardSystemOptions(appName = \"spring-example\") }\n        tracing { enableSpanReceiver() }\n        httpClient {\n          HttpClientSystemOptions(baseUrl = \"http://localhost:$appPort\")\n        }\n        postgresql {\n          PostgresqlOptions(databaseName = \"stove\", configureExposedConfiguration = { cfg ->\n            listOf(\"spring.datasource.url=${cfg.jdbcUrl}\")\n          })\n        }\n        kafka {\n          KafkaSystemOptions(configureExposedConfiguration = {\n            listOf(\"kafka.bootstrapServers=${it.bootstrapServers}\")\n          })\n        }\n        springBoot(runner = { params -> run(params) { addTestSystemDependencies() } })\n      }.run()\n\n  override suspend fun afterProject() = Stove.stop()\n}\n```\n\nThen write tests as usual — the dashboard captures everything automatically:\n\n```kotlin\ntest(\"should create order and publish event\") {\n  stove {\n    http {\n      postAndExpectBodilessResponse(\"/orders\", body = CreateOrderRequest(orderId).some()) {\n        it.status shouldBe 201\n      }\n    }\n\n    kafka {\n      shouldBePublished<OrderCreatedEvent> {\n        actual.orderId == orderId\n      }\n    }\n\n    postgresql {\n      shouldQuery<Order>(\"SELECT * FROM orders WHERE id = '$orderId'\") {\n        it.first().status shouldBe \"CREATED\"\n      }\n    }\n  }\n}\n```\n\nOpen [http://localhost:4040](http://localhost:4040) to see every HTTP request, Kafka message, database query, and distributed trace — in real time.\n\n## How It Relates to Reporting and Tracing\n\nDashboard, [Reporting](13-reporting.md), and [Tracing](15-tracing.md) are complementary:\n\n| Feature | Reporting | Tracing | Dashboard |\n|---------|-----------|---------|--------|\n| When | On test failure | On test failure | Always (real-time) |\n| Where | Console/CI output | Console/CI output | Browser UI |\n| What | Test actions + assertions | Application call chain | Everything + history |\n| Persistence | None (ephemeral) | None (ephemeral) | SQLite (across runs) |\n\n<span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">Use all three together</span> for the best experience:\n\n- **Reporting** gives you immediate feedback in the terminal when something breaks\n- **Tracing** gives you the execution trace and assertion DSL in your test code\n- **Dashboard** gives you a browsable, persistent view of all test runs — successful and failed\n\n## Troubleshooting\n\n### Dashboard UI shows \"Waiting for test events...\"\n\n1. Verify the `stove` CLI is running: `stove --version`\n2. Check that gRPC ports match: CLI default is `4041`, Kotlin default is `4041`\n3. Look for connection errors in the CLI's terminal output\n\n### Tests run fine but nothing appears in Dashboard\n\n1. Ensure `dashboard {}` is registered in your Stove config\n2. Verify `stove-dashboard` is in your test dependencies\n3. Check that the CLI started *before* running tests\n\n### Dashboard works locally but not in CI\n\nDashboard is designed for <span data-rn=\"underline\" data-rn-color=\"#009688\">local development</span>. In CI, use [Reporting](13-reporting.md) and [Tracing](15-tracing.md) for failure diagnostics — they output to the console and don't require a running server.\n\n### Data from previous runs clutters the UI\n\n```bash\nstove --clear\n```\n\nThis wipes the SQLite database and exits. Start the CLI again for a clean slate.\n\n### Database schema is corrupted or migrations fail\n\n```bash\nstove --fresh-start\n```\n\nThis backs up the existing database (printing the backup path), deletes it, and recreates a fresh one with all migrations applied. The servers start normally after — no need to run `stove` again.\n"
  },
  {
    "path": "docs/Components/19-provided-application.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Provided Application</span> (Black-Box Testing)\n\nStove normally starts the application under test locally via a framework starter (`springBoot()`, `ktor()`, etc.). With `providedApplication()`, you can skip that entirely and <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">test against a remote, already-deployed application</span> --- regardless of what language or framework it's built with.\n\n## When to Use\n\n- **Staging/pre-production validation** --- verify deployed services before release\n- **Polyglot testing** --- the app can be Go, Python, .NET, Rust, Node.js, or anything else\n- **Microservice integration** --- test a service through its public API and verify side effects in databases, Kafka, and caches\n- **Smoke testing** --- run Stove tests as post-deployment checks in CI/CD\n\n## Configure\n\n`providedApplication()` replaces the framework starter (`springBoot()`, `ktor()`, etc.) in the `with` block. HTTP, databases, and other systems are configured separately as usual.\n\n```kotlin hl_lines=\"3 8-13\"\nStove().with {\n    // Your app's API --- configured via httpClient as usual\n    httpClient {\n        HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\")\n    }\n\n    // Signal: app is already running, don't start it\n    providedApplication {\n        ProvidedApplicationOptions(\n            readiness = ReadinessStrategy.HttpGet(\n                url = \"https://staging.myapp.com/actuator/health\"\n            )\n        )\n    }\n}.run()\n```\n\n### Health Check\n\nThe optional readiness check verifies the remote application is reachable before tests run. If the check fails after all retries, Stove throws immediately with a clear error.\n\n```kotlin\nReadinessStrategy.HttpGet(\n    url = \"https://staging.myapp.com/health\",   // Health endpoint URL\n    timeout = 30.seconds,                        // HTTP request timeout\n    retries = 10,                                // Number of retry attempts\n    retryDelay = 1.seconds,                      // Delay between retries\n    expectedStatusCodes = setOf(200)              // Status codes considered healthy\n)\n```\n\n### No Health Check\n\nIf you're sure the app is up, skip the readiness check entirely:\n\n```kotlin\nprovidedApplication()  // No-op --- just satisfies the AUT requirement\n```\n\n## Complete Example\n\n```kotlin hl_lines=\"4 11-14 17-24 27\"\nclass TestConfig : AbstractProjectConfig() {\n    override suspend fun beforeProject() {\n        Stove().with {\n            // The app itself\n            httpClient {\n                HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\")\n            }\n\n            // Infrastructure the app connects to\n            postgresql {\n                PostgresqlOptions.provided(\n                    jdbcUrl = \"jdbc:postgresql://staging-db:5432/myapp\",\n                    host = \"staging-db\", port = 5432,\n                    configureExposedConfiguration = { emptyList() }\n                )\n            }\n\n            kafka {\n                KafkaSystemOptions.provided(\n                    bootstrapServers = \"staging-kafka:9092\",\n                    configureExposedConfiguration = { emptyList() }\n                )\n            }\n\n            // App is already deployed\n            providedApplication {\n                ProvidedApplicationOptions(\n                    readiness = ReadinessStrategy.HttpGet(\n                        url = \"https://staging.myapp.com/actuator/health\",\n                        timeout = 15.seconds\n                    )\n                )\n            }\n        }.run()\n    }\n\n    override suspend fun afterProject() = Stove.stop()\n}\n```\n\n## Writing Tests\n\nTests are written exactly the same way --- the DSL doesn't change:\n\n```kotlin\ntest(\"create order and verify side effects\") {\n    stove {\n        http {\n            postAndExpectJson<OrderResponse>(\"/orders\", body = request.some()) { order ->\n                order.id shouldNotBe null\n            }\n        }\n\n        postgresql {\n            shouldQuery<Order>(\"SELECT * FROM orders WHERE id = ?\", listOf(orderId)) { rows ->\n                rows shouldHaveSize 1\n            }\n        }\n\n        kafka {\n            // Use consumer() to read directly from topics (no interceptor needed)\n            consumer<String, OrderCreatedEvent>(\"orders.output\", readOnly = true) { record ->\n                record.value().orderId shouldBe orderId\n            }\n        }\n    }\n}\n```\n\n## Limitations\n\n| Feature | Available? | Notes |\n|---------|-----------|-------|\n| HTTP/gRPC assertions | Yes | Via `httpClient {}` and `grpc {}` |\n| Database queries | Yes | Via `postgresql {}`, `mongodb {}`, etc. |\n| Kafka publish + consumer | Yes | `publish()` and `consumer()` work directly |\n| Kafka `shouldBeConsumed` | No | Requires interceptor bridge inside the app |\n| `using<T> {}` (Bridge) | No | Remote app's DI container is not accessible |\n\n!!! warning \"Bridge Not Supported\"\n    `using<MyService> { }` accesses the application's DI container, which is only possible when the app runs in the same JVM. With `providedApplication()`, calling `using<T>` throws a clear error explaining this.\n\n## Suggested Source Set\n\nFor projects that have both local e2e tests and black-box tests against deployed apps:\n\n```\nmy-service/\n  src/\n    main/           # Application code\n    test/            # Unit tests\n    test-e2e/        # Stove e2e tests (app started locally)\n    test-blackbox/   # Stove black-box tests (providedApplication)\n```\n\nSee also: [Multiple Systems](20-multiple-systems.md) for testing against multiple named service instances.\n"
  },
  {
    "path": "docs/Components/20-multiple-systems.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Multiple Systems</span>\n\nBy default, Stove registers one instance per system type --- one PostgreSQL, one Kafka, one HTTP client. With multiple systems, you can register <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">multiple instances of the same system type</span>, each identified by a typed key.\n\n## When to Use\n\n- **Microservice integration** --- call multiple services, each with its own HTTP client or gRPC stub\n- **Multiple databases** --- verify state in separate PostgreSQL or MongoDB instances\n- **Multi-cluster Kafka** --- publish/consume from different Kafka clusters\n- **Cross-service verification** --- after calling your app, check that dependent services received the right data\n\n## Define Keys\n\nKeys are Kotlin singleton objects implementing `SystemKey`:\n\n```kotlin\nobject OrderService : SystemKey\nobject PaymentService : SystemKey\nobject AppDb : SystemKey\nobject AnalyticsDb : SystemKey\n```\n\n!!! tip \"Why Objects Instead of Strings?\"\n    Kotlin objects give you compile-time safety, IDE autocomplete, and refactor-safe references. Typos become compile errors. The same key can be reused across protocols --- `httpClient(PaymentService)` and `grpc(PaymentService)` both refer to the same logical service.\n\n## Configure\n\nPass the key as the first argument to any system DSL function:\n\n```kotlin hl_lines=\"3 8 13 18\"\nStove().with {\n    // Default HTTP client --- your app\n    httpClient {\n        HttpClientSystemOptions(baseUrl = \"https://myapp.staging.com\")\n    }\n\n    // Keyed HTTP clients --- dependent services\n    httpClient(OrderService) {\n        HttpClientSystemOptions(baseUrl = \"https://order.internal.com\")\n    }\n    httpClient(PaymentService) {\n        HttpClientSystemOptions(baseUrl = \"https://payment.internal.com\")\n    }\n\n    // Keyed databases\n    postgresql(AppDb) {\n        PostgresqlOptions.provided(\n            jdbcUrl = \"jdbc:postgresql://staging-db:5432/myapp\",\n            host = \"staging-db\", port = 5432,\n            configureExposedConfiguration = { emptyList() }\n        )\n    }\n    postgresql(AnalyticsDb) {\n        PostgresqlOptions.provided(\n            jdbcUrl = \"jdbc:postgresql://analytics-db:5432/analytics\",\n            host = \"analytics-db\", port = 5432,\n            configureExposedConfiguration = { emptyList() }\n        )\n    }\n\n    providedApplication()\n}.run()\n```\n\n## Write Tests\n\nPass the key to the validation DSL:\n\n```kotlin hl_lines=\"5 12 19 26\"\ntest(\"create order, verify across services and databases\") {\n    stove {\n        // Call the app (default HTTP --- no key)\n        http {\n            postAndExpectJson<OrderResponse>(\"/orders\", body = request.some()) { order ->\n                order.id shouldNotBe null\n            }\n        }\n\n        // Verify order service received the order\n        http(OrderService) {\n            getResponse(\"/api/orders/$orderId\") { resp ->\n                resp.status shouldBe 200\n            }\n        }\n\n        // Verify payment was processed\n        http(PaymentService) {\n            getResponse(\"/api/payments?orderId=$orderId\") { resp ->\n                resp.status shouldBe 200\n            }\n        }\n\n        // Verify in app's database\n        postgresql(AppDb) {\n            shouldQuery<Order>(\"SELECT * FROM orders WHERE id = ?\", listOf(orderId)) { rows ->\n                rows shouldHaveSize 1\n            }\n        }\n\n        // Verify analytics event landed\n        postgresql(AnalyticsDb) {\n            shouldQuery<AnalyticsEvent>(\"SELECT * FROM events WHERE order_id = ?\", listOf(orderId)) { events ->\n                events.first().type shouldBe \"ORDER_CREATED\"\n            }\n        }\n    }\n}\n```\n\n## Supported Systems\n\nAll multi-instance dependency systems support keyed registration:\n\n| Category | Systems |\n|----------|---------|\n| **Databases** | PostgreSQL, MySQL, MSSQL, MongoDB, Cassandra, Couchbase, Redis, Elasticsearch |\n| **Protocol clients** | HTTP Client, gRPC |\n| **Messaging** | Kafka |\n| **Mocking** | WireMock, gRPC Mock |\n\nSingle-instance systems (Bridge, Tracing, Dashboard) and framework starters (`springBoot()`, `ktor()`) do **not** support keyed registration --- there is only one application under test.\n\n!!! info \"Spring Kafka\"\n    The Spring Kafka starter (`stove-spring-kafka`) does not support keyed instances because it is tied to a single Spring application context. Use the standalone `stove-kafka` module if you need multiple Kafka instances.\n\n## Default and Keyed Coexist\n\nDefault (unkeyed) and keyed instances of the same type are independent:\n\n```kotlin\n// Registration\nhttpClient { HttpClientSystemOptions(baseUrl = \"https://myapp.com\") }      // default\nhttpClient(OrderService) { HttpClientSystemOptions(baseUrl = \"https://order.internal.com\") }  // keyed\n\n// Validation\nhttp { /* hits myapp.com */ }              // default\nhttp(OrderService) { /* hits order.internal.com */ }  // keyed\n```\n\n## Reporting\n\nKeyed systems produce distinguishable names in reports and traces:\n\n```\nHTTP > GET /orders                          # default\nHTTP [OrderService] > GET /api/orders/123   # keyed\nHTTP [PaymentService] > GET /api/payments   # keyed\nPostgreSQL [AppDb] > shouldQuery > SELECT   # keyed\nPostgreSQL [AnalyticsDb] > shouldQuery      # keyed\n```\n\n## Error Handling\n\nIf you pass a key that wasn't registered, you get a clear runtime error:\n\n```\nSystemNotRegisteredException: HttpSystem was not registered.\nNo HttpSystem registered with key 'OrderService'\n```\n\n## Combining with Provided Application\n\nKeyed systems and `providedApplication()` are designed to work together for full black-box testing:\n\n```kotlin hl_lines=\"2-4 7-14 17\"\nStove().with {\n    // Your app's API\n    httpClient { HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\") }\n\n    // Dependent services and infrastructure\n    httpClient(OrderService) { HttpClientSystemOptions(baseUrl = \"https://order.internal.com\") }\n    postgresql(AppDb) {\n        PostgresqlOptions.provided(\n            jdbcUrl = \"jdbc:postgresql://staging-db:5432/myapp\",\n            host = \"staging-db\", port = 5432,\n            configureExposedConfiguration = { emptyList() }\n        )\n    }\n    kafka {\n        KafkaSystemOptions.provided(\n            bootstrapServers = \"staging-kafka:9092\",\n            configureExposedConfiguration = { emptyList() }\n        )\n    }\n\n    // App already running\n    providedApplication {\n        ProvidedApplicationOptions(\n            readiness = ReadinessStrategy.HttpGet(url = \"https://staging.myapp.com/health\")\n        )\n    }\n}.run()\n```\n\nSee also: [Provided Application](19-provided-application.md) for testing against deployed apps.\n"
  },
  {
    "path": "docs/Components/21-mcp.md",
    "content": "# MCP\n\nStove CLI exposes a local MCP endpoint for AI agents. It lets agents inspect failed end-to-end tests through compact, structured tools instead of loading full logs into context.\n\nMCP starts automatically when you run `stove`:\n\n```bash\nstove\n```\n\nThe startup output includes the endpoint:\n\n```text\nStove CLI v0.23.0 running\nUI:   http://localhost:4040\nREST: http://localhost:4040/api/v1\nMCP:  http://localhost:4040/mcp\ngRPC: localhost:4041\n```\n\n## Discovery\n\nAgents and humans can discover Stove MCP from:\n\n- the `stove` startup banner\n- the dashboard UI\n- `GET http://localhost:4040/api/v1/meta`\n- project or agent instructions\n\nThe metadata endpoint includes:\n\n```json\n{\n  \"stove_cli_version\": \"0.23.0\",\n  \"mcp\": {\n    \"enabled\": true,\n    \"transport\": \"streamable-http\",\n    \"endpoint\": \"http://localhost:4040/mcp\",\n    \"scope\": \"read-only-test-observability\"\n  }\n}\n```\n\nMost MCP clients need the endpoint URL explicitly. There is no guaranteed universal auto-discovery mechanism for local MCP servers, so the endpoint is advertised in the places above.\n\n## Integration\n\nStove MCP is served by `stove-cli`; application systems and test JVMs do not start or host MCP themselves. The integration path is:\n\n1. Start `stove`.\n2. Configure the MCP client or agent runtime to use the Streamable HTTP endpoint from the startup banner or `/api/v1/meta`.\n3. Run the tests as usual. Stove records runs, entries, traces, and snapshots through its normal dashboard pipeline.\n4. Let the agent query MCP for compact evidence after a failure.\n\nGeneric MCP client configuration should point at the same HTTP port as the dashboard:\n\n```json\n{\n  \"mcpServers\": {\n    \"stove\": {\n      \"transport\": \"streamable-http\",\n      \"url\": \"http://localhost:4040/mcp\"\n    }\n  }\n}\n```\n\nExact configuration keys vary by agent runtime, but the important values are the transport and URL. If the dashboard runs on a custom port, use the endpoint printed by `stove` or returned from `/api/v1/meta`.\n\nRecommended agent instruction:\n\n```text\nWhen Stove is running, prefer the local Stove MCP endpoint for failed-test triage.\nStart with stove_failures, then use the returned run_id + test_id with\nstove_failure_detail. Drill into stove_timeline, stove_trace, or stove_snapshot\nonly when needed. If MCP is unavailable, ambiguous, or incomplete, fall back to\nnormal test output, Stove reports, and logs.\n```\n\nAgents should not infer a test selector from names alone. A database can contain multiple apps, multiple runs per app, and duplicate test names. Use the exact `run_id + test_id` returned by MCP.\n\n## Agent Workflow\n\nUse MCP as an optimization, not as a dependency:\n\n1. Call `stove_failures`.\n2. Pick a specific `run_id` and `test_id` from the result.\n3. Call `stove_failure_detail` for the compact failure packet.\n4. Drill into `stove_timeline`, `stove_trace`, or `stove_snapshot` only when needed.\n5. If MCP is unavailable, ambiguous, or missing data, fall back to normal test output, Stove failure reports, and logs.\n\nStove data is hierarchical:\n\n```text\ndatabase\n  -> apps by app_name\n    -> runs by run_id\n      -> tests by test_id\n        -> entries, spans, snapshots\n```\n\n`app_name` is a grouping label. A single database can contain multiple apps, and each app can have many runs. `run_id + test_id` is the exact test selector.\n\n## Tools\n\n| Tool | Purpose |\n|------|---------|\n| `stove_apps` | Lists apps recorded in the dashboard database |\n| `stove_runs` | Lists runs, filterable by app and status |\n| `stove_failures` | Default entrypoint; failed tests grouped by app and run |\n| `stove_failure_detail` | Compact detail for one exact failed test |\n| `stove_timeline` | Ordered test actions, failure-focused by default |\n| `stove_trace` | Critical path and exception evidence from correlated spans |\n| `stove_snapshot` | System snapshot summaries and targeted JSON drill-down |\n| `stove_raw_evidence` | Capped raw lookup for one entry, span, or snapshot |\n\nEvery failure result includes ready-to-use next tool calls, so agents do not need to guess scopes from names.\n\n## Token Budgeting\n\nMCP tools default to compact output. Large payloads are truncated deterministically and include omitted counts or follow-up tool calls. Sensitive keys such as `authorization`, `cookie`, `password`, `secret`, `token`, `apiKey`, and `credential` are redacted before data is returned.\n\nUse `budget` when a client needs a different amount of detail:\n\n```json\n{\n  \"budget\": \"tiny\"\n}\n```\n\nSupported values are `tiny`, `compact`, and `full`. Tools that expose raw evidence also accept `max_chars`.\n\n## Security\n\nThe MCP endpoint is read-only and local-only. It does not expose tools to clear data, retry tests, delete runs, or mutate snapshots.\n\n`/mcp` accepts loopback clients and localhost `Host`/`Origin` headers. Requests from non-local hosts are rejected to reduce the risk of browser or DNS rebinding abuse.\n\n## Troubleshooting\n\nIf an agent cannot use MCP:\n\n- confirm `stove` is running\n- check the startup banner for the actual port\n- open `http://localhost:4040/api/v1/meta` and verify `mcp.enabled` is `true`\n- make sure the MCP client is configured with `http://localhost:4040/mcp`\n- fall back to normal test output and logs if the endpoint cannot be reached\n\nIf MCP returns no failures, the latest recorded runs may have passed, the dashboard dependency may not be registered in the test config, or the test run may still be in progress.\n"
  },
  {
    "path": "docs/Components/22-container.md",
    "content": "# Container AUT (`stove-container`)\n\n`stove-container` runs the application under test as a Docker image. It works with **any language and any framework** — Go, Python, Node.js, Rust, .NET, JVM, anything that ships in a container — and gives you image-level parity with what you deploy to production.\n\nFor a host-binary AUT (process mode), see [`stove-process`](../other-languages/index.md). For a Go-specific walkthrough that pairs `stove-container` with PostgreSQL + Kafka + tracing + coverage, see [Go Container Mode](../other-languages/go-container.md).\n\n## What `stove-container` is responsible for\n\n- Pulling / locating the image, configuring it as a Testcontainers `GenericContainer`\n- Mapping Stove configurations to environment variables (`envMapper`) or CLI arguments (`argsMapper`)\n- Optional pre-start hook (`beforeStarted`) with resolved configurations\n- Container start, readiness check, log streaming\n- Graceful stop with configurable timeout, force-close fallback\n\n## What `stove-container` is **not** responsible for\n\n- **Building the image.** That is the user's pipeline. Stove only needs an image reference.\n- **Choosing the image registry or auth.** Use Testcontainers / Docker config like you would for any other test.\n- **Owning the Dockerfile.** Show your existing production Dockerfile to Stove via a tag.\n\n## Install\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$stoveVersion\"))\n    testImplementation(\"com.trendyol:stove-container\")\n}\n```\n\n## Image source patterns\n\n`containerApp(...)` only needs an image reference. Where it comes from is your choice:\n\n| Pattern | When to use | How |\n|---------|-------------|-----|\n| **CI artifact** | Most realistic CI path | CI publishes a tag (e.g. `ghcr.io/acme/app:sha-abc`); test reads it from a system property or env var |\n| **Registry pull** | Image already published; no local build needed | Just reference the tag — Testcontainers pulls lazily on first use |\n| **Local build** (optional) | Inner-loop convenience when iterating on the Dockerfile | Wire a Gradle `Exec` task running `docker build`; have a *separate* test task `dependsOn` it |\n\nThe minimal Gradle wiring for the CI path:\n\n```kotlin title=\"build.gradle.kts\"\nval containerImage = providers.environmentVariable(\"APP_IMAGE\")\n    .orElse(providers.gradleProperty(\"app.image\"))\n    .orElse(\"my-app:local\")     // local fallback only\n\ntasks.register<Test>(\"e2eTest-container\") {\n    useJUnitPlatform()\n    systemProperty(\"app.container.image\", containerImage.get())\n}\n```\n\n```bash\n# CI\nAPP_IMAGE=ghcr.io/acme/app:sha-abc123 ./gradlew e2eTest-container\n# or\n./gradlew e2eTest-container -Papp.image=ghcr.io/acme/app:sha-abc123\n```\n\nA separate optional task can wrap `docker build` for local convenience without coupling it to the main test task.\n\n## DSL: `containerApp(...)`\n\n```kotlin\nimport com.trendyol.stove.container.ContainerTarget\nimport com.trendyol.stove.container.containerApp\nimport com.trendyol.stove.system.application.envMapper\n\ncontainerApp(\n    image = System.getProperty(\"app.container.image\"),\n    target = ContainerTarget.Server(\n        hostPort = 8090,\n        internalPort = 8090,\n        portEnvVar = \"APP_PORT\",\n        bindHostPort = false      // host network → no need to bind\n    ),\n    envProvider = envMapper {\n        \"database.host\" to \"DB_HOST\"\n        \"database.port\" to \"DB_PORT\"\n        \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n        env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:4317\")\n    },\n    configureContainer = {\n        withNetworkMode(\"host\")\n    },\n    beforeStarted = { configurations ->\n        // optional async hook with resolved configs\n    }\n)\n```\n\n### Parameters\n\n| Parameter | Type | Purpose |\n|-----------|------|---------|\n| `image` | `String` | Image reference. From CI tag, registry, or local build — Stove does not care |\n| `target` | `ContainerTarget` | `Server` (HTTP / gRPC / TCP) or `Worker` (consumers, jobs); carries the readiness strategy |\n| `registry` | `String` | Image registry override (defaults to `DEFAULT_REGISTRY`) |\n| `compatibleSubstitute` | `String?` | Substitute image for arch/OS compatibility (Apple Silicon / arm64) |\n| `command` | `List<String>` | Override container command (gets `argsMapper` output appended) |\n| `envProvider` | `EnvProvider` | `envMapper { ... }` mapping Stove configs to env vars |\n| `argsProvider` | `ArgsProvider` | `argsMapper(prefix, separator) { ... }` for CLI-flag-driven apps |\n| `beforeStarted` | suspend lambda | Async hook with resolved configs, runs before container start |\n| `configureContainer` | `GenericContainer<*>.()` | Anything Testcontainers exposes — bind mounts, network mode, capabilities, log consumers |\n| `gracefulShutdownTimeout` | `Duration` | Defaults to 5 seconds; falls back to force-close on timeout |\n\n### `ContainerTarget` variants\n\n| Variant | Use case | Default readiness |\n|---------|----------|-------------------|\n| `ContainerTarget.Server(hostPort, internalPort, portEnvVar, bindHostPort)` | HTTP / gRPC / TCP servers | HTTP GET `http://localhost:$hostPort/health` |\n| `ContainerTarget.Worker()` | Kafka consumers, batch jobs | 2-second fixed delay |\n\n`bindHostPort = false` is the right default when using `withNetworkMode(\"host\")` — the container shares the host network namespace and binding the port again would conflict.\n\n### Readiness strategies\n\n`ContainerTarget.Server` defaults to `ReadinessStrategy.HttpGet`. You can override:\n\n```kotlin\ntarget = ContainerTarget.Server(\n    hostPort = 8090,\n    internalPort = 8090,\n    portEnvVar = \"APP_PORT\",\n    readiness = ReadinessStrategy.TcpPort(8090)   // for raw TCP / gRPC w/o HTTP\n)\n```\n\n| Strategy | Use case |\n|----------|----------|\n| `ReadinessStrategy.HttpGet(url, timeout, retries, retryDelay, expectedStatusCodes)` | REST APIs |\n| `ReadinessStrategy.TcpPort(port)` | gRPC / raw TCP (no HTTP) |\n| `ReadinessStrategy.Probe { ... }` | Custom (file, DB query, log scan, etc.) |\n| `ReadinessStrategy.FixedDelay(duration)` | Workers / no readiness signal |\n\n## Networking strategies\n\n=== \"Host network (Linux only)\"\n\n    ```kotlin\n    target = ContainerTarget.Server(hostPort = 8090, internalPort = 8090,\n        portEnvVar = \"APP_PORT\", bindHostPort = false),\n    configureContainer = { withNetworkMode(\"host\") }\n    ```\n\n    Container shares the host's network namespace. The app reaches PostgreSQL / Kafka on `localhost`. Does **not** work on Docker Desktop for macOS / Windows.\n\n=== \"Port binding (cross-platform)\"\n\n    ```kotlin\n    target = ContainerTarget.Server(hostPort = 8090, internalPort = 8090,\n        portEnvVar = \"APP_PORT\", bindHostPort = true),\n    configureContainer = { withNetwork(Network.SHARED) }\n    ```\n\n    Stove binds `hostPort → internalPort`. The app reaches databases / brokers via shared network aliases or `host.docker.internal`.\n\n## `configureContainer { ... }`\n\nAccepts a `GenericContainer<*>.()` block. Anything Testcontainers exposes is available:\n\n```kotlin\nconfigureContainer = {\n    withNetworkMode(\"host\")\n    withFileSystemBind(hostPath, \"/inside/container\")\n    withLogConsumer(Slf4jLogConsumer(LoggerFactory.getLogger(\"app\")))\n    withEnv(\"EXTRA_DEBUG\", \"1\")\n    withCreateContainerCmdModifier { cmd -> /* low-level docker-java */ }\n}\n```\n\nUse bind mounts for any data the container or the test needs to share with the host: coverage directories, fixture seeds, read-only configs.\n\n## `beforeStarted { ... }`\n\nAsync hook that runs after Stove resolves all configurations but before the container starts. Useful for prepping data the app expects on boot.\n\n```kotlin\nbeforeStarted = { configurations ->\n    seedRedisCache(configurations[\"redis.host\"]!!)\n}\n```\n\n## Switching between process and container mode\n\nA single `StoveConfig.kt` can serve both starters by branching on a system property. Infrastructure systems and tests stay identical — only the AUT runner changes.\n\n```kotlin\nwhen ((System.getProperty(\"aut.mode\") ?: \"process\").lowercase()) {\n    \"process\"   -> processApp { ProcessApplicationOptions(/* ... */) }\n    \"container\" -> containerApp(/* ... */)\n    else        -> error(\"Unsupported aut.mode\")\n}\n```\n\n```kotlin\ntasks.register<Test>(\"e2eTest\")           { systemProperty(\"aut.mode\", \"process\") }\ntasks.register<Test>(\"e2eTest-container\") { systemProperty(\"aut.mode\", \"container\") }\n```\n\nA common pattern: `e2eTest` runs process mode locally for fast iteration; `e2eTest-container` runs container mode in CI against the image the build job just published.\n\n## Common pitfalls\n\n| Symptom | Cause | Fix |\n|---------|-------|-----|\n| `connection refused` to Postgres / Kafka inside container | Container can't reach Testcontainers on `localhost` | `withNetworkMode(\"host\")` (Linux) or shared network + aliases (cross-platform) |\n| Stove never sees `/health` | Wrong port / binding | Confirm `bindHostPort` matches network mode; verify app listens on `internalPort` |\n| `Failed to start container application` | Image missing or unauthorized pull | Verify the image exists locally / in the registry; check `docker images` and registry credentials |\n| Slow inner loop | Image build dominates iteration | Use [`stove-process`](../other-languages/index.md) for daily dev; container mode in CI |\n| App killed before clean shutdown | `gracefulShutdownTimeout` too short for the app | Bump `gracefulShutdownTimeout` on `containerApp(...)` |\n\n## Reference\n\n- Module source: `starters/container/stove-container/`\n- DSL source: `starters/container/stove-container/src/main/kotlin/com/trendyol/stove/container/ContainerDsl.kt`\n- Go-specific recipe (process **and** container modes in one repo): [`recipes/process/golang/go-showcase`](https://github.com/Trendyol/stove/tree/main/recipes/process/golang/go-showcase)\n- Related docs:\n  - [Go Container Mode](../other-languages/go-container.md) — Go-specific walkthrough that uses this module\n  - [Other Languages & Stacks](../other-languages/index.md) — process vs. container overview\n  - [Dashboard](18-dashboard.md) and [MCP](21-mcp.md) — observability for any AUT, including container ones\n  - [Tracing](15-tracing.md) — distributed tracing across the test and the container\n"
  },
  {
    "path": "docs/Components/index.md",
    "content": "# Components\n\nStove uses a pluggable architecture. Each physical dependency is a separate module you add only when the test actually needs it. By default, components use <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Testcontainers</span> under the hood, but they can also connect to [provided instances](11-provided-instances.md) (existing infrastructure) when Docker is unavailable or undesirable.\n\nThis section also includes observability and agent-facing tooling such as [Reporting](13-reporting.md), [Tracing](15-tracing.md), [Dashboard](18-dashboard.md), and [MCP](21-mcp.md). These do not replace a dependency system; they help you understand failures and share compact evidence with tools.\n\nIf you have not picked an application starter yet, start with [Supported Frameworks](../frameworks/index.md) first and then come back here for the physical dependencies.\n\nMost teams start with 2 to 4 components, not the whole catalog.\n\n## Start From The Workflow\n\n<div class=\"grid cards\" markdown>\n\n-   **REST service with a database**\n\n    Start with [HTTP Client](05-http.md), one database such as [PostgreSQL](06-postgresql.md), and optionally [WireMock](04-wiremock.md) for external calls.\n\n-   **Kafka-driven workflow**\n\n    Start with [Kafka](02-kafka.md), your main database, and [Tracing](15-tracing.md) for better failure diagnosis.\n\n-   **Service calling external dependencies**\n\n    Start with [HTTP Client](05-http.md), [WireMock](04-wiremock.md), or [gRPC Mock](14-grpc-mock.md) depending on the remote protocol.\n\n-   **Hard-to-debug end-to-end failures**\n\n    Add [Tracing](15-tracing.md) and [Reporting](13-reporting.md) early so failures show execution context instead of only raw assertions.\n\n-   **Visual test observability**\n\n    Add [Dashboard](18-dashboard.md) for a real-time web dashboard that shows every test interaction, distributed trace, and system snapshot across runs. Agents can use [MCP](21-mcp.md) for compact failed-test evidence.\n\n-   **Testing a deployed service (any language)**\n\n    Use [Provided Application](19-provided-application.md) with [HTTP Client](05-http.md) and your databases. Add [Multiple Systems](20-multiple-systems.md) if you need to verify multiple dependent services.\n\n</div>\n\n## Common Starting Sets\n\n| You are testing | Start with |\n|-----------------|------------|\n| HTTP API backed by SQL | `stove-http` + `stove-postgres` or `stove-mysql` |\n| Event-driven service | `stove-kafka` + your database + `stove-tracing` |\n| External provider integration | `stove-http` + `stove-wiremock` |\n| gRPC service | `stove-grpc` + `stove-grpc-mock` |\n| Stateful service with caching | your database + `stove-redis` |\n| Deployed service (any language) | `stove-http` + your databases + `providedApplication()` |\n\n## Available Components\n\n| Component | Module | Description |\n|-----------|--------|-------------|\n| [Kafka](02-kafka.md) | `stove-kafka` | Message broker for event-driven architectures |\n| [Couchbase](01-couchbase.md) | `stove-couchbase` | NoSQL document database |\n| [Elasticsearch](03-elasticsearch.md) | `stove-elasticsearch` | Search and analytics engine |\n| [PostgreSQL](06-postgresql.md) | `stove-postgres` | Relational database |\n| [MySQL](16-mysql.md) | `stove-mysql` | Relational database |\n| [MongoDB](07-mongodb.md) | `stove-mongodb` | NoSQL document database |\n| [MSSQL](08-mssql.md) | `stove-mssql` | Microsoft SQL Server |\n| [Redis](09-redis.md) | `stove-redis` | In-memory data store |\n| [Cassandra](17-cassandra.md) | `stove-cassandra` | Wide-column NoSQL database |\n| [WireMock](04-wiremock.md) | `stove-wiremock` | HTTP mock server for external services |\n| [gRPC Mock](14-grpc-mock.md) | `stove-grpc-mock` | gRPC mock server for external gRPC services |\n| [HTTP Client](05-http.md) | `stove-http` | HTTP client for testing your API |\n| [gRPC](12-grpc.md) | `stove-grpc` | gRPC client for testing gRPC services |\n| [Bridge](10-bridge.md) | Built-in | Access to application's DI container |\n| [Tracing](15-tracing.md) | `stove-tracing` | Execution tracing with OpenTelemetry for failure diagnostics |\n| [Provided Instances](11-provided-instances.md) | Built-in | Connect to existing infrastructure instead of containers |\n| [Provided Application](19-provided-application.md) | Built-in | Test against a remote, already-deployed application (any language) |\n| [Multiple Systems](20-multiple-systems.md) | Built-in | Register multiple instances of the same system type with typed keys |\n| [Reporting](13-reporting.md) | `stove-extensions-kotest` or `stove-extensions-junit` | Rich failure reports with execution context |\n| [Dashboard](18-dashboard.md) | `stove-dashboard` + [CLI](https://github.com/Trendyol/stove/tree/main/tools/stove-cli) | Real-time web dashboard for test observability |\n| [MCP](21-mcp.md) | `stove-cli` | Local read-only agent API for compact failed-test evidence |\n\n## Quick Start\n\nAdd one starter, then only the components your scenario needs:\n\n=== \"Gradle\"\n\n    ```kotlin\n    dependencies {\n        testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n        testImplementation(\"com.trendyol:stove\")\n\n        // Pick one application starter\n        testImplementation(\"com.trendyol:stove-spring\")\n\n        // Add only what the test touches\n        testImplementation(\"com.trendyol:stove-http\")\n        testImplementation(\"com.trendyol:stove-postgres\")\n        testImplementation(\"com.trendyol:stove-wiremock\")\n        testImplementation(\"com.trendyol:stove-tracing\")\n    }\n    ```\n\nSwap in `stove-kafka`, `stove-redis`, `stove-grpc`, `stove-mongodb`, or other modules as your flow requires.\n\n## Architecture Overview\n\nEach component follows a consistent pattern:\n\n1. **Configuration** - Define how the component should be set up\n2. **Container/Runtime** - Manages the testcontainer or provided instance\n3. **DSL** - Fluent API for test assertions\n4. **Cleanup** - Automatic resource management\n\n```kotlin\nStove()\n  .with {\n    // Each component is configured in the `with` block\n    kafka { KafkaSystemOptions(...) }\n    couchbase { CouchbaseSystemOptions(...) }\n    http { HttpClientSystemOptions(...) }\n    wiremock { WireMockSystemOptions(...) }\n    tracing { enableSpanReceiver() }\n    \n    // Application under test\n    springBoot(runner = { params -> myApp.run(params) })\n  }\n  .run() // Starts all components and the application\n\n// Test your application\nstove {\n  http { /* HTTP assertions */ }\n  kafka { /* Kafka assertions */ }\n  couchbase { /* Database assertions */ }\n}\n```\n\n## Component Categories\n\n### Databases\n\n| Type | Components | Use Case |\n|------|------------|----------|\n| Document | [Couchbase](01-couchbase.md), [MongoDB](07-mongodb.md), [Elasticsearch](03-elasticsearch.md) | JSON document storage, search |\n| Relational | [PostgreSQL](06-postgresql.md), [MySQL](16-mysql.md), [MSSQL](08-mssql.md) | Structured data, transactions |\n| Key-Value | [Redis](09-redis.md) | Caching, sessions, pub/sub |\n| Wide-Column | [Cassandra](17-cassandra.md) (`stove-cassandra`) | Time-series, IoT, large-scale writes |\n\n### Messaging\n\n| Component | Use Case |\n|-----------|----------|\n| [Kafka](02-kafka.md) | Event streaming, message queues, pub/sub |\n\n### Network\n\n| Component | Use Case |\n|-----------|----------|\n| [HTTP Client](05-http.md) | Testing your application's REST API |\n| [gRPC](12-grpc.md) | Testing your application's gRPC services |\n| [WireMock](04-wiremock.md) | Mocking external HTTP services |\n| [gRPC Mock](14-grpc-mock.md) | Mocking external gRPC services |\n\n### Application Integration\n\n| Component | Use Case |\n|-----------|----------|\n| [Bridge](10-bridge.md) | Access application beans and services directly (supported by Spring, Ktor, and Micronaut starters) |\n| [Provided Application](19-provided-application.md) | Test against a remote app without starting it locally --- any language, any framework |\n| [Multiple Systems](20-multiple-systems.md) | Register multiple named instances of the same system type (e.g., two PostgreSQL databases) |\n| [Reporting](13-reporting.md) | Detailed execution reports and failure diagnostics |\n| [Tracing](15-tracing.md) | <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">Execution tracing with full call chain visibility on failure</span> |\n| [Dashboard](18-dashboard.md) | Real-time web dashboard for browsing test runs, traces, and system snapshots |\n| [MCP](21-mcp.md) | Local read-only agent API for compact failed-test evidence from the dashboard database |\n\n## Common Configuration Pattern\n\nAll components follow a similar configuration pattern:\n\n```kotlin hl_lines=\"4-8 12-16\"\ncomponentName {\n  ComponentSystemOptions(\n    // Container configuration\n    container = ContainerOptions(\n      registry = \"docker.io\",\n      image = \"component-image\",\n      tag = \"version\"\n    ),\n    \n    // Expose configuration to your application\n    configureExposedConfiguration = { cfg ->\n      listOf(\n        \"app.component.host=${cfg.host}\",\n        \"app.component.port=${cfg.port}\"\n      )\n    }\n  )\n}\n```\n\n## Testcontainer vs Provided Instance\n\nEach component supports two modes:\n\n### Container Mode (Default)\n\nStove automatically manages testcontainers:\n\n```kotlin\nkafka {\n  KafkaSystemOptions(\n    container = KafkaContainerOptions(tag = \"latest\"),\n    configureExposedConfiguration = { cfg -> listOf(...) }\n  )\n}\n```\n\n### Provided Instance Mode\n\nConnect to existing infrastructure (useful for CI/CD):\n\n```kotlin\nkafka {\n  KafkaSystemOptions.provided(\n    bootstrapServers = \"localhost:9092\",\n    configureExposedConfiguration = { cfg -> \n      listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n    }\n  )\n}\n```\n\nSee [Provided Instances](11-provided-instances.md) for detailed documentation.\n\n## Migrations Support\n\nDatabase components support migrations:\n\n```kotlin\nclass CreateTablesMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n  override val order: Int = 1\n  \n  override suspend fun execute(connection: PostgresSqlMigrationContext) {\n    connection.operations.execute(\"CREATE TABLE ...\")\n  }\n}\n\npostgresql {\n  PostgresqlOptions(...).migrations {\n    register<CreateTablesMigration>()\n  }\n}\n```\n\n## Cleanup Support\n\nAll components support cleanup functions for data isolation:\n\n```kotlin\ncouchbase {\n  CouchbaseSystemOptions(\n    defaultBucket = \"bucket\",\n    cleanup = { cluster ->\n      cluster.query(\"DELETE FROM `bucket` WHERE type = 'test'\")\n    },\n    configureExposedConfiguration = { cfg ->\n      listOf(\n        \"couchbase.hosts=${cfg.hostsWithPort}\",\n        \"couchbase.username=${cfg.username}\",\n        \"couchbase.password=${cfg.password}\"\n      )\n    }\n  )\n}\n```\n\n## Best Practices\n\n1. **Use random data** - Generate unique identifiers for each test to avoid conflicts\n2. **Leverage cleanup functions** - Clean test data between runs\n3. **Configure timeouts appropriately** - Set realistic timeouts for your environment\n4. **Use the DSL consistently** - Leverage the fluent API for readable tests\n5. **Combine components** - <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">Test complete workflows across multiple systems</span>\n\n## Example: Full Stack Test\n\n```kotlin hl_lines=\"7 12 19 26 31 36\"\ntest(\"should process order end-to-end\") {\n  stove {\n    val orderId = UUID.randomUUID().toString()\n    \n    // Mock payment service\n    wiremock {\n      mockPost(\"/payments\", statusCode = 200, responseBody = PaymentResult(success = true).some())\n    }\n    \n    // Create order via API\n    http {\n      postAndExpectBody<OrderResponse>(\"/orders\", body = CreateOrderRequest(orderId).some()) { \n        it.status shouldBe 201 \n      }\n    }\n    \n    // Verify stored in database\n    couchbase {\n      shouldGet<Order>(\"orders\", orderId) { order ->\n        order.status shouldBe \"CREATED\"\n      }\n    }\n    \n    // Verify event published\n    kafka {\n      shouldBePublished<OrderCreatedEvent> { actual.orderId == orderId }\n    }\n    \n    // Verify indexed for search\n    elasticsearch {\n      shouldGet<Order>(index = \"orders\", key = orderId) { it.status shouldBe \"CREATED\" }\n    }\n    \n    // Verify cached\n    redis {\n      client().connect().sync().get(\"order:$orderId\") shouldNotBe null\n    }\n  }\n}\n```\n\n## Detailed Documentation\n\n- [Couchbase](01-couchbase.md) - NoSQL document database with N1QL queries\n- [Kafka](02-kafka.md) - Message streaming with producer/consumer testing\n- [Elasticsearch](03-elasticsearch.md) - Search engine with query DSL support\n- [WireMock](04-wiremock.md) - Mock external HTTP dependencies\n- [gRPC Mock](14-grpc-mock.md) - Mock external gRPC services\n- [HTTP Client](05-http.md) - Test your REST API endpoints\n- [gRPC](12-grpc.md) - Test your gRPC services with Wire and grpc-kotlin\n- [PostgreSQL](06-postgresql.md) - Relational database with SQL support\n- [MongoDB](07-mongodb.md) - Document database with aggregation support\n- [MSSQL](08-mssql.md) - Microsoft SQL Server support\n- [Redis](09-redis.md) - In-memory data store for caching\n- [Cassandra](17-cassandra.md) - Wide-column NoSQL database with CQL support\n- [Bridge](10-bridge.md) - Direct access to application beans\n- [Provided Instances](11-provided-instances.md) - Use external infrastructure\n- [Provided Application](19-provided-application.md) - Test against a deployed app (any language)\n- [Multiple Systems](20-multiple-systems.md) - Multiple named instances of the same system type\n- [Reporting](13-reporting.md) - Detailed execution reports and failure diagnostics\n- [Tracing](15-tracing.md) - <span data-rn=\"underline\" data-rn-color=\"#009688\">Execution tracing with OpenTelemetry for full call chain visibility</span>\n- [Dashboard](18-dashboard.md) - Real-time web dashboard for browsing test runs, traces, and snapshots\n- [MCP](21-mcp.md) - Local read-only agent API for failed-test triage\n"
  },
  {
    "path": "docs/assets/rough-notation.iife.js",
    "content": "var RoughNotation=function(t){\"use strict\";const e=\"http://www.w3.org/2000/svg\";class s{constructor(t){this.seed=t}next(){return this.seed?(2**31-1&(this.seed=Math.imul(48271,this.seed)))/2**31:Math.random()}}function i(t,e,s,i,n){return{type:\"path\",ops:u(t,e,s,i,n)}}function n(t,e,s){const n=(t||[]).length;if(n>2){const i=[];for(let e=0;e<n-1;e++)i.push(...u(t[e][0],t[e][1],t[e+1][0],t[e+1][1],s));return e&&i.push(...u(t[n-1][0],t[n-1][1],t[0][0],t[0][1],s)),{type:\"path\",ops:i}}return 2===n?i(t[0][0],t[0][1],t[1][0],t[1][1],s):{type:\"path\",ops:[]}}function o(t,e,s,i,o){return function(t,e){return n(t,!0,e)}([[t,e],[t+s,e],[t+s,e+i],[t,e+i]],o)}function r(t,e,s,i,n){return function(t,e,s,i){const[n,o]=g(i.increment,t,e,i.rx,i.ry,1,i.increment*a(.1,a(.4,1,s),s),s);let r=l(n,null,s);if(!s.disableMultiStroke){const[n]=g(i.increment,t,e,i.rx,i.ry,1.5,0,s),o=l(n,null,s);r=r.concat(o)}return{estimatedPoints:o,opset:{type:\"path\",ops:r}}}(t,e,n,function(t,e,s){const i=Math.sqrt(2*Math.PI*Math.sqrt((Math.pow(t/2,2)+Math.pow(e/2,2))/2)),n=Math.max(s.curveStepCount,s.curveStepCount/Math.sqrt(200)*i),o=2*Math.PI/n;let r=Math.abs(t/2),h=Math.abs(e/2);const a=1-s.curveFitting;return r+=c(r*a,s),h+=c(h*a,s),{increment:o,rx:r,ry:h}}(s,i,n)).opset}function h(t){return t.randomizer||(t.randomizer=new s(t.seed||0)),t.randomizer.next()}function a(t,e,s,i=1){return s.roughness*i*(h(s)*(e-t)+t)}function c(t,e,s=1){return a(-t,t,e,s)}function u(t,e,s,i,n,o=!1){const r=o?n.disableMultiStrokeFill:n.disableMultiStroke,h=f(t,e,s,i,n,!0,!1);if(r)return h;const a=f(t,e,s,i,n,!0,!0);return h.concat(a)}function f(t,e,s,i,n,o,r){const a=Math.pow(t-s,2)+Math.pow(e-i,2),u=Math.sqrt(a);let f=1;f=u<200?1:u>500?.4:-.0016668*u+1.233334;let l=n.maxRandomnessOffset||0;l*l*100>a&&(l=u/10);const g=l/2,d=.2+.2*h(n);let p=n.bowing*n.maxRandomnessOffset*(i-e)/200,_=n.bowing*n.maxRandomnessOffset*(t-s)/200;p=c(p,n,f),_=c(_,n,f);const m=[],w=()=>c(g,n,f),v=()=>c(l,n,f);return o&&(r?m.push({op:\"move\",data:[t+w(),e+w()]}):m.push({op:\"move\",data:[t+c(l,n,f),e+c(l,n,f)]})),r?m.push({op:\"bcurveTo\",data:[p+t+(s-t)*d+w(),_+e+(i-e)*d+w(),p+t+2*(s-t)*d+w(),_+e+2*(i-e)*d+w(),s+w(),i+w()]}):m.push({op:\"bcurveTo\",data:[p+t+(s-t)*d+v(),_+e+(i-e)*d+v(),p+t+2*(s-t)*d+v(),_+e+2*(i-e)*d+v(),s+v(),i+v()]}),m}function l(t,e,s){const i=t.length,n=[];if(i>3){const o=[],r=1-s.curveTightness;n.push({op:\"move\",data:[t[1][0],t[1][1]]});for(let e=1;e+2<i;e++){const s=t[e];o[0]=[s[0],s[1]],o[1]=[s[0]+(r*t[e+1][0]-r*t[e-1][0])/6,s[1]+(r*t[e+1][1]-r*t[e-1][1])/6],o[2]=[t[e+1][0]+(r*t[e][0]-r*t[e+2][0])/6,t[e+1][1]+(r*t[e][1]-r*t[e+2][1])/6],o[3]=[t[e+1][0],t[e+1][1]],n.push({op:\"bcurveTo\",data:[o[1][0],o[1][1],o[2][0],o[2][1],o[3][0],o[3][1]]})}if(e&&2===e.length){const t=s.maxRandomnessOffset;n.push({op:\"lineTo\",data:[e[0]+c(t,s),e[1]+c(t,s)]})}}else 3===i?(n.push({op:\"move\",data:[t[1][0],t[1][1]]}),n.push({op:\"bcurveTo\",data:[t[1][0],t[1][1],t[2][0],t[2][1],t[2][0],t[2][1]]})):2===i&&n.push(...u(t[0][0],t[0][1],t[1][0],t[1][1],s));return n}function g(t,e,s,i,n,o,r,h){const a=[],u=[],f=c(.5,h)-Math.PI/2;u.push([c(o,h)+e+.9*i*Math.cos(f-t),c(o,h)+s+.9*n*Math.sin(f-t)]);for(let r=f;r<2*Math.PI+f-.01;r+=t){const t=[c(o,h)+e+i*Math.cos(r),c(o,h)+s+n*Math.sin(r)];a.push(t),u.push(t)}return u.push([c(o,h)+e+i*Math.cos(f+2*Math.PI+.5*r),c(o,h)+s+n*Math.sin(f+2*Math.PI+.5*r)]),u.push([c(o,h)+e+.98*i*Math.cos(f+r),c(o,h)+s+.98*n*Math.sin(f+r)]),u.push([c(o,h)+e+.9*i*Math.cos(f+.5*r),c(o,h)+s+.9*n*Math.sin(f+.5*r)]),[u,a]}function d(t,e){return{maxRandomnessOffset:2,roughness:\"highlight\"===t?3:1.5,bowing:1,stroke:\"#000\",strokeWidth:1.5,curveTightness:0,curveFitting:.95,curveStepCount:9,fillStyle:\"hachure\",fillWeight:-1,hachureAngle:-41,hachureGap:-1,dashOffset:-1,dashGap:-1,zigzagOffset:-1,combineNestedSvgPaths:!1,disableMultiStroke:\"double\"!==t,disableMultiStrokeFill:!1,seed:e}}function p(t,s,h,a,c,u){const f=[];let l=h.strokeWidth||2;const g=function(t){const e=t.padding;if(e||0===e){if(\"number\"==typeof e)return[e,e,e,e];if(Array.isArray(e)){const t=e;if(t.length)switch(t.length){case 4:return[...t];case 1:return[t[0],t[0],t[0],t[0]];case 2:return[...t,...t];case 3:return[...t,t[1]];default:return[t[0],t[1],t[2],t[3]]}}}return[5,5,5,5]}(h),p=void 0===h.animate||!!h.animate,_=h.iterations||2,m=h.rtl?1:0,w=d(\"single\",u);switch(h.type){case\"underline\":{const t=s.y+s.h+g[2];for(let e=m;e<_+m;e++)e%2?f.push(i(s.x+s.w,t,s.x,t,w)):f.push(i(s.x,t,s.x+s.w,t,w));break}case\"strike-through\":{const t=s.y+s.h/2;for(let e=m;e<_+m;e++)e%2?f.push(i(s.x+s.w,t,s.x,t,w)):f.push(i(s.x,t,s.x+s.w,t,w));break}case\"box\":{const t=s.x-g[3],e=s.y-g[0],i=s.w+(g[1]+g[3]),n=s.h+(g[0]+g[2]);for(let s=0;s<_;s++)f.push(o(t,e,i,n,w));break}case\"bracket\":{const t=Array.isArray(h.brackets)?h.brackets:h.brackets?[h.brackets]:[\"right\"],e=s.x-2*g[3],i=s.x+s.w+2*g[1],o=s.y-2*g[0],r=s.y+s.h+2*g[2];for(const h of t){let t;switch(h){case\"bottom\":t=[[e,s.y+s.h],[e,r],[i,r],[i,s.y+s.h]];break;case\"top\":t=[[e,s.y],[e,o],[i,o],[i,s.y]];break;case\"left\":t=[[s.x,o],[e,o],[e,r],[s.x,r]];break;case\"right\":t=[[s.x+s.w,o],[i,o],[i,r],[s.x+s.w,r]]}t&&f.push(n(t,!1,w))}break}case\"crossed-off\":{const t=s.x,e=s.y,n=t+s.w,o=e+s.h;for(let s=m;s<_+m;s++)s%2?f.push(i(n,o,t,e,w)):f.push(i(t,e,n,o,w));for(let s=m;s<_+m;s++)s%2?f.push(i(t,o,n,e,w)):f.push(i(n,e,t,o,w));break}case\"circle\":{const t=d(\"double\",u),e=s.w+(g[1]+g[3]),i=s.h+(g[0]+g[2]),n=s.x-g[3]+e/2,o=s.y-g[0]+i/2,h=Math.floor(_/2),a=_-2*h;for(let s=0;s<h;s++)f.push(r(n,o,e,i,t));for(let t=0;t<a;t++)f.push(r(n,o,e,i,w));break}case\"highlight\":{const t=d(\"highlight\",u);l=.95*s.h;const e=s.y+s.h/2;for(let n=m;n<_+m;n++)n%2?f.push(i(s.x+s.w,e,s.x,e,t)):f.push(i(s.x,e,s.x+s.w,e,t));break}}if(f.length){const s=function(t){const e=[];for(const s of t){let t=\"\";for(const i of s.ops){const s=i.data;switch(i.op){case\"move\":t.trim()&&e.push(t.trim()),t=`M${s[0]} ${s[1]} `;break;case\"bcurveTo\":t+=`C${s[0]} ${s[1]}, ${s[2]} ${s[3]}, ${s[4]} ${s[5]} `;break;case\"lineTo\":t+=`L${s[0]} ${s[1]} `}}t.trim()&&e.push(t.trim())}return e}(f),i=[],n=[];let o=0;const r=(t,e,s)=>t.setAttribute(e,s);for(const a of s){const s=document.createElementNS(e,\"path\");if(r(s,\"d\",a),r(s,\"fill\",\"none\"),r(s,\"stroke\",h.color||\"currentColor\"),r(s,\"stroke-width\",\"\"+l),p){const t=s.getTotalLength();i.push(t),o+=t}t.appendChild(s),n.push(s)}if(p){let t=0;for(let e=0;e<n.length;e++){const s=n[e],r=i[e],h=o?c*(r/o):0,u=a+t,f=s.style;f.strokeDashoffset=\"\"+r,f.strokeDasharray=\"\"+r,f.animation=`rough-notation-dash ${h}ms ease-out ${u}ms forwards`,t+=h}}}}class _{constructor(t,e){this._state=\"unattached\",this._resizing=!1,this._seed=Math.floor(Math.random()*2**31),this._lastSizes=[],this._animationDelay=0,this._resizeListener=()=>{this._resizing||(this._resizing=!0,setTimeout(()=>{this._resizing=!1,\"showing\"===this._state&&this.haveRectsChanged()&&this.show()},400))},this._e=t,this._config=JSON.parse(JSON.stringify(e)),this.attach()}get animate(){return this._config.animate}set animate(t){this._config.animate=t}get animationDuration(){return this._config.animationDuration}set animationDuration(t){this._config.animationDuration=t}get iterations(){return this._config.iterations}set iterations(t){this._config.iterations=t}get color(){return this._config.color}set color(t){this._config.color!==t&&(this._config.color=t,this.refresh())}get strokeWidth(){return this._config.strokeWidth}set strokeWidth(t){this._config.strokeWidth!==t&&(this._config.strokeWidth=t,this.refresh())}get padding(){return this._config.padding}set padding(t){this._config.padding!==t&&(this._config.padding=t,this.refresh())}attach(){if(\"unattached\"===this._state&&this._e.parentElement){!function(){if(!window.__rno_kf_s){const t=window.__rno_kf_s=document.createElement(\"style\");t.textContent=\"@keyframes rough-notation-dash { to { stroke-dashoffset: 0; } }\",document.head.appendChild(t)}}();const t=this._svg=document.createElementNS(e,\"svg\");t.setAttribute(\"class\",\"rough-annotation\");const s=t.style;s.position=\"absolute\",s.top=\"0\",s.left=\"0\",s.overflow=\"visible\",s.pointerEvents=\"none\",s.width=\"100px\",s.height=\"100px\";const i=\"highlight\"===this._config.type;if(this._e.insertAdjacentElement(i?\"beforebegin\":\"afterend\",t),this._state=\"not-showing\",i){const t=window.getComputedStyle(this._e).position;(!t||\"static\"===t)&&(this._e.style.position=\"relative\")}this.attachListeners()}}detachListeners(){window.removeEventListener(\"resize\",this._resizeListener),this._ro&&this._ro.unobserve(this._e)}attachListeners(){this.detachListeners(),window.addEventListener(\"resize\",this._resizeListener,{passive:!0}),!this._ro&&\"ResizeObserver\"in window&&(this._ro=new window.ResizeObserver(t=>{for(const e of t)e.contentRect&&this._resizeListener()})),this._ro&&this._ro.observe(this._e)}haveRectsChanged(){if(this._lastSizes.length){const t=this.rects();if(t.length!==this._lastSizes.length)return!0;for(let e=0;e<t.length;e++)if(!this.isSameRect(t[e],this._lastSizes[e]))return!0}return!1}isSameRect(t,e){const s=(t,e)=>Math.round(t)===Math.round(e);return s(t.x,e.x)&&s(t.y,e.y)&&s(t.w,e.w)&&s(t.h,e.h)}isShowing(){return\"not-showing\"!==this._state}refresh(){this.isShowing()&&!this.pendingRefresh&&(this.pendingRefresh=Promise.resolve().then(()=>{this.isShowing()&&this.show(),delete this.pendingRefresh}))}show(){switch(this._state){case\"unattached\":break;case\"showing\":this.hide(),this._svg&&this.render(this._svg,!0);break;case\"not-showing\":this.attach(),this._svg&&this.render(this._svg,!1)}}hide(){if(this._svg)for(;this._svg.lastChild;)this._svg.removeChild(this._svg.lastChild);this._state=\"not-showing\"}remove(){this._svg&&this._svg.parentElement&&this._svg.parentElement.removeChild(this._svg),this._svg=void 0,this._state=\"unattached\",this.detachListeners()}render(t,e){let s=this._config;e&&(s=JSON.parse(JSON.stringify(this._config)),s.animate=!1);const i=this.rects();let n=0;i.forEach(t=>n+=t.w);const o=s.animationDuration||800;let r=0;for(let e=0;e<i.length;e++){const h=o*(i[e].w/n);p(t,i[e],s,r+this._animationDelay,h,this._seed),r+=h}this._lastSizes=i,this._state=\"showing\"}rects(){const t=[];if(this._svg)if(this._config.multiline){const e=this._e.getClientRects();for(let s=0;s<e.length;s++)t.push(this.svgRect(this._svg,e[s]))}else t.push(this.svgRect(this._svg,this._e.getBoundingClientRect()));return t}svgRect(t,e){const s=t.getBoundingClientRect(),i=e;return{x:(i.x||i.left)-(s.x||s.left),y:(i.y||i.top)-(s.y||s.top),w:i.width,h:i.height}}}return t.annotate=function(t,e){return new _(t,e)},t.annotationGroup=function(t){let e=0;for(const s of t){const t=s;t._animationDelay=e;e+=0===t.animationDuration?0:t.animationDuration||800}const s=[...t];return{show(){for(const t of s)t.show()},hide(){for(const t of s)t.hide()}}},Object.defineProperty(t,\"__esModule\",{value:!0}),t}({});\n"
  },
  {
    "path": "docs/best-practices.md",
    "content": "# Best Practices\n\nHere are some practices we've found helpful when writing end-to-end tests with Stove. These aren't hard rules, but they'll make your tests more maintainable and easier to work with.\n\n## Test Organization\n\n### Use Dedicated Source Set for E2E Tests\n\nInstead of placing <span data-rn=\"underline\" data-rn-color=\"#ff9800\">e2e</span> tests in the regular `src/test` folder, <span data-rn=\"underline\" data-rn-color=\"#009688\">create a dedicated `src/test-e2e` source set</span>. This provides better separation between unit/integration tests and e2e tests:\n\n```\nsrc/\n├── main/kotlin/           # Application code\n├── test/kotlin/           # Unit tests\n└── test-e2e/kotlin/       # E2E tests with Stove\n    ├── config/\n    │   └── TestConfig.kt  # Contains Stove setup\n    ├── features/\n    │   ├── OrderE2ETest.kt\n    │   ├── UserE2ETest.kt\n    │   └── ProductE2ETest.kt\n    └── shared/\n        ├── TestData.kt\n        └── Assertions.kt\n```\n\n### Gradle Configuration\n\nHere's how to set up the `test-e2e` source set in your `build.gradle.kts`:\n\n```kotlin\nsourceSets {\n    @Suppress(\"LocalVariableName\")\n    val `test-e2e` by creating {\n        compileClasspath += sourceSets.main.get().output\n        runtimeClasspath += sourceSets.main.get().output\n    }\n    \n    val testE2eImplementation by configurations.getting {\n        extendsFrom(configurations.testImplementation.get())\n    }\n    configurations[\"testE2eRuntimeOnly\"].extendsFrom(configurations.runtimeOnly.get())\n}\n\n// Register e2e test task\ntasks.register<Test>(\"e2eTest\") {\n    description = \"Runs e2e tests.\"\n    group = \"verification\"\n    testClassesDirs = sourceSets[\"test-e2e\"].output.classesDirs\n    classpath = sourceSets[\"test-e2e\"].runtimeClasspath\n\n    useJUnitPlatform()\n    reports {\n        junitXml.required.set(true)\n        html.required.set(true)\n    }\n}\n\n// Configure IDEA to recognize test-e2e as test sources\nidea {\n    module {\n        testSources.from(sourceSets[\"test-e2e\"].allSource.sourceDirectories)\n        testResources.from(sourceSets[\"test-e2e\"].resources.sourceDirectories)\n    }\n}\n```\n\n### Running E2E Tests\n\n```bash\n# Run only e2e tests\n./gradlew e2eTest\n\n# Run unit tests (doesn't include e2e)\n./gradlew test\n\n# Run all tests\n./gradlew test e2eTest\n```\n\n### Benefits of Separate Source Set\n\n| Benefit | Description |\n|---------|-------------|\n| **Isolation** | E2E tests run independently from unit tests |\n| **CI Flexibility** | Run unit tests quickly, e2e tests separately or in parallel |\n| **Resource Management** | Different JVM settings for e2e tests (more memory, longer timeouts) |\n| **Clear Boundaries** | Developers know exactly where e2e tests live |\n\n!!! tip \"See Examples\"\n    Check the [recipes](https://github.com/Trendyol/stove/tree/main/recipes) folder for complete working examples with this structure.\n\n### Single Setup, Multiple Tests\n\n<span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">Configure Stove once for all tests:</span>\n\n```kotlin hl_lines=\"4 10 18\"\n// ✅ Good: Single configuration for all tests\nclass TestConfig : AbstractProjectConfig() {\n    override suspend fun beforeProject() {\n        Stove()\n            .with { /* configuration */ }\n            .run()\n    }\n    \n    override suspend fun afterProject() {\n        Stove.stop()\n    }\n}\n\n// ❌ Bad: Configuration per test class\nclass MyTest : FunSpec({\n    beforeSpec {\n        Stove().with { /* */ }.run()  // Don't do this!\n    }\n})\n```\n\n## Test Data Management\n\n### Use Unique Test Data\n\nGenerate unique identifiers to prevent test interference:\n\n```kotlin hl_lines=\"4 5 18\"\n// ✅ Good: Unique data per test\ntest(\"should create order\") {\n    val orderId = UUID.randomUUID().toString()\n    val userId = \"user-${UUID.randomUUID()}\"\n    \n    stove {\n        http {\n            postAndExpectBody<OrderResponse>(\n                uri = \"/orders\",\n                body = CreateOrderRequest(id = orderId, userId = userId).some()\n            ) { /* assertions */ }\n        }\n    }\n}\n\n// ❌ Bad: Hardcoded IDs that may conflict\ntest(\"should create order\") {\n    val orderId = \"order-123\"  // May conflict with other tests\n    // ...\n}\n```\n\n### Isolate Shared Infrastructure Resources\n\nWhen using provided instances (shared infrastructure in CI/CD), use **unique prefixes for all resources** to prevent parallel test runs from interfering with each other:\n\n```kotlin\nobject TestRunContext {\n    val runId: String = System.getenv(\"CI_JOB_ID\") \n        ?: UUID.randomUUID().toString().take(8)\n    \n    val databaseName = \"testdb_$runId\"\n    val topicPrefix = \"test_${runId}_\"\n    val indexPrefix = \"test_${runId}_\"\n}\n\n// Use unique names in configuration\nStove()\n    .with {\n        postgresql {\n            PostgresqlOptions.provided(\n                databaseName = TestRunContext.databaseName,\n                // ...\n            )\n        }\n        springBoot(\n            withParameters = listOf(\n                \"kafka.topic.orders=${TestRunContext.topicPrefix}orders\",\n                \"elasticsearch.index.products=${TestRunContext.indexPrefix}products\"\n            )\n        )\n    }\n```\n\n!!! tip \"Detailed Guide\"\n    See [Provided Instances - Test Isolation](Components/11-provided-instances.md#test-isolation-with-shared-infrastructure) for comprehensive examples for each system.\n\n### Use Cleanup Functions\n\nClean up test data to maintain isolation. The `cleanup` parameter is passed inside the options:\n\n```kotlin\nStove()\n    .with {\n        couchbase {\n            CouchbaseSystemOptions(\n                defaultBucket = \"bucket\",\n                cleanup = { cluster ->\n                    // Clean test data after tests complete\n                    cluster.query(\"DELETE FROM `bucket` WHERE type = 'test'\")\n                },\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"couchbase.hosts=${cfg.hostsWithPort}\",\n                        \"couchbase.username=${cfg.username}\",\n                        \"couchbase.password=${cfg.password}\"\n                    )\n                }\n            )\n        }\n        \n        kafka {\n            KafkaSystemOptions(\n                cleanup = { admin ->\n                    // Delete test topics after tests complete\n                    val testTopics = admin.listTopics().names().get()\n                        .filter { it.startsWith(\"test-\") }\n                    if (testTopics.isNotEmpty()) {\n                        admin.deleteTopics(testTopics).all().get()\n                    }\n                },\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"kafka.bootstrapServers=${cfg.bootstrapServers}\",\n                        \"kafka.interceptorClasses=${cfg.interceptorClass}\"\n                    )\n                }\n            )\n        }\n    }\n    .run()\n```\n\n### Test Data Builders\n\nCreate reusable test data builders:\n\n```kotlin\nobject TestData {\n    fun createUser(\n        id: String = UUID.randomUUID().toString(),\n        name: String = \"Test User\",\n        email: String = \"test-${UUID.randomUUID()}@example.com\"\n    ) = User(id = id, name = name, email = email)\n    \n    fun createProduct(\n        id: String = UUID.randomUUID().toString(),\n        name: String = \"Test Product\",\n        price: Double = 99.99\n    ) = Product(id = id, name = name, price = price)\n}\n\n// Usage in tests\ntest(\"should create user\") {\n    val user = TestData.createUser(name = \"John Doe\")\n    // ...\n}\n```\n\n## Assertions\n\n### Be Specific with Assertions\n\nTest specific behaviors, not just successful responses:\n\n```kotlin\n// ✅ Good: Specific assertions\nstove {\n    http {\n        postAndExpectBody<OrderResponse>(\n            uri = \"/orders\",\n            body = CreateOrderRequest(id = orderId, amount = 99.99).some()\n        ) { response ->\n            response.status shouldBe 201\n            response.body().id shouldBe orderId\n            response.body().amount shouldBe 99.99\n            response.body().status shouldBe \"CREATED\"\n            response.body().createdAt shouldNotBe null\n        }\n    }\n}\n\n// ❌ Bad: Only checking status code\nstove {\n    http {\n        postAndExpectBodilessResponse(\"/orders\", body = order.some()) { response ->\n            response.status shouldBe 201  // Not enough!\n        }\n    }\n}\n```\n\n### <span data-rn=\"underline\" data-rn-color=\"#009688\">Verify Side Effects</span>\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">Test the complete flow including side effects: make the request, then verify database state, published events, search index, and cache.</span>\n\n```kotlin hl_lines=\"8 17 24 31 38\"\ntest(\"should process order completely\") {\n    val orderId = UUID.randomUUID().toString()\n    \n    stove {\n        // 1. Make the request\n        http {\n            postAndExpectBody<OrderResponse>(\n                uri = \"/orders\",\n                body = CreateOrderRequest(id = orderId).some()\n            ) { response ->\n                response.status shouldBe 201\n            }\n        }\n        \n        // 2. Verify database state\n        couchbase {\n            shouldGet<Order>(\"orders\", orderId) { order ->\n                order.status shouldBe \"CREATED\"\n            }\n        }\n        \n        // 3. Verify event was published\n        kafka {\n            shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {\n                actual.orderId == orderId\n            }\n        }\n        \n        // 4. Verify search index updated\n        elasticsearch {\n            shouldGet<Order>(index = \"orders\", key = orderId) { order ->\n                order.status shouldBe \"CREATED\"\n            }\n        }\n        \n        // 5. Verify cache populated\n        redis {\n            client().connect().sync().get(\"order:$orderId\") shouldNotBe null\n        }\n    }\n}\n```\n\n## Performance\n\n### Use keepDependenciesRunning for Development\n\nSpeed up local development:\n\n```kotlin\nStove {\n    keepDependenciesRunning()  // Containers stay running between test runs\n}.with {\n    // ...\n}.run()\n```\n\n!!! tip\n    Disable `keepDependenciesRunning()` in CI/CD for clean environments.\n\n### Configure Appropriate Timeouts\n\nSet realistic timeouts for your environment:\n\n```kotlin\n// HTTP client timeout\nhttp {\n    HttpClientSystemOptions(\n        baseUrl = \"http://localhost:8080\",\n        timeout = 30.seconds  // Adjust based on your app's response times\n    )\n}\n\n// Kafka assertion timeout\nkafka {\n    shouldBePublished<OrderCreatedEvent>(atLeastIn = 20.seconds) {\n        // Allow enough time for async processing\n        actual.orderId == orderId\n    }\n}\n```\n\n### Run Tests in Parallel (With Care)\n\nIf running tests in parallel, ensure proper isolation:\n\n```kotlin\n// Use unique data per test\ntest(\"test 1\") {\n    val id = UUID.randomUUID().toString()  // Unique per test\n    // ...\n}\n\ntest(\"test 2\") {\n    val id = UUID.randomUUID().toString()  // Different ID\n    // ...\n}\n```\n\n## External Services\n\n### Mock External Dependencies\n\nUse WireMock for external services:\n\n```kotlin\n// ✅ Good: Mock external services\nstove {\n    wiremock {\n        mockPost(\n            url = \"/payments/charge\",\n            statusCode = 200,\n            responseBody = PaymentResult(success = true, transactionId = \"tx-123\").some()\n        )\n    }\n    \n    http {\n        postAndExpectBody<OrderResponse>(\n            uri = \"/orders\",\n            body = CreateOrderRequest(amount = 99.99).some()\n        ) { response ->\n            response.body().paymentStatus shouldBe \"PAID\"\n        }\n    }\n}\n\n// ❌ Bad: Calling real external services in tests\n// - Tests become flaky\n// - Tests are slow\n// - May incur costs\n// - Can't test edge cases\n```\n\n### Test Error Scenarios\n\n<span data-rn=\"highlight\" data-rn-color=\"#ff980055\" data-rn-duration=\"800\">Test how your application handles failures:</span>\n\n```kotlin\ntest(\"should handle payment failure gracefully\") {\n    stove {\n        wiremock {\n            mockPost(\n                url = \"/payments/charge\",\n                statusCode = 500,\n                responseBody = ErrorResponse(\"Payment service unavailable\").some()\n            )\n        }\n        \n        http {\n            postAndExpectBody<OrderResponse>(\n                uri = \"/orders\",\n                body = CreateOrderRequest(amount = 99.99).some()\n            ) { response ->\n                response.status shouldBe 503\n                response.body().status shouldBe \"PAYMENT_FAILED\"\n            }\n        }\n    }\n}\n\ntest(\"should retry on transient failures\") {\n    stove {\n        wiremock {\n            behaviourFor(\"/payments/charge\", WireMock::post) {\n                initially {\n                    aResponse().withStatus(503)\n                }\n                then {\n                    aResponse().withStatus(503)\n                }\n                then {\n                    aResponse()\n                        .withStatus(200)\n                        .withBody(it.serialize(PaymentResult(success = true)))\n                }\n            }\n        }\n        \n        // Application should retry and eventually succeed\n        http {\n            postAndExpectBody<OrderResponse>(\n                uri = \"/orders\",\n                body = CreateOrderRequest(amount = 99.99).some()\n            ) { response ->\n                response.status shouldBe 201\n            }\n        }\n    }\n}\n```\n\n## Serialization\n\n### Align Serializers\n\n<span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">Ensure Stove uses the same serialization as your application:</span>\n\n```kotlin\n// If your app uses custom Jackson configuration\nval customObjectMapper = ObjectMapper().apply {\n    registerModule(JavaTimeModule())\n    disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n    setSerializationInclusion(JsonInclude.Include.NON_NULL)\n}\n\nStove()\n    .with {\n        http {\n            HttpClientSystemOptions(\n                baseUrl = \"http://localhost:8080\",\n                contentConverter = JacksonConverter(customObjectMapper)\n            )\n        }\n        \n        kafka {\n            KafkaSystemOptions(\n                serde = StoveSerde.jackson.anyByteArraySerde(customObjectMapper)\n            ) { /* config */ }\n        }\n        \n        wiremock {\n            WireMockSystemOptions(\n                serde = StoveSerde.jackson.anyByteArraySerde(customObjectMapper)\n            )\n        }\n    }\n    .run()\n```\n\n## Application Configuration\n\n### Make Configuration Testable\n\nYour application should accept configuration from various sources:\n\n```kotlin\n// ✅ Good: Configurable properties\n@Configuration\nclass KafkaConfig(\n    @Value(\"\\${kafka.bootstrapServers}\") private val bootstrapServers: String,\n    @Value(\"\\${kafka.offset:latest}\") private val offset: String,\n    @Value(\"\\${kafka.autoCreateTopics:false}\") private val autoCreate: Boolean\n) {\n    // Stove can override these via command line args\n}\n```\n\n### External Service URLs Must Be Configurable\n\nWhen using WireMock, <span data-rn=\"box\" data-rn-color=\"#ef5350\">all external service URLs must point to WireMock's URL</span>:\n\n```kotlin hl_lines=\"4 5 16 17\"\n// ✅ Good: External service URLs are configurable\n@Configuration\nclass ExternalServicesConfig(\n    @Value(\"\\${payment.service.url}\") val paymentUrl: String,\n    @Value(\"\\${inventory.service.url}\") val inventoryUrl: String\n)\n\n// In tests, pass WireMock URL for all external services\nStove()\n    .with {\n        wiremock {\n            WireMockSystemOptions(port = 9090)\n        }\n        springBoot(\n            withParameters = listOf(\n                \"payment.service.url=http://localhost:9090\",\n                \"inventory.service.url=http://localhost:9090\"\n            )\n        )\n    }\n```\n\n```kotlin hl_lines=\"3\"\n// ❌ Bad: Hardcoded URLs won't be intercepted by WireMock\nclass PaymentClient {\n    private val url = \"http://payment-service.com\"  // WireMock can't intercept this!\n}\n```\n\n```kotlin hl_lines=\"4\"\n// ❌ Bad: Hardcoded values\n@Configuration\nclass KafkaConfig {\n    private val bootstrapServers = \"localhost:9092\"  // Can't change in tests!\n}\n```\n\n### Use Test Profiles Wisely\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">Minimize differences between test and production:</span>\n\n```kotlin\nspringBoot(\n    runner = { params -> myApp.run(params) },\n    withParameters = listOf(\n        \"server.port=8080\",\n        \"spring.profiles.active=default\",  // Use default profile when possible\n        \"logging.level.root=warn\",\n        // Override only what's necessary\n        \"kafka.bootstrapServers=${kafkaConfig.bootstrapServers}\"\n    )\n)\n```\n\n## Debugging\n\n### Enable Verbose Logging When Needed\n\n```kotlin\nspringBoot(\n    runner = { params -> myApp.run(params) },\n    withParameters = listOf(\n        \"logging.level.root=debug\",  // For debugging\n        \"logging.level.org.springframework.web=trace\"\n    )\n)\n```\n\n### Use Container Inspection\n\nDebug container issues:\n\n```kotlin\nstove {\n    mongodb {\n        val info = inspect()\n        println(\"Container ID: ${info?.containerId}\")\n        println(\"Network: ${info?.network}\")\n        println(\"IP: ${info?.ipAddress}\")\n    }\n}\n```\n\n### Access Application Beans\n\nDebug by accessing application components:\n\n```kotlin\nstove {\n    using<OrderRepository> {\n        val order = findById(orderId)\n        println(\"Order state: $order\")\n    }\n    \n    using<OrderService, PaymentService> { orderService, paymentService ->\n        // Debug complex scenarios\n    }\n}\n```\n\n## CI/CD Considerations\n\n### Use Provided Instances in CI\n\nFor faster CI builds, use pre-provisioned infrastructure:\n\n```kotlin\nval isCI = System.getenv(\"CI\") == \"true\"\n\nStove()\n    .with {\n        kafka {\n            if (isCI) {\n                KafkaSystemOptions.provided(\n                    bootstrapServers = System.getenv(\"KAFKA_SERVERS\"),\n                    configureExposedConfiguration = { cfg ->\n                        listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n                    }\n                )\n            } else {\n                KafkaSystemOptions {\n                    listOf(\"kafka.bootstrapServers=${it.bootstrapServers}\")\n                }\n            }\n        }\n    }\n    .run()\n```\n\n### Configure Docker Registry\n\nFor corporate environments:\n\n```kotlin\n// Set globally for all components\nDEFAULT_REGISTRY = System.getenv(\"DOCKER_REGISTRY\") ?: \"docker.io\"\n```\n\n### Handle Resource Constraints\n\nConfigure for CI resource limits:\n\n```kotlin\nStove()\n    .with {\n        couchbase {\n            CouchbaseSystemOptions(\n                container = CouchbaseContainerOptions(\n                    containerFn = { container ->\n                        container.withCreateContainerCmdModifier { cmd ->\n                            cmd.hostConfig?.withMemory(512 * 1024 * 1024)  // 512MB limit\n                        }\n                    }\n                )\n            ) { /* config */ }\n        }\n    }\n    .run()\n```\n\n## Common Anti-Patterns\n\n### ❌ Testing Implementation Details\n\n```kotlin\n// Bad: Testing internal implementation\nusing<OrderRepository> {\n    save(order)\n}\nshouldGet<Order>(orderId) { /* verify */ }\n\n// Good: Test through the API\nhttp {\n    postAndExpectBody<OrderResponse>(\"/orders\", body = order.some()) { /* verify */ }\n}\ncouchbase {\n    shouldGet<Order>(\"orders\", orderId) { /* verify */ }\n}\n```\n\n### ❌ Sleeping Instead of Waiting\n\n```kotlin hl_lines=\"4 9\"\n// Bad: Fixed sleep\nhttp { post(\"/async-operation\") }\nThread.sleep(5000)  // Fragile!\nkafka { shouldBeConsumed<Event> { true } }\n\n// Good: Poll with timeout\nkafka {\n    shouldBePublished<Event>(atLeastIn = 10.seconds) {\n        actual.id == expectedId\n    }\n}\n```\n\n### ❌ Sharing State Between Tests\n\n```kotlin hl_lines=\"2 5 9 14\"\n// Bad: Shared mutable state\nvar createdUserId: String? = null\n\ntest(\"create user\") {\n    createdUserId = createUser()\n}\n\ntest(\"get user\") {\n    getUser(createdUserId!!)  // Depends on test order!\n}\n\n// Good: Independent tests\ntest(\"create and get user\") {\n    val userId = createUser()\n    getUser(userId)\n}\n```\n\n### ❌ Overly Broad Assertions\n\n```kotlin\n// Bad: Too vague\nresponse.status shouldBe 200\n\n// Good: Specific assertions\nresponse.status shouldBe 200\nresponse.body().id shouldBe expectedId\nresponse.body().status shouldBe \"ACTIVE\"\nresponse.body().createdAt shouldNotBe null\n```\n\n## Summary\n\n| Do | Don't |\n|----|-------|\n| Use unique test data | Use hardcoded IDs |\n| Test through public APIs | Test implementation details |\n| Mock external services | Call real external services |\n| Use appropriate timeouts | Use fixed sleeps |\n| Clean up test data | Leave test artifacts |\n| Keep tests independent | Share state between tests |\n| Be specific in assertions | Use vague assertions |\n| Test error scenarios | Only test happy paths |\n"
  },
  {
    "path": "docs/blog/dashboard-0.23.0.md",
    "content": "# Stove Dashboard in 0.23.0: See Your E2E Runs Live\n\nEnd-to-end tests usually answer one question: pass or fail. When they fail, you jump between logs, traces, and broker/db tools to understand what happened.\n\nAs of **Stove 0.23.0**, you can use **Stove Dashboard** and the **`stove` CLI** to watch test execution in a local dashboard while tests are running.\n\nDashboard gives you:\n\n- a real-time timeline of test actions\n- distributed trace trees linked to tests\n- state snapshots across systems\n- persistent run history in SQLite\n\nInstead of treating failures as black boxes, you can inspect the full story in one place.\n\n<p align=\"center\">\n  <video width=\"900\" controls>\n    <source src=\"../../assets/stove_dashboard.webm\" type=\"video/webm\" />\n    Your browser does not support embedded videos. You can download the Stove Dashboard demo from\n    ../../assets/stove_dashboard.webm.\n  </video>\n</p>\n\n## Quick Setup (5 Minutes)\n\n### 1) Install and start the CLI\n\n```bash\nbrew install Trendyol/trendyol-tap/stove\nstove\n```\n\nBy default, the dashboard is at `http://localhost:4040` and gRPC receiver is at `localhost:4041`.\n\n### 2) Add the test dependency and tracing plugin\n\n```kotlin\n// build.gradle.kts\nplugins {\n  id(\"com.trendyol.stove.tracing\") version \"$stoveVersion\"\n}\n\ndependencies {\n  testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n  testImplementation(\"com.trendyol:stove-dashboard\")\n  testImplementation(\"com.trendyol:stove-tracing\")\n}\n\nstoveTracing {\n  serviceName.set(\"product-api\")\n}\n```\n\nThe tracing Gradle plugin attaches the OpenTelemetry agent to your test tasks, which is required for the dashboard's trace view.\n\n### 3) Register Dashboard in Stove config\n\n```kotlin\nStove()\n  .with {\n    dashboard { DashboardSystemOptions(appName = \"product-api\") }\n    tracing { enableSpanReceiver() } // recommended for trace view\n    // other systems: http, kafka, postgresql, wiremock...\n  }.run()\n```\n\n### 4) Run tests and open dashboard\n\n```bash\n./gradlew test\n```\n\nOpen `http://localhost:4040` and inspect runs as they stream in.\n\n## How to Use Dashboard During Debugging\n\nWhen a test fails (or behaves unexpectedly), this sequence is usually the fastest:\n\n1. **Timeline:** find the first failed action and inspect its input/output and expected/actual values.\n2. **Trace:** jump to the span tree to locate the failure point inside app call flow.\n3. **Snapshots:** confirm system state around the failure boundary.\n4. **Kafka Explorer:** verify published/consumed message counts and payloads.\n\nThis gives you both sides of the picture: test-level assertions and application-level execution details.\n\n## Daily Workflow That Works Well\n\nUse Dashboard as a local companion while iterating:\n\n1. Start CLI once: `stove`\n2. Keep it running in a separate terminal\n3. Run focused tests repeatedly (class or test-level)\n4. Inspect changes immediately in Timeline/Trace views\n5. Use Reporting + Tracing in CI; use Dashboard primarily for local debugging speed\n\nDashboard is fault-tolerant by design. If CLI is not running, tests continue normally and Dashboard emission auto-degrades without breaking test execution.\n\n## Minimal End-to-End Example\n\n```kotlin\nclass StoveConfig : AbstractProjectConfig() {\n  override suspend fun beforeProject() =\n    Stove()\n      .with {\n        dashboard { DashboardSystemOptions(appName = \"spring-example\") }\n        tracing { enableSpanReceiver() }\n        // other systems...\n      }.run()\n\n  override suspend fun afterProject() = Stove.stop()\n}\n```\n\nYou keep writing tests exactly as before; Dashboard captures entries/spans/snapshots automatically.\n\n## Troubleshooting Quick Checks\n\n- **UI stuck at waiting state:** ensure `stove` is running before tests.\n- **No events appear:** verify `stove-dashboard` dependency and `dashboard {}` registration.\n- **Port mismatch:** align `DashboardSystemOptions(cliPort = ...)` with CLI `--grpc-port`.\n- **Too much historical data:** run `stove --clear`.\n\n## Links\n\n- [Dashboard component docs](../Components/18-dashboard.md)\n- [0.23.0 release notes](../release-notes/0.23.0.md)\n- [Tracing component docs](../Components/15-tracing.md)\n- [Getting started](../getting-started.md)\n"
  },
  {
    "path": "docs/blog/polyglot-0.24.0.md",
    "content": "# Stove 0.24.0 — Going Polyglot, and an MCP for AI Triage\n\nStove 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.\n\nThis release pushes on five things at once: <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">Go becomes a first-class application under test</span>, <span data-rn=\"underline\" data-rn-color=\"#009688\">any container image can be the AUT</span>, 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 <span data-rn=\"underline\" data-rn-color=\"#ff9800\">`stove` CLI grows an MCP endpoint</span> so AI agents can triage failed runs without scraping logs.\n\n## Why polyglot, why now\n\nMicroservice 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.\n\nThe 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.\n\nSo 0.24.0 splits AUT lifecycle from test logic. Two new starters, the test surface unchanged:\n\n- **`stove-process`** runs your app as a host binary. Fast iteration, easy debugging.\n- **`stove-container`** runs your app as a Docker image. CI parity with the artifact you ship.\n\nBoth 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.\n\n## A tour: Go on Stove\n\nGo gets the deepest treatment because it's the showcase language. The [`go-showcase`](https://github.com/Trendyol/stove/tree/main/recipes/process/golang/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.\n\n### The Go side\n\nThe Go application stays small. All tracing is in the infrastructure layer — `otelhttp` wraps the mux, `otelsql` wraps the DB driver. Business handlers stay clean:\n\n```go title=\"handlers.go\"\nfunc handleCreateProduct(db *sql.DB, producer KafkaProducer) http.HandlerFunc {\n    return func(w http.ResponseWriter, r *http.Request) {\n        var req createProductRequest\n        json.NewDecoder(r.Body).Decode(&req)\n\n        product := Product{ID: uuid.New().String(), Name: req.Name, Price: req.Price}\n        insertProduct(r.Context(), db, product)  // otelsql traces this automatically\n\n        if producer != nil {\n            event := ProductCreatedEvent{ID: product.ID, Name: product.Name, Price: product.Price}\n            eventBytes, _ := json.Marshal(event)\n            producer.SendMessage(\"product.created\", product.ID, eventBytes)\n        }\n        writeJSON(w, http.StatusCreated, product)\n    }\n}\n```\n\nThe 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.\n\n### The Kotlin side\n\n```kotlin\ntest(\"create product, verify HTTP, DB, Kafka, traces\") {\n    stove {\n        var productId: String? = null\n\n        http {\n            postAndExpectBody<ProductResponse>(\n                uri = \"/api/products\",\n                body = CreateProductRequest(name = \"Test\", price = 42.99).some()\n            ) { actual ->\n                actual.status shouldBe 201\n                productId = actual.body().id\n            }\n        }\n\n        postgresql {\n            shouldQuery<ProductRow>(\n                query = \"SELECT id, name, price FROM products WHERE id = '$productId'\",\n                mapper = productRowMapper\n            ) { rows -> rows.size shouldBe 1 }\n        }\n\n        kafka {\n            shouldBePublished<ProductCreatedEvent>(10.seconds) {\n                actual.name == \"Test\"\n            }\n        }\n\n        tracing {\n            shouldContainSpan(\"http.request\")\n            shouldNotHaveFailedSpans()\n        }\n    }\n}\n```\n\nIf you removed the file path, you couldn't tell from this test that the AUT is in Go. That's the point.\n\n### Kafka, in three flavors\n\n`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`](https://github.com/trendyol/stove/tree/main/go/stove-kafka), a small Go library that forwards produced/consumed/committed messages over gRPC to Stove's observer.\n\nThe bridge is library-agnostic at its core. First-party integrations exist for the three Go Kafka clients people actually use:\n\n- [IBM/sarama](https://github.com/IBM/sarama) via `ProducerInterceptor` / `ConsumerInterceptor`\n- [twmb/franz-go](https://github.com/twmb/franz-go) via `kgo.WithHooks(...)`\n- [segmentio/kafka-go](https://github.com/segmentio/kafka-go) via tiny `ReportWritten` / `ReportRead` helpers\n\nWant confluent-kafka-go or something else? Skip the subpackages, use the core API:\n\n```go\nbridge.ReportPublished(ctx, &stovekafka.PublishedMessage{...})\nbridge.ReportConsumed(ctx,  &stovekafka.ConsumedMessage{...})\nbridge.ReportCommitted(ctx, topic, partition, offset+1)\n```\n\nIn 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.**\n\n### Coverage from black-box tests\n\nA 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.\n\n```bash\n./gradlew e2eTestWithCoverage -Pgo.coverage=true\n./gradlew e2eTest-containerWithCoverage -Pgo.coverage=true\n```\n\nPer-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()`:\n\n```go\nsignal.Ignore(syscall.SIGPIPE)\n```\n\nThat's it. No framework changes were needed for coverage; it's a Gradle concern, an env var, and an existing graceful-shutdown signal.\n\n## Container mode is not just for Go\n\n`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.\n\nOne thing worth being explicit about: <span data-rn=\"highlight\" data-rn-color=\"#ff980055\" data-rn-duration=\"800\">building the image is not Stove's job</span>. `containerApp(...)` only needs an image reference. Where it comes from is your call:\n\n- A tag your CI just produced (`-Papp.image=ghcr.io/acme/app:sha-abc`)\n- A pull from a registry, lazy on first use\n- An optional Gradle `Exec` task that runs `docker build` for local iteration\n\nMost teams already have a perfectly good image-build pipeline. Stove doesn't try to own it.\n\n```kotlin\ncontainerApp(\n    image = System.getProperty(\"app.container.image\"),\n    target = ContainerTarget.Server(\n        hostPort = 8090, internalPort = 8090,\n        portEnvVar = \"APP_PORT\", bindHostPort = false\n    ),\n    envProvider = envMapper {\n        \"database.host\" to \"DB_HOST\"\n        \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n        env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:4317\")\n    },\n    configureContainer = { withNetworkMode(\"host\") }\n)\n```\n\n`configureContainer { ... }` exposes the underlying Testcontainers `GenericContainer`, so anything Testcontainers can do — bind mounts, network mode, log consumers, capabilities — is available without bespoke API surface.\n\nA 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.\n\n## Black-box mode: testing apps Stove didn't start\n\nPolyglot 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.\n\n```kotlin\nStove().with {\n    httpClient { HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\") }\n\n    postgresql {\n        PostgresqlOptions.provided(\n            jdbcUrl = \"jdbc:postgresql://staging-db:5432/myapp\",\n            host = \"staging-db\", port = 5432,\n            configureExposedConfiguration = { emptyList() }\n        )\n    }\n\n    providedApplication {\n        ProvidedApplicationOptions(\n            readiness = ReadinessStrategy.HttpGet(url = \"https://staging.myapp.com/health\")\n        )\n    }\n}.run()\n```\n\nSame 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).\n\nThe 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.\n\n## Multiple instances of the same system, with keys\n\nMicroservice 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:\n\n```kotlin\nobject OrderService : SystemKey\nobject PaymentService : SystemKey\nobject AppDb : SystemKey\nobject AnalyticsDb : SystemKey\n\nStove().with {\n    httpClient { HttpClientSystemOptions(baseUrl = \"https://myapp.com\") }                       // default\n    httpClient(OrderService) { HttpClientSystemOptions(baseUrl = \"https://order.internal\") }\n    httpClient(PaymentService) { HttpClientSystemOptions(baseUrl = \"https://pay.internal\") }\n\n    postgresql(AppDb) { /* ... */ }\n    postgresql(AnalyticsDb) { /* ... */ }\n}.run()\n```\n\nIn tests, the same key drives the validation DSL:\n\n```kotlin\nhttp(OrderService) { getResponse(\"/api/orders/$orderId\") { /* ... */ } }\npostgresql(AnalyticsDb) { shouldQuery<AnalyticsEvent>(/* ... */) { /* ... */ } }\n```\n\nKeys are Kotlin `object`s — 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.\n\nThis 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.\n\n## MCP — failure triage for AI agents\n\nThe 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.\n\nIf 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.\n\nThe 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](https://modelcontextprotocol.io/) endpoint on the same `stove` CLI that exposes that data as structured tools:\n\n```text\n$ stove\nStove CLI v0.24.0 running\nUI:   http://localhost:4040\nREST: http://localhost:4040/api/v1\nMCP:  http://localhost:4040/mcp\ngRPC: localhost:4041\n```\n\nWire any MCP-capable agent at `http://localhost:4040/mcp`. Then the conversation looks like:\n\n```text\nAgent: stove_failures()\n  → 2 failed runs across go-showcase and order-service\nAgent: stove_failure_detail(run_id=\"...\", test_id=\"...\")\n  → compact failure packet: assertion, expected vs actual,\n    timeline of last 5 actions, exception class\nAgent: stove_trace(run_id=\"...\", test_id=\"...\")\n  → critical path: 4 spans, exception in PostgresOrderRepository.save\n```\n\nEight 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.\n\nTwo design decisions worth calling out:\n\n1. **`run_id + test_id` is 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.\n\n2. **Loopback only.** The `/mcp` endpoint accepts only localhost `Host`/`Origin` headers 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.\n\nIf MCP is unavailable, agents fall back to normal test output and logs — it's an optimization, not a dependency.\n\n## Putting it together\n\nStove 0.24.0 is one consistent picture, even though the changes touch four different surfaces:\n\n- 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.\n- 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.mode` flip.\n- 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.\n\nThree integrations, one feedback loop. That's the release.\n\n---\n\n## Getting started\n\nUpgrade the CLI:\n\n```bash\nbrew upgrade stove\n```\n\nAdd the modules you need to your test classpath:\n\n```kotlin\ntestImplementation(platform(\"com.trendyol:stove-bom:0.24.0\"))\ntestImplementation(\"com.trendyol:stove-process\")     // host binary\ntestImplementation(\"com.trendyol:stove-container\")   // Docker image\ntestImplementation(\"com.trendyol:stove-dashboard\")   // dashboard streaming\ntestImplementation(\"com.trendyol:stove-tracing\")     // distributed tracing\ntestImplementation(\"com.trendyol:stove-kafka\")       // Kafka assertions\n```\n\nFor Go Kafka assertions:\n\n```bash\ngo get github.com/trendyol/stove/go/stove-kafka\n```\n\n## Links\n\n- [Full 0.24.0 release notes](../release-notes/0.24.0.md)\n- [Other Languages & Stacks overview](../other-languages/index.md)\n- [Go Process Mode](../other-languages/go-process.md)\n- [Go Container Mode](../other-languages/go-container.md)\n- [MCP component docs](../Components/21-mcp.md)\n- [Dashboard component docs](../Components/18-dashboard.md)\n- [`go-showcase` recipe](https://github.com/Trendyol/stove/tree/main/recipes/process/golang/go-showcase) — process *and* container modes in one repo\n- [`stove-kafka` Go bridge](https://github.com/Trendyol/stove/tree/main/go/stove-kafka)\n"
  },
  {
    "path": "docs/blog/tracing-0.21.0.md",
    "content": "# Execution Tracing in Stove 0.21.0\n\nIf you've spent any time debugging e2e test failures, you know the routine. The test says <span data-rn=\"box\" data-rn-color=\"#ef5350\">\"expected 201 but was 500\"</span> and you're left reverse-engineering what actually happened. Did the request reach the controller? Did the database reject the write? Did a downstream service return something unexpected? You open the logs, grep for request IDs, cross-reference timestamps, and eventually piece together the story. Twenty minutes later, you have an answer.\n\nThe fundamental problem is that e2e tests treat the application as a black box. They can tell you the output was wrong, but they have no visibility into the execution path that produced it. For simple flows that's fine. For a request that touches a gRPC service, two REST APIs, a database, and a Kafka topic before returning a response, it's a real productivity drain. In a microservice architecture with multiple integration points, this kind of failure can easily take 30 minutes to diagnose. Multiply that by every flaky test in your CI pipeline, and the cost adds up fast.\n\nStove 0.21.0 introduces execution tracing to address this. When a test fails, you get the <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">entire call chain</span> of your application: every controller method, every database query, every Kafka message, every HTTP call, with timing and the exact point of failure. The bug might be buried deep in the persistence layer, but the trace pinpoints it without a single grep.\n\n## Stove in 30 Seconds\n\nFor those new to [Stove](https://github.com/Trendyol/stove): it's an end-to-end testing framework for the JVM. It spins up your **real application** with **real dependencies** (PostgreSQL, Kafka, MongoDB, Redis, etc. via Testcontainers) and gives you a unified Kotlin DSL for assertions across all of them. It works with Spring Boot, Ktor, Micronaut, and Quarkus. Tests can be written in Kotlin, Java, or Scala.\n\nThe key idea: test your entire application stack as it runs in production, not a stripped-down mock version.\n\n## A Real Application: The Spring Showcase\n\nTo demonstrate tracing, let's walk through a realistic application. The [spring-showcase](https://github.com/Trendyol/stove/tree/main/recipes/jvm/kotlin-recipes/spring-showcase) recipe is an order service that touches six different integration points during a single request:\n\n```mermaid\nflowchart LR\n    A[\"HTTP POST /api/orders\"] --> B[OrderService]\n    B --> C[\"Fraud Detection (gRPC)\"]\n    B --> D[\"Inventory Check (REST)\"]\n    B --> E[\"Payment (REST)\"]\n    B --> F[\"PostgreSQL - Save Order\"]\n    B --> G[\"Kafka - Publish Events\"]\n    B --> H[\"db-scheduler - Schedule Email\"]\n```\n\nHere's the service code. Each method is annotated with `@WithSpan` so the <span data-rn=\"underline\" data-rn-color=\"#ff9800\">OpenTelemetry</span> agent captures it:\n\n```kotlin hl_lines=\"11\"\n@Service\nclass OrderService(\n  private val orderRepository: OrderRepository,\n  private val inventoryClient: InventoryClient,\n  private val paymentClient: PaymentClient,\n  private val fraudDetectionClient: FraudDetectionClient,\n  private val eventPublisher: OrderEventPublisher,\n  private val emailSchedulerService: EmailSchedulerService\n) {\n  @WithSpan(\"OrderService.createOrder\")\n  suspend fun createOrder(userId: String, productId: String, amount: Double): Order {\n    // Step 1: Check fraud via gRPC\n    checkFraudViaGrpc(orderId, userId, amount, productId)\n\n    // Step 2: Check inventory via REST\n    checkInventoryViaRest(productId)\n\n    // Step 3: Process payment via REST\n    val payment = processPaymentViaRest(userId, amount)\n\n    // Step 4: Save to database\n    val savedOrder = saveOrderToDatabase(orderId, userId, productId, amount, payment.transactionId!!)\n\n    // Step 5: Publish events to Kafka\n    publishEventsToKafka(savedOrder, payment.transactionId)\n\n    // Step 6: Schedule confirmation email\n    scheduleConfirmationEmail(savedOrder)\n\n    return savedOrder\n  }\n}\n```\n\nAnd here's how the Stove test covers the entire flow in a single test:\n\n```kotlin hl_lines=\"2 4 5 16 25 26 34 46 56 64 72\"\ntest(\"The Complete Order Flow - Every Feature in One Test\") {\n  stove {\n    // 1. Mock the external gRPC service (Fraud Detection)\n    grpcMock {\n      mockUnary(\n        serviceName = \"frauddetection.FraudDetectionService\",\n        methodName = \"CheckFraud\",\n        response = CheckFraudResponse.newBuilder()\n          .setIsFraudulent(false)\n          .setRiskScore(0.15)\n          .build()\n      )\n    }\n\n    // 2. Mock the external REST APIs (Inventory + Payment)\n    wiremock {\n      mockGet(url = \"/inventory/$productId\", statusCode = 200,\n        responseBody = InventoryResponse(productId, available = true, quantity = 10).some())\n      mockPost(url = \"/payments/charge\", statusCode = 200,\n        responseBody = PaymentResult(success = true, transactionId = \"txn-123\", amount = amount).some())\n    }\n\n    // 3. Call our API\n    http {\n      postAndExpectBody<OrderResponse>(uri = \"/api/orders\",\n        body = CreateOrderRequest(userId, productId, amount).some()\n      ) { response ->\n        response.status shouldBe 201\n        response.body().status shouldBe \"CONFIRMED\"\n      }\n    }\n\n    // 4. Verify database state\n    postgresql {\n      shouldQuery<OrderRow>(\n        query = \"SELECT * FROM orders WHERE user_id = '$userId'\",\n        mapper = { row -> OrderRow(/* ... */) }\n      ) { orders ->\n        orders.size shouldBe 1\n        orders.first().status shouldBe \"CONFIRMED\"\n      }\n    }\n\n    // 5. Verify Kafka events\n    kafka {\n      shouldBePublished<OrderCreatedEvent> {\n        actual.userId == userId && actual.productId == productId\n      }\n      shouldBePublished<PaymentProcessedEvent> {\n        actual.amount == amount && actual.success\n      }\n    }\n\n    // 6. Verify the consumer updated the read model (CQRS)\n    kafka {\n      shouldBeConsumed<OrderCreatedEvent> {\n        actual.userId == userId\n      }\n    }\n\n    // 7. Test our gRPC server\n    grpc {\n      channel<OrderQueryServiceCoroutineStub> {\n        val order = getOrder(GetOrderRequest.newBuilder().setOrderId(orderId!!).build())\n        order.found shouldBe true\n      }\n    }\n\n    // 8. Verify scheduled tasks\n    tasks {\n      shouldBeExecuted<OrderEmailPayload> {\n        this.orderId == orderId && this.userId == userId\n      }\n    }\n  }\n}\n```\n\n<span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">One test covering eight integration points against real infrastructure.</span>\n\n## Setting Up Tracing\n\nTracing takes <span data-rn=\"underline\" data-rn-color=\"#009688\">two configuration steps</span>.\n\n### Step 1: Enable in your Stove config\n\n```kotlin hl_lines=\"3-4\"\nStove()\n  .with {\n    tracing {\n      enableSpanReceiver()\n    }\n    // ... your other systems (http, kafka, postgresql, etc.)\n  }\n  .run()\n```\n\n### Step 2: Attach the OpenTelemetry agent in your build\n\nCopy [`StoveTracingConfiguration.kt`](https://github.com/Trendyol/stove/blob/main/buildSrc/src/main/kotlin/com/trendyol/stove/gradle/StoveTracingConfiguration.kt) to your project's `buildSrc/src/main/kotlin/` directory, then add to your `build.gradle.kts`:\n\n```kotlin hl_lines=\"3-5\"\nimport com.trendyol.stove.gradle.stoveTracing\n\nstoveTracing {\n  serviceName = \"my-service\"\n  testTaskNames = listOf(\"e2eTest\") // optional: scope to specific test tasks\n}\n```\n\nThis handles downloading the OpenTelemetry Java Agent, configuring JVM arguments, attaching the agent to your test tasks, and dynamically assigning ports so parallel test runs don't conflict.\n\n!!! tip \"Gradle Plugin available since 0.21.2\"\n    Starting with [0.21.2](../release-notes/0.21.2.md), a standalone Gradle plugin is available that eliminates the need to copy this file. See the [0.21.2 release notes](../release-notes/0.21.2.md) for details.\n\n<span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">No code changes to your application are needed.</span> The OpenTelemetry agent instruments 100+ libraries (Spring, JDBC, Kafka, gRPC, HTTP clients, Redis, MongoDB, and more) automatically. The `@WithSpan` annotations are optional. They add your own method-level spans on top of what the agent already captures.\n\n## What Happens When a Test Fails\n\nTo see this in practice, we ran the spring-showcase with a bug deliberately injected in the persistence layer: a validation that rejects orders over $1000. The test output included the full execution report:\n\n<div class=\"stove-report\" data-rn-group>\n<pre tabindex=\"0\"><code>╔═════════════════════════════════════════════════════════════════════════════\n                        STOVE TEST EXECUTION REPORT\n\n Test: The Complete Order Flow - Every Feature in One Test\n ID:   TheShowcase::The Complete Order Flow - Every Feature in One Test\n <span data-rn=\"highlight\" data-rn-color=\"#ef535055\" data-rn-duration=\"800\">Status: FAILED</span>\n╠═════════════════════════════════════════════════════════════════════════════\n\n TIMELINE\n ────────\n\n 17:27:22.298 ✓ PASSED [gRPC Mock] Register unary stub: FraudDetectionService/CheckFraud\n     Output: risk_score: 0.15 reason: \"low_risk_user\"\n\n 17:27:22.335 ✓ PASSED [WireMock] Register stub: GET /inventory/macbook-pro-16\n     Metadata: {statusCode=200}\n\n 17:27:22.341 ✓ PASSED [WireMock] Register stub: POST /payments/charge\n     Metadata: {statusCode=200}\n\n <span data-rn=\"box\" data-rn-color=\"#ef5350\">17:27:25.092 ✗ FAILED [HTTP] POST /api/orders</span>\n     Input:  CreateOrderRequest(userId=user-4b9bb522, productId=macbook-pro-16, amount=2499.99)\n     Output: {\"message\":\"Internal server error\",\"errorCode\":\"INTERNAL_ERROR\"}\n     Metadata: {status=500}\n     Expected: Response&lt;OrderResponse&gt; matching expectation\n     <span data-rn=\"underline\" data-rn-color=\"#ef5350\" data-rn-duration=\"400\">Error: expected:&lt;201&gt; but was:&lt;500&gt;</span>\n\n╠═════════════════════════════════════════════════════════════════════════════\n\n SYSTEM SNAPSHOTS\n ────────────────\n\n ┌─ GRPC MOCK ────────────────────────────\n   Registered stubs: 1\n   Received requests: 1\n   Matched requests: 1\n\n ┌─ WIREMOCK ─────────────────────────────\n   Registered stubs (this test): 2\n   Served requests (this test): 2 (matched: 2)\n\n ┌─ KAFKA ────────────────────────────────\n   Consumed: 0\n   <span data-rn=\"highlight\" data-rn-color=\"#ff980055\" data-rn-duration=\"800\">Published: 0</span>\n   Failed: 0\n\n╚═════════════════════════════════════════════════════════════════════════════</code></pre>\n</div>\n\nThe report is structured in two parts. First, a timeline of every test step showing what passed and what failed. Then, a snapshot of each system's state at the moment of failure. You can already read the situation: the gRPC mock matched its request, WireMock served both stubs successfully, but Kafka has zero messages. The application crashed before it could publish any events.\n\nBelow the report, the **execution trace** shows what happened inside the application:\n\n<div class=\"stove-report\" data-rn-group>\n<pre tabindex=\"0\"><code>═══════════════════════════════════════════════════════════════\nEXECUTION TRACE (Call Chain)\n═══════════════════════════════════════════════════════════════\n\n✓ POST /api/orders [250ms]\n├── ✓ OrderService.createOrder [245ms]\n│   ├── ✓ OrderService.checkFraudViaGrpc [30ms]\n│   │   └── ✓ FraudDetectionClient.checkFraud [25ms]\n│   ├── ✓ OrderService.checkInventoryViaRest [40ms]\n│   │   └── http.url: http://localhost:54648/inventory/macbook-pro-16\n│   ├── ✓ OrderService.processPaymentViaRest [35ms]\n│   │   └── http.url: http://localhost:54648/payments/charge\n│   <span data-rn=\"box\" data-rn-color=\"#ef5350\">├── ✗ OrderService.saveOrderToDatabase [8ms]  ◄── FAILURE POINT</span>\n│   │   └── ✗ PostgresOrderRepository.save [5ms]\n│   │       │  <span data-rn=\"highlight\" data-rn-color=\"#ef535055\">Error: OrderPersistenceException</span>\n│   │       │  <span data-rn=\"highlight\" data-rn-color=\"#ffd54f77\" data-rn-duration=\"800\">Message: Failed to persist order: amount exceeds internal threshold</span>\n│   │       │    at PostgresOrderRepository.validateOrderAmount(PostgresOrderRepository.kt:102)\n│   │       └── db.system: postgresql</code></pre>\n</div>\n\nThe fraud, inventory, and payment steps all passed. The failure happened in `OrderService.saveOrderToDatabase`, specifically in `PostgresOrderRepository.save`, with the exception type, message, and stack trace right there. Without tracing, this would have been a 500 error with no context. With tracing, the root cause is <span data-rn=\"underline\" data-rn-color=\"#009688\">immediately visible</span>.\n\n## Automatic Trace Propagation\n\nStove injects trace headers into every outgoing interaction without any test code changes:\n\n- **HTTP requests** get a `traceparent` header\n- **Kafka messages** get trace headers\n- **gRPC calls** get trace metadata\n\nThis is visible in the actual test output. The HTTP request sent by Stove:\n\n```\nREQUEST: http://localhost:8024/api/orders\nMETHOD: POST\nHEADERS:\n  Accept: application/json\n  X-Stove-Test-Id: TheShowcase::The Complete Order Flow - Every Feature in One Test\n  traceparent: 00-475e686523af0b4ee0433f91a69a6b55-81edd5ba7e4dec42-01\n```\n\nAnd the WireMock request log confirming the propagation reached the downstream call:\n\n```\nRequest received:\n127.0.0.1 - GET /inventory/macbook-pro-16\n  traceparent: [00-475e686523af0b4ee0433f91a69a6b55-e3f138ac02509a0b-01]\n```\n\nSame trace ID (`475e686523af0b4ee0433f91a69a6b55`), different span ID. The entire call chain is correlated.\n\n## Per-Test Trace Isolation\n\nA critical detail: <span data-rn=\"box\" data-rn-color=\"#009688\">every test gets its own trace</span>. Stove generates a unique trace ID at the start of each test and injects it into every outgoing interaction. All spans collected during that test are correlated back to that trace ID and that test alone.\n\nThis means traces from concurrent or sequential tests never bleed into each other. When a test fails, the execution trace shows *only* what happened during that specific test, not spans from a previous test that happened to use the same Kafka topic or a background job triggered by an earlier request.\n\nThis is not something you get for free with OpenTelemetry. In production, a trace starts when a request enters the system. In testing, there's no natural entry point. Stove creates one. It manages the W3C trace context lifecycle (start, propagate, end) per test, ties it to the test identity (`X-Stove-Test-Id` header), and ensures the OTLP receiver maps incoming spans to the correct test. The result is that tracing in Stove is <span data-rn=\"underline\" data-rn-color=\"#ff9800\">deterministic and test-scoped</span>, not a sampling-based best-effort like production tracing.\n\n## Trace Validation DSL\n\nBeyond automatic failure reports, you can actively assert on the execution flow using the `tracing { }` DSL. This is useful when you want to verify *how* your application handled a request, not just *that* it produced the right output:\n\n```kotlin hl_lines=\"11 12 13 14 17 20 23\"\ntest(\"order processing should call all expected services\") {\n  stove {\n    http {\n      postAndExpectBody<OrderResponse>(\"/api/orders\", request.some()) { response ->\n        response.status shouldBe 201\n      }\n    }\n\n    tracing {\n      // Verify which operations happened\n      shouldContainSpan(\"OrderService.createOrder\")\n      shouldContainSpan(\"OrderService.checkFraudViaGrpc\")\n      shouldContainSpan(\"OrderService.checkInventoryViaRest\")\n      shouldContainSpan(\"PostgresOrderRepository.save\")\n\n      // Verify no operations failed\n      shouldNotHaveFailedSpans()\n\n      // Performance assertions\n      executionTimeShouldBeLessThan(500.milliseconds)\n\n      // Attribute assertions\n      shouldHaveSpanWithAttribute(\"db.system\", \"postgresql\")\n\n      // Debugging helpers\n      println(renderTree())    // Print the hierarchical tree\n      println(renderSummary()) // Print compact summary\n    }\n  }\n}\n```\n\nThe DSL supports:\n\n- **Span assertions**: `shouldContainSpan()`, `shouldNotContainSpan()`, `shouldContainSpanMatching()`\n- **Failure assertions**: `shouldNotHaveFailedSpans()`, `shouldHaveFailedSpan()`\n- **Performance assertions**: `executionTimeShouldBeLessThan()`, `spanCountShouldBeAtLeast()`\n- **Attribute assertions**: `shouldHaveSpanWithAttribute()`, `shouldHaveSpanWithAttributeContaining()`\n- **Query methods**: `findSpanByName()`, `getFailedSpans()`, `getTotalDuration()`\n- **Async support**: `waitForSpans(expectedCount, timeoutMs)` for async flows\n\n## How It Works\n\n```mermaid\nsequenceDiagram\n    participant Test as Stove Test\n    participant App as Application\n    participant OTel as OTel Agent\n    participant Receiver as OTLP Receiver\n    participant Report as Report Builder\n\n    Test->>App: HTTP POST with traceparent\n    OTel->>OTel: Instrument libraries\n    App->>App: Process request\n    OTel->>Receiver: Export spans via OTLP gRPC\n    Receiver->>Receiver: Correlate spans by trace ID\n\n    alt Test passes\n        Test->>Test: Traces available via DSL\n    else Test fails\n        Report->>Receiver: Query spans for this test\n        Report->>Report: Build report + trace tree\n        Report->>Test: Display combined report\n    end\n```\n\nThe architecture:\n\n1. **OpenTelemetry Java Agent** attaches to your application process (configured via Gradle) and instruments 100+ libraries without code changes\n2. **Stove starts an OTLP gRPC receiver** on a dynamically assigned port that collects spans exported by the agent\n3. **W3C `traceparent` headers** are injected into every HTTP, Kafka, and gRPC interaction, correlating all spans back to the originating test\n4. **On test failure**, the report builder queries the collected spans, builds a hierarchical tree, and renders it alongside the execution report\n5. **Ports are dynamically assigned** so parallel test runs on CI don't conflict\n\nWorth noting: the OTel agent does add some startup overhead to the test JVM (a few seconds). For most e2e test suites that spin up Testcontainers, this is negligible relative to container startup time. If it matters, tracing can be toggled off with `enabled = false` in the Gradle config.\n\n## Practical Advice\n\n1. **Enable tracing by default.** The overhead is minimal compared to container startup, and the diagnostic value on failure is significant.\n2. **Use `tracing { }` sparingly.** The automatic failure reports cover most debugging needs. Reserve the DSL for cases where you want to assert on the execution flow itself, for example verifying that a cache was hit instead of the database.\n3. **Start with `shouldNotHaveFailedSpans()`.** The simplest assertion that catches unexpected errors anywhere in the call chain.\n4. **Filter noisy instrumentations.** Some libraries generate a lot of spans. Tune with `disabledInstrumentations`:\n\n```kotlin hl_lines=\"3-4\"\nstoveTracing {\n  serviceName = \"my-service\"\n  disabledInstrumentations = listOf(\"jdbc\", \"hibernate\", \"spring-scheduling\")\n}\n```\n\n## Getting Started\n\nAdd the dependencies:\n\n```kotlin hl_lines=\"6 8 9\"\ndependencies {\n  testImplementation(platform(\"com.trendyol:stove-bom:0.21.0\"))\n\n  testImplementation(\"com.trendyol:stove\")\n  testImplementation(\"com.trendyol:stove-spring\")       // or stove-ktor, stove-micronaut\n  testImplementation(\"com.trendyol:stove-tracing\")\n  testImplementation(\"com.trendyol:stove-extensions-kotest\") // or stove-extensions-junit\n  // Add components as needed: stove-postgres, stove-kafka, stove-http, etc.\n}\n```\n\nEnable tracing in two steps:\n\n```kotlin hl_lines=\"3 6\"\n// build.gradle.kts\nimport com.trendyol.stove.gradle.stoveTracing\n\nstoveTracing {\n  serviceName = \"my-service\"\n}\n\n// Stove config\ntracing {\n  enableSpanReceiver()\n}\n```\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">For a complete working example, see the [spring-showcase recipe](https://github.com/Trendyol/stove/tree/main/recipes/jvm/kotlin-recipes/spring-showcase). It demonstrates all Stove features together (HTTP, gRPC, Kafka, PostgreSQL, WireMock, db-scheduler, and tracing) in a realistic Spring Boot application.</span>\n\n---\n\n**Links:**\n\n- [Stove on GitHub](https://github.com/Trendyol/stove)\n- [Tracing documentation](../Components/15-tracing.md)\n- [Spring Showcase recipe](https://github.com/Trendyol/stove/tree/main/recipes/jvm/kotlin-recipes/spring-showcase)\n- [Full 0.21.0 release notes](../release-notes/0.21.0.md)\n- [Getting started guide](../getting-started.md)\n"
  },
  {
    "path": "docs/css/custom.css",
    "content": "/* Wider content area */\n.md-grid {\n    max-width: 1400px;\n    margin-top: 0;\n}\n\n/* Responsive images */\nimg {\n    width: 100%;\n}\n\n/* Social link styling */\n.md-social__link {\n    width: max-content;\n}\n\n/* Code block refinements */\n.highlight code {\n    font-size: 0.68rem;\n    line-height: 1.45;\n}\n\n/* Slightly tighter line height in code for density */\ncode {\n    font-size: 0.78em;\n}\n\n/* Admonition title font weight */\n.md-typeset .admonition-title {\n    font-weight: 600;\n}\n\n/* Code block hl_lines: override default background for RoughNotation */\n.highlight pre .hll {\n    background-color: transparent !important;\n}\n.highlight pre {\n    position: relative;\n}\n/* Clip RoughNotation SVGs that extend beyond code block edges */\n.highlight {\n    overflow: hidden;\n}\n/* Contain annotation SVGs within the code block */\n.highlight pre > svg,\n.highlight code > svg {\n    pointer-events: none;\n}\n\n/* Stove annotated report blocks (RoughNotation) */\n.stove-report pre {\n    background: var(--md-code-bg-color);\n    color: var(--md-code-fg-color);\n    font-size: .68rem;\n    line-height: 1.45;\n    padding: 1em 1.2em;\n    border-radius: .1rem;\n    position: relative;\n    overflow-x: auto;\n}\n\n.stove-report pre code {\n    font-family: var(--md-code-font-family, \"Roboto Mono\", monospace);\n    background: transparent;\n    padding: 0;\n    font-size: inherit;\n    color: inherit;\n}\n"
  },
  {
    "path": "docs/frameworks/index.md",
    "content": "# Supported Frameworks\n\nStove keeps the testing model consistent across frameworks, but application startup is framework-specific. Pick the starter that matches your runtime, then keep the rest of the test DSL the same.\n\n## Pick Your Starter\n\n<div class=\"grid cards\" markdown>\n\n-   :material-sprout: **Spring Boot**\n\n    For applications built on Spring Boot. Supports `bridge()` for direct bean access.\n\n    [Open the Spring Boot guide](spring-boot.md)\n\n-   :material-lightning-bolt: **Ktor**\n\n    For applications built on Ktor. Supports `bridge()` for direct bean access.\n\n    [Open the Ktor guide](ktor.md)\n\n-   :material-hexagon-outline: **Micronaut**\n\n    For applications built on Micronaut. Supports `bridge()` for direct bean access.\n\n    [Open the Micronaut guide](micronaut.md)\n\n-   :material-fire: **Quarkus**\n\n    For applications built on Quarkus. `bridge()` is not available yet.\n\n    [Open the Quarkus guide](quarkus.md)\n\n</div>\n\n## How to Choose\n\nPick the starter that matches your application's framework — that's it. The test DSL, components, and assertions work the same way regardless of which starter you use.\n\n- **`bridge()` support**: available in Spring Boot, Ktor, and Micronaut starters. Not yet available in Quarkus.\n\n## At A Glance\n\n| Framework | Starter | Entrypoint style | Bridge | Example |\n|-----------|---------|------------------|--------|---------|\n| Spring Boot | `stove-spring` | `runApplication(...)` wrapped in `run(args)` | Yes | [spring-example](https://github.com/Trendyol/stove/tree/main/examples/spring-example) |\n| Ktor | `stove-ktor` | `embeddedServer(...)` wrapped in `run(args)` | Yes | [ktor-example](https://github.com/Trendyol/stove/tree/main/examples/ktor-example) |\n| Micronaut | `stove-micronaut` | `ApplicationContext` startup wrapped in `run(args)` | Yes | [micronaut-example](https://github.com/Trendyol/stove/tree/main/examples/micronaut-example) |\n| Quarkus | `stove-quarkus` | `@QuarkusMain` entrypoint plus `Quarkus.run(*args)` | Not yet | [quarkus-example](https://github.com/Trendyol/stove/tree/main/examples/quarkus-example) |\n\n## What Stays The Same\n\nNo matter which starter you pick:\n\n- Stove starts your physical dependencies first\n- component configuration still comes from the same `Stove().with { ... }` DSL\n- reporting and tracing still integrate the same way\n- you can mix Kafka, PostgreSQL, WireMock, HTTP, gRPC, Redis, and other components\n\nIf you are new to Stove, start with [Getting Started](../getting-started.md) first, then come back here to pick the framework-specific setup.\n"
  },
  {
    "path": "docs/frameworks/ktor.md",
    "content": "# Ktor\n\n`stove-ktor` is the starter for applications built on Ktor. Stove starts the real Ktor application and keeps the test setup in one place.\n\n## Dependency\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-ktor\")\n}\n```\n\n## Application Entrypoint\n\nExpose a reusable `run` function and return the started `Application`. The exact shape depends on your DI framework:\n\n=== \"Koin\"\n\n    ```kotlin\n    fun main(args: Array<String>) {\n      run(args, shouldWait = true)\n    }\n\n    fun run(\n      args: Array<String>,\n      shouldWait: Boolean = false,\n      testModules: List<Module> = emptyList()\n    ): Application {\n      val config = loadConfiguration<AppConfiguration>(args)\n\n      val applicationEngine = embeddedServer(Netty, port = config.port, host = \"localhost\") {\n        install(Koin) {\n          modules(appModule, *testModules.toTypedArray())\n        }\n        configureRouting()\n      }\n\n      applicationEngine.start(wait = shouldWait)\n      return applicationEngine.application\n    }\n    ```\n\n=== \"Ktor-DI\"\n\n    ```kotlin\n    fun main(args: Array<String>) {\n      run(args, shouldWait = true)\n    }\n\n    fun run(\n      args: Array<String>,\n      shouldWait: Boolean = false,\n      testDependencies: (DependencyRegistrar.() -> Unit)? = null\n    ): Application {\n      val config = loadConfiguration<AppConfiguration>(args)\n\n      val applicationEngine = embeddedServer(Netty, port = config.port, host = \"localhost\") {\n        install(DI) {\n          dependencies {\n            provide<MyService> { MyServiceImpl() }\n            testDependencies?.invoke(this)\n          }\n        }\n        configureRouting()\n      }\n\n      applicationEngine.start(wait = shouldWait)\n      return applicationEngine.application\n    }\n    ```\n\n## Minimal Stove Setup\n\n```kotlin\nStove()\n  .with {\n    ktor(\n      runner = { params -> run(params, shouldWait = false) },\n      withParameters = listOf(\"port=8080\")\n    )\n  }\n  .run()\n```\n\n## What You Get\n\n- real Ktor startup from your own server bootstrap\n- `bridge()` support with automatic DI detection\n- easy composition with Kafka, databases, WireMock, tracing, and HTTP assertions\n\n## Bridge and DI Support\n\nKtor Bridge automatically detects which DI framework your application uses at runtime:\n\n| DI Framework | Detection | Priority |\n|-------------|-----------|----------|\n| **Ktor-DI** | `dependencies { ... }` is active in the application | Preferred when both are present |\n| **Koin** | `install(Koin) { ... }` is active | Used when Ktor-DI is not active |\n| **Custom** | Manual resolver provided via `bridge { app, type -> ... }` | Explicit override |\n\n### Registering Test Dependencies\n\n=== \"Koin\"\n\n    Pass test modules that override production beans:\n\n    ```kotlin\n    Stove()\n      .with {\n        bridge()\n        ktor(\n          runner = { params ->\n            run(\n              params,\n              shouldWait = false,\n              testModules = listOf(\n                module {\n                  single<TimeProvider>(override = true) { FixedTimeProvider() }\n                }\n              )\n            )\n          }\n        )\n      }\n      .run()\n    ```\n\n=== \"Ktor-DI\"\n\n    Pass a lambda that registers test overrides (later `provide<T>` calls override earlier ones):\n\n    ```kotlin\n    Stove()\n      .with {\n        bridge()\n        ktor(\n          runner = { params ->\n            run(params, shouldWait = false) {\n              provide<TimeProvider> { FixedTimeProvider() }\n            }\n          }\n        )\n      }\n      .run()\n    ```\n\n=== \"Custom Resolver\"\n\n    For other DI frameworks (Kodein, Dagger, etc.), provide a custom resolver:\n\n    ```kotlin\n    Stove()\n      .with {\n        bridge { application, type ->\n          myDiContainer.resolve(type)\n        }\n        ktor(runner = { params -> run(params, shouldWait = false) })\n      }\n      .run()\n    ```\n\n### Using Bridge in Tests\n\n```kotlin\nstove {\n  using<UserService> {\n    val user = findById(123)\n    user.name shouldBe \"John\"\n  }\n\n  using<List<PaymentService>> {\n    forEach { service -> service.validate() }\n  }\n}\n```\n\nSee the [Bridge documentation](../Components/10-bridge.md) for complete usage patterns including multi-bean access, value capture, and generic type resolution.\n\n## Example\n\n- [ktor-example](https://github.com/Trendyol/stove/tree/main/examples/ktor-example)\n"
  },
  {
    "path": "docs/frameworks/micronaut.md",
    "content": "# Micronaut\n\n`stove-micronaut` is the starter for applications built on Micronaut. It uses the same Stove DSL as the other starters.\n\n## Dependency\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-micronaut\")\n}\n```\n\n## Application Entrypoint\n\nExpose a reusable `run` function that returns the started `ApplicationContext`:\n\n```kotlin\nfun main(args: Array<String>) {\n  run(args)\n}\n\nfun run(\n  args: Array<String>,\n  init: ApplicationContext.() -> Unit = {}\n): ApplicationContext {\n  val context = ApplicationContext\n    .builder()\n    .args(*args)\n    .build()\n    .also(init)\n    .start()\n\n  context.findBean(EmbeddedApplication::class.java).ifPresent { app ->\n    if (!app.isRunning) {\n      app.start()\n    }\n  }\n\n  return context\n}\n```\n\n## Minimal Stove Setup\n\n```kotlin\nStove()\n  .with {\n    micronaut(\n      runner = { params -> run(params) },\n      withParameters = listOf(\"micronaut.server.port=8080\")\n    )\n  }\n  .run()\n```\n\n## What You Get\n\n- Micronaut startup through the real app context\n- `bridge()` support\n- clean integration with PostgreSQL, WireMock, Kafka, HTTP, and tracing\n\n## Example\n\n- [micronaut-example](https://github.com/Trendyol/stove/tree/main/examples/micronaut-example)\n"
  },
  {
    "path": "docs/frameworks/quarkus.md",
    "content": "# Quarkus\n\n`stove-quarkus` lets Stove start a Quarkus application in the same JVM as your test run, so reporting and `stove-tracing` continue to work with the normal Quarkus `main` entrypoint.\n\n!!! warning \"Bridge support\"\n    `bridge()` is not available in `stove-quarkus` yet.\n\n## Dependency\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-quarkus\")\n    testImplementation(\"com.trendyol:stove-extensions-kotest\")\n\n    testImplementation(\"com.trendyol:stove-http\")\n    testImplementation(\"com.trendyol:stove-postgres\")\n    testImplementation(\"com.trendyol:stove-kafka\")\n    testImplementation(\"com.trendyol:stove-wiremock\")\n    testImplementation(\"com.trendyol:stove-tracing\")\n}\n```\n\n## Application Entrypoint\n\nKeep a normal Quarkus entrypoint and let Stove call it from tests:\n\n```kotlin\n@QuarkusMain\nobject QuarkusMainApp {\n  @JvmStatic\n  fun main(args: Array<String>) {\n    Quarkus.run(*args)\n  }\n}\n```\n\nIf your application does not expose an HTTP endpoint, publish an explicit startup signal so Stove can detect readiness:\n\n```kotlin\n@ApplicationScoped\nclass StoveStartupSignal {\n  fun onStart(@Observes event: StartupEvent) {\n    System.setProperty(\"stove.quarkus.ready\", \"true\")\n  }\n\n  fun onStop(@Observes event: ShutdownEvent) {\n    System.clearProperty(\"stove.quarkus.ready\")\n  }\n}\n```\n\n## Minimal Stove Setup\n\n```kotlin\nStove()\n  .with {\n    tracing {\n      enableSpanReceiver()\n    }\n\n    quarkus(\n      runner = { params -> QuarkusMainApp.main(params) },\n      withParameters = listOf(\"quarkus.http.port=8080\")\n    )\n  }\n  .run()\n```\n\n## Kafka Note\n\nIf you use `stove-kafka` with Quarkus Kafka clients, add this to `application.properties`:\n\n```properties\nquarkus.class-loading.parent-first-artifacts=org.apache.kafka:kafka-clients\n```\n\nThis keeps the Kafka client classes shared so Stove's Kafka interceptor bridge can attach correctly.\n\n## Example\n\n- [quarkus-example](https://github.com/Trendyol/stove/tree/main/examples/quarkus-example)\n"
  },
  {
    "path": "docs/frameworks/spring-boot.md",
    "content": "# Spring Boot\n\n`stove-spring` is the starter for applications built on Spring Boot. It supports `bridge()` for direct bean access in tests.\n\n## Dependency\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-spring\")\n}\n```\n\n## Application Entrypoint\n\nExpose a reusable `run(args, init)` function so Stove can call the real app entrypoint:\n\n```kotlin\n@SpringBootApplication\nclass ExampleApp\n\nfun main(args: Array<String>) {\n  run(args)\n}\n\nfun run(\n  args: Array<String>,\n  init: SpringApplication.() -> Unit = {}\n): ConfigurableApplicationContext = runApplication<ExampleApp>(*args, init = init)\n```\n\n## Minimal Stove Setup\n\n```kotlin\nStove()\n  .with {\n    springBoot(\n      runner = { params -> run(params) },\n      withParameters = listOf(\"server.port=8080\")\n    )\n  }\n  .run()\n```\n\n## What You Get\n\n- Spring Boot startup through the real application entrypoint\n- `bridge()` support for bean access\n- full access to Stove components such as PostgreSQL, Kafka, WireMock, HTTP, and tracing\n\n## Spring Boot 4.x\n\nIf your application uses Spring Boot 4.x, use `addTestDependencies4x` instead of `addTestDependencies` when registering test beans:\n\n```kotlin\nimport com.trendyol.stove.addTestDependencies4x\n\nspringBoot(\n  runner = { params ->\n    runApplication<MyApp>(*params) {\n      addTestDependencies4x {\n        registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)\n        registerBean { StoveSerde.jackson.anyByteArraySerde(yourObjectMapper()) }\n      }\n    }\n  }\n)\n```\n\nSee the [Kafka](../Components/02-kafka.md) and [Bridge](../Components/10-bridge.md) docs for full Spring Boot 4.x bean registration details.\n\n## Examples\n\n- [spring-example](https://github.com/Trendyol/stove/tree/main/examples/spring-example)\n- [spring-standalone-example](https://github.com/Trendyol/stove/tree/main/examples/spring-standalone-example)\n- [spring-streams-example](https://github.com/Trendyol/stove/tree/main/examples/spring-streams-example)\n- [spring-4x-example](https://github.com/Trendyol/stove/tree/main/examples/spring-4x-example)\n"
  },
  {
    "path": "docs/getting-started.md",
    "content": "# Getting Started\n\nGet Stove running in your project in just a few minutes. Stove helps you write end-to-end tests by spinning up your application and all its dependencies (databases, message queues, etc.) together, so you can <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">test the real thing instead of mocks</span>.\n\nIf you already know your application framework, the quickest route is [Supported Frameworks](frameworks/index.md). This guide focuses on the shared setup that applies across all starters.\n\n## What You'll Need\n\nMake sure you have these installed:\n\n- **JDK 17+** - Stove needs Java 17 or higher\n- **Docker** - Required when using container mode (the default). Not needed if you use [provided instances](Components/11-provided-instances.md) to connect to existing infrastructure\n- **Kotlin 1.8+** - For writing your tests\n- **Gradle or Maven** - We use Gradle in all examples, but Maven works too\n\n!!! tip \"IDE Setup\"\n    If you're using IntelliJ IDEA, grab the Kotest plugin. It adds run buttons and makes test discovery much smoother.\n\n## Fastest Path\n\nIf you want the smallest useful Stove setup, do this first:\n\n1. Add `stove`, one starter, one test extension, and `stove-http`.\n2. Expose a reusable application entrypoint that Stove can call.\n3. Start Stove once for the suite.\n4. Make one real HTTP request and assert the result.\n\nEverything else can be added incrementally.\n\n## Step 1: Add The Minimum Dependencies\n\nStart with the smallest set that proves the flow works:\n\n```kotlin\nrepositories {\n    mavenCentral()\n}\n\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$stoveVersion\"))\n    testImplementation(\"com.trendyol:stove\")\n\n    // Pick one starter\n    testImplementation(\"com.trendyol:stove-spring\")\n\n    // Pick one test framework extension\n    testImplementation(\"com.trendyol:stove-extensions-kotest\")\n\n    // Start with one public surface\n    testImplementation(\"com.trendyol:stove-http\")\n}\n```\n\n!!! info \"Latest Version\"\n    Check the [Releases](https://github.com/Trendyol/stove/releases) page for the latest version.\n\n!!! tip \"Version Alignment\"\n    Keep the Stove BOM and all Stove test dependencies on the same version. If you use the dashboard, keep `stove-cli` on that same version too.\n\nReplace `stove-spring` with the starter that matches your runtime:\n\n- Spring Boot: `stove-spring`\n- Ktor: `stove-ktor`\n- Micronaut: `stove-micronaut`\n- Quarkus: `stove-quarkus`\n\nThen add only the components you actually need:\n\n- `stove-http` for REST APIs\n- `stove-kafka` for event flows\n- `stove-postgres`, `stove-mysql`, `stove-mongodb`, or `stove-redis` for persistence\n- `stove-wiremock` or `stove-grpc-mock` for external dependencies\n- `stove-tracing` for richer diagnostics\n\nIf you are using Ktor, also add your preferred DI support. See the [Ktor guide](frameworks/ktor.md) for the exact setup.\n\n## Step 2: Prepare Your Application\n\nStove needs to start your application from tests, which means your app needs a reusable entrypoint. The shared pattern is:\n\n1. keep the normal `main`\n2. move the actual startup logic into a reusable `run(args)` style function\n3. pass that function to Stove as the `runner`\n\nYou only need the version for your framework:\n\n=== \"Spring Boot\"\n\n    ```kotlin hl_lines=\"13 15 16 17 18\"\n    // Before\n    @SpringBootApplication\n    class MyApplication\n    \n    fun main(args: Array<String>) {\n        runApplication<MyApplication>(*args)\n    }\n    \n    // After\n    @SpringBootApplication\n    class MyApplication\n    \n    fun main(args: Array<String>) = run(args)\n    \n    fun run(\n        args: Array<String>,\n        init: SpringApplication.() -> Unit = {}\n    ): ConfigurableApplicationContext {\n        return runApplication<MyApplication>(*args, init = init)\n    }\n    ```\n\n=== \"Ktor with Koin\"\n\n    ```kotlin hl_lines=\"10 12 14 15 21\"\n    // Before\n    fun main() {\n        embeddedServer(Netty, port = 8080) {\n            install(Koin) { modules(appModule) }\n            configureRouting()\n        }.start(wait = true)\n    }\n    \n    // After - Accept test modules for overriding beans\n    object MyApp {\n        @JvmStatic\n        fun main(args: Array<String>) = run(args)\n        \n        fun run(\n            args: Array<String>,\n            wait: Boolean = true,\n            testModules: List<Module> = emptyList()\n        ): Application {\n            return embeddedServer(Netty, port = args.getPort()) {\n                install(Koin) {\n                    modules(appModule, *testModules.toTypedArray())\n                }\n                configureRouting()\n            }.start(wait = wait).application\n        }\n    }\n    ```\n\n=== \"Ktor with Ktor-DI\"\n\n    ```kotlin hl_lines=\"10 12 14 15 22\"\n    // Before\n    fun main() {\n        embeddedServer(Netty, port = 8080) {\n            install(DI) { dependencies { provide<MyService> { MyServiceImpl() } } }\n            configureRouting()\n        }.start(wait = true)\n    }\n    \n    // After - Accept test dependency overrides\n    object MyApp {\n        @JvmStatic\n        fun main(args: Array<String>) = run(args)\n        \n        fun run(\n            args: Array<String>,\n            wait: Boolean = true,\n            testDependencies: (DependencyRegistrar.() -> Unit)? = null\n        ): Application {\n            return embeddedServer(Netty, port = args.getPort()) {\n                install(DI) {\n                    dependencies {\n                        provide<MyService> { MyServiceImpl() }\n                        testDependencies?.invoke(this)  // Apply test overrides\n                    }\n                }\n                configureRouting()\n            }.start(wait = wait).application\n        }\n    }\n    ```\n\n=== \"Micronaut\"\n\n    ```kotlin\n    fun main(args: Array<String>) {\n        run(args)\n    }\n\n    fun run(\n        args: Array<String>,\n        init: ApplicationContext.() -> Unit = {}\n    ): ApplicationContext {\n        val context = ApplicationContext\n            .builder()\n            .args(*args)\n            .build()\n            .also(init)\n            .start()\n\n        context.findBean(EmbeddedApplication::class.java).ifPresent { app ->\n            if (!app.isRunning) {\n                app.start()\n            }\n        }\n\n        return context\n    }\n    ```\n\n=== \"Quarkus\"\n\n    ```kotlin\n    package com.example\n\n    import io.quarkus.runtime.Quarkus\n    import io.quarkus.runtime.ShutdownEvent\n    import io.quarkus.runtime.StartupEvent\n    import io.quarkus.runtime.annotations.QuarkusMain\n    import jakarta.enterprise.context.ApplicationScoped\n    import jakarta.enterprise.event.Observes\n\n    @QuarkusMain\n    object QuarkusMainApp {\n        @JvmStatic\n        fun main(args: Array<String>) {\n            Quarkus.run(*args)\n        }\n    }\n\n    @ApplicationScoped\n    class StoveStartupSignal {\n        fun onStart(@Observes event: StartupEvent) {\n            System.setProperty(\"stove.quarkus.ready\", \"true\")\n        }\n\n        fun onStop(@Observes event: ShutdownEvent) {\n            System.clearProperty(\"stove.quarkus.ready\")\n        }\n    }\n    ```\n\n    Stove calls your Quarkus `main` function directly. If your app has no HTTP endpoint, publish a startup signal like the one above so Stove can detect readiness. See the [Quarkus guide](frameworks/quarkus.md) for the full setup, including Kafka and tracing notes.\n\n## Step 3: Create Test Configuration\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">Set up Stove once for your entire test suite.</span> This configuration runs before all your tests and shuts down after they're done. Use <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Stove()</span> and <span data-rn=\"underline\" data-rn-color=\"#ff9800\">.with { }</span> to configure your test environment. \n\nIf you are aiming for the fastest first success, start with one starter plus `stove-http`, confirm the app boots and responds, and only then add Kafka, databases, tracing, or mocks.\n\nWe recommend putting e2e tests in a separate `src/test-e2e` source set to keep them separate from unit tests (see [Best Practices](best-practices.md#use-dedicated-source-set-for-e2e-tests) for the Gradle setup).\n\n!!! info \"Test Framework Extensions\"\n    `StoveKotestExtension` and `StoveJUnitExtension` are separate packages that must be on your classpath:\n\n    ```kotlin\n    testImplementation(\"com.trendyol:stove-extensions-kotest\") // For Kotest\n    // or\n    testImplementation(\"com.trendyol:stove-extensions-junit\")  // For JUnit\n    ```\n\n    **Kotest** requires **6.1.3** or later. **JUnit** requires **Jupiter 6.x** if possible. In Kotest 6.x, `AbstractProjectConfig` is no longer auto-scanned. Create a `kotest.properties` file in your test resources (e.g. `src/test-e2e/resources/kotest.properties`):\n\n    ```properties\n    kotest.framework.config.fqn=com.myapp.e2e.TestConfig\n    ```\n\n    Set the value to the fully qualified name of your `AbstractProjectConfig` class.\n\n    If you are testing a Quarkus application, see the [Quarkus guide](frameworks/quarkus.md) for the starter-specific setup and limitations.\n\n=== \"Kotest\"\n\n    ```kotlin hl_lines=\"10 12 13 16 31\"\n    // src/test-e2e/kotlin/io/kotest/provided/ProjectConfig.kt\n    import com.trendyol.stove.extensions.kotest.StoveKotestExtension\n    import com.trendyol.stove.system.Stove\n    import com.trendyol.stove.system.stove\n    import com.trendyol.stove.http.*\n    import com.trendyol.stove.spring.springBoot\n    \n    class TestConfig : AbstractProjectConfig() {\n        // Optional: Add this for detailed failure reports with execution context\n        override val extensions: List<Extension> = listOf(StoveKotestExtension())\n        \n        override suspend fun beforeProject() {\n            Stove()\n                .with {\n                    httpClient {\n                        HttpClientSystemOptions(\n                            baseUrl = \"http://localhost:8080\"\n                        )\n                    }\n                    \n                    // Replace `springBoot` with `ktor`, `micronaut`, or `quarkus` as needed\n                    springBoot(\n                        runner = { params -> \n                            com.myapp.run(params)\n                        },\n                        withParameters = listOf(\n                            \"server.port=8080\",\n                            \"logging.level.root=warn\"\n                        )\n                    )\n                }\n                .run()\n        }\n        \n        override suspend fun afterProject() {\n            Stove.stop()\n        }\n    }\n    ```\n\n=== \"JUnit\"\n\n    ```kotlin\n    // src/test-e2e/kotlin/e2e/TestConfig.kt\n    import com.trendyol.stove.extensions.junit.StoveJUnitExtension\n    import com.trendyol.stove.system.Stove\n    import com.trendyol.stove.http.*\n    import com.trendyol.stove.spring.springBoot\n    import org.junit.jupiter.api.extension.ExtendWith\n    \n    // Optional: Add this annotation for detailed failure reports\n    @ExtendWith(StoveJUnitExtension::class)\n    @TestInstance(TestInstance.Lifecycle.PER_CLASS)\n    abstract class BaseE2ETest {\n        \n        companion object {\n            @JvmStatic\n            @BeforeAll\n            fun setup() = runBlocking {\n                Stove()\n                    .with {\n                        httpClient {\n                            HttpClientSystemOptions(\n                                baseUrl = \"http://localhost:8080\"\n                            )\n                        }\n                        \n                        // Replace `springBoot` with `ktor`, `micronaut`, or `quarkus` as needed\n                        springBoot(\n                            runner = { params -> \n                                com.myapp.run(params)\n                            },\n                            withParameters = listOf(\n                                \"server.port=8080\",\n                                \"logging.level.root=warn\"\n                            )\n                        )\n                    }\n                    .run()\n            }\n            \n            @JvmStatic\n            @AfterAll\n            fun teardown() = runBlocking {\n                Stove.stop()\n            }\n        }\n    }\n    ```\n\n## Step 4: Write Your First Test\n\n=== \"Kotest\"\n\n    ```kotlin\n    import com.trendyol.stove.system.stove\n    \n    class MyFirstE2ETest : FunSpec({\n        \n        test(\"should return hello world\") {\n            stove {\n                http {\n                    get<String>(\"/hello\") { response ->\n                        response shouldBe \"Hello, World!\"\n                    }\n                }\n            }\n        }\n        \n        test(\"should create a user\") {\n            stove {\n                http {\n                    postAndExpectBody<UserResponse>(\n                        uri = \"/users\",\n                        body = CreateUserRequest(name = \"John\", email = \"john@example.com\").some()\n                    ) { response ->\n                        response.status shouldBe 201\n                        response.body().name shouldBe \"John\"\n                    }\n                }\n            }\n        }\n    })\n    ```\n\n=== \"JUnit\"\n\n    ```kotlin\n    import com.trendyol.stove.system.stove\n    \n    class MyFirstE2ETest : BaseE2ETest() {\n        \n        @Test\n        fun `should return hello world`() = runBlocking {\n            stove {\n                http {\n                    get<String>(\"/hello\") { response ->\n                        response shouldBe \"Hello, World!\"\n                    }\n                }\n            }\n        }\n        \n        @Test\n        fun `should create a user`() = runBlocking {\n            stove {\n                http {\n                    postAndExpectBody<UserResponse>(\n                        uri = \"/users\",\n                        body = CreateUserRequest(name = \"John\", email = \"john@example.com\").some()\n                    ) { response ->\n                        response.status shouldBe 201\n                        response.body().name shouldBe \"John\"\n                    }\n                }\n            }\n        }\n    }\n    ```\n\n## Step 5: Add More Components\n\nOnce you've got the basics working, you'll probably want to <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">add more components</span>. Here's how you'd set up a typical stack:\n\n```kotlin hl_lines=\"9 19 33 37 40\"\nStove()\n    .with {\n        httpClient {\n            HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n        }\n        \n        // Add Kafka for event-driven tests\n        kafka {\n            KafkaSystemOptions {\n                listOf(\n                    \"kafka.bootstrapServers=${it.bootstrapServers}\",\n                    \"kafka.interceptorClasses=${it.interceptorClass}\"\n                )\n            }\n        }\n        \n        // Add Couchbase for database tests\n        couchbase {\n            CouchbaseSystemOptions(\n                defaultBucket = \"myBucket\",\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"couchbase.hosts=${cfg.hostsWithPort}\",\n                        \"couchbase.username=${cfg.username}\",\n                        \"couchbase.password=${cfg.password}\"\n                    )\n                }\n            )\n        }\n        \n        // Add WireMock for external service mocking\n        wiremock {\n            WireMockSystemOptions(port = 9090)\n        }\n        \n        // Add bridge for DI container access\n        bridge()\n        \n        springBoot(\n            runner = { params -> com.myapp.run(params) },\n            withParameters = listOf(\n                \"server.port=8080\",\n                \"external.service.url=http://localhost:9090\"\n            )\n        )\n    }\n    .run()\n```\n\n## Step 6: Write Tests That Span Multiple Systems\n\nHere's where <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">Stove really shines</span>. You can write tests that touch multiple systems and verify everything works together:\n\n```kotlin hl_lines=\"4 8 17 31 38 46\"\nimport com.trendyol.stove.system.stove\n\ntest(\"should create order and publish event\") {\n    stove {\n        val orderId = UUID.randomUUID().toString()\n        \n        // Mock external payment service\n        wiremock {\n            mockPost(\n                url = \"/payments\",\n                statusCode = 200,\n                responseBody = PaymentResult(success = true).some()\n            )\n        }\n        \n        // Create order via API\n        http {\n            postAndExpectBody<OrderResponse>(\n                uri = \"/orders\",\n                body = CreateOrderRequest(\n                    id = orderId,\n                    items = listOf(\"item1\", \"item2\"),\n                    amount = 99.99\n                ).some()\n            ) { response ->\n                response.status shouldBe 201\n            }\n        }\n        \n        // Verify order stored in database\n        couchbase {\n            shouldGet<Order>(\"orders\", orderId) { order ->\n                order.status shouldBe \"CREATED\"\n                order.amount shouldBe 99.99\n            }\n        }\n        \n        // Verify event was published\n        kafka {\n            shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {\n                actual.orderId == orderId &&\n                actual.amount == 99.99\n            }\n        }\n        \n        // Access application beans directly\n        using<OrderService> {\n            val order = getOrder(orderId)\n            order.status shouldBe \"CREATED\"\n        }\n    }\n}\n```\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">Stove starts your application with its dependencies, runs your tests, and shuts everything down when done.</span>\n\n## Running Tests\n\nRun all your tests:\n\n```bash\n./gradlew test\n```\n\nOr run a specific test class:\n\n```bash\n./gradlew test --tests \"com.myapp.e2e.OrderE2ETest\"\n```\n\nIf you're using the `test-e2e` source set, you might have a separate task:\n\n```bash\n./gradlew e2eTest\n```\n\n## Next Steps\n\nNow that you're up and running, here's what to explore next:\n\n- **Components** - Check out the [Components documentation](Components/index.md) to see what's available\n- **Quarkus** - If your application uses Quarkus, follow the [Quarkus guide](frameworks/quarkus.md)\n- **Tracing** - Enable [Tracing](Components/15-tracing.md) to see exactly what happened inside your application when a test fails\n- **Reporting** - Set up [Reporting](Components/13-reporting.md) to get detailed failure diagnostics\n- **Dashboard** - Start the [local dashboard](Components/18-dashboard.md) when you want live timelines, traces, snapshots, and the REST API\n- **MCP** - Let AI agents inspect failed tests through the local [Stove MCP endpoint](Components/21-mcp.md) served by `stove`\n- **gRPC Mocking** - Mock external gRPC services with [gRPC Mocking](Components/14-grpc-mock.md)\n- **Best Practices** - Read the [Best Practices guide](best-practices.md) for tips on writing effective e2e tests\n- **Troubleshooting** - Hit an issue? Check the [Troubleshooting guide](troubleshooting.md)\n- **Examples** - Browse the [Examples](https://github.com/Trendyol/stove/tree/main/examples) and [Recipes](https://github.com/Trendyol/stove/tree/main/recipes) for complete working projects\n\n## Common Patterns\n\n### Keep Containers Running Between Test Runs\n\nStarting containers takes time. During development, you can <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">keep them running between test runs</span> to speed things up:\n\n```kotlin hl_lines=\"2\"\nStove {\n    keepDependenciesRunning()\n}.with {\n    // Your configuration\n}.run()\n```\n\n### Using a Custom Container Registry\n\nIf you're behind a corporate firewall or need to use a private registry:\n\n```kotlin\n// Set globally\nDEFAULT_REGISTRY = \"your.registry.com\"\n\n// Or per component\nkafka {\n    KafkaSystemOptions(\n        container = KafkaContainerOptions(\n            registry = \"your.registry.com\"\n        )\n    )\n}\n```\n\n### Use Unique Test Data\n\nTo avoid test conflicts, <span data-rn=\"underline\" data-rn-color=\"#ff9800\">generate unique data for each test run</span>:\n\n```kotlin\ntest(\"should create user\") {\n    val userId = UUID.randomUUID().toString()\n    val email = \"test-${UUID.randomUUID()}@example.com\"\n    \n    stove {\n        // Use unique data to avoid conflicts\n    }\n}\n```\n\n## Troubleshooting Quick Tips\n\n| Problem | Solution |\n|---------|----------|\n| Docker not found | Ensure Docker is running and accessible |\n| Port conflicts | Use dynamic ports or ensure no conflicts |\n| Slow startup | Enable `keepDependenciesRunning()` for development |\n| Serialization errors | Configure `StoveSerde` to match your app's serializer |\n| Test isolation issues | Use unique test data and cleanup functions |\n\nFor more help, see the [Troubleshooting Guide](troubleshooting.md).\n"
  },
  {
    "path": "docs/index.md",
    "content": "# Stove\n\nStove is an end-to-end testing framework for JVM applications. It boots your real application together with the dependencies it actually uses, so your tests exercise the real runtime flow instead of a hand-built harness full of mocks.\n\nIf your service talks to HTTP APIs, Kafka, databases, Redis, gRPC services, or external providers, Stove lets you bring those pieces into one test setup and assert the full behavior in one place.\n\nSince JVM languages interoperate, your application and tests do not need to use the same language. Write the app in Java, Kotlin, or Scala, and keep the tests consistent on the Stove side.\n\nWhen running in container mode (the default), Stove uses [Testcontainers](https://github.com/testcontainers/testcontainers-java) under the hood, so <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Docker</span> must be installed. If you use [provided instances](Components/11-provided-instances.md) to connect to existing infrastructure, Docker is not required.\n\n!!! note \"Not a Replacement for Unit Tests\"\n    Stove is for end-to-end and component tests, not unit tests. Keep unit tests for fast feedback on isolated logic.\n\n## See It Quickly\n\nThe core idea is small:\n\n```kotlin\nStove()\n  .with {\n    httpClient {\n      HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n    }\n\n    kafka {\n      KafkaSystemOptions(...)\n    }\n\n    springBoot(\n      runner = { params -> run(params) },\n      withParameters = listOf(\"server.port=8080\")\n    )\n  }\n  .run()\n\nstove {\n  http {\n    get<String>(\"/hello\") { body ->\n      body shouldContain \"hello\"\n    }\n  }\n\n  kafka {\n    shouldBePublished<String> { it.contains(\"created\") }\n  }\n}\n```\n\nYou start the real app, bring up only the dependencies you need, and assert through the surfaces that matter.\n\n## Choose Your Path\n\n<div class=\"grid cards\" markdown>\n\n-   **New to Stove**\n\n    Start with the shared setup model and learn the basic DSL once.\n\n    [Getting Started](getting-started.md)\n\n-   **Already know your framework**\n\n    Pick the starter that matches your application runtime.\n\n    [Supported Frameworks](frameworks/index.md)\n\n-   **Already know your dependencies**\n\n    Add Kafka, PostgreSQL, WireMock, HTTP, tracing, and other components as needed.\n\n    [Components](Components/index.md)\n\n-   **Want a working project**\n\n    Open a complete example and adapt it instead of starting from scratch.\n\n    [Examples on GitHub](https://github.com/Trendyol/stove/tree/main/examples)\n\n-   **Running without Docker or in CI/CD**\n\n    Use provided instances to connect to existing infrastructure instead of spinning up containers.\n\n    [Provided Instances](Components/11-provided-instances.md)\n\n-   **Debugging failures or using AI agents**\n\n    Add the local dashboard, tracing, and MCP so failures come with timelines, spans, snapshots, and compact agent-readable evidence.\n\n    [Dashboard & MCP](Components/18-dashboard.md)\n\n</div>\n\n## Why Stove\n\nThe JVM ecosystem has strong application frameworks, but e2e setup is usually framework-specific and repetitive. Teams end up rebuilding the same boilerplate around containers, startup wiring, ports, config injection, test cleanup, and diagnostics.\n\nStove standardizes that workflow:\n\n- start physical dependencies first (via containers or [provided instances](Components/11-provided-instances.md))\n- boot the real application through its actual entrypoint\n- inject container/runtime configuration into the app\n- assert through HTTP, Kafka, gRPC, databases, and tracing\n- keep the same test DSL across frameworks\n\n<span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">One testing model, multiple JVM stacks.</span>\n\n## Supported Frameworks\n\nStove currently ships starters for:\n\n- [Spring Boot](frameworks/spring-boot.md)\n- [Ktor](frameworks/ktor.md)\n- [Micronaut](frameworks/micronaut.md)\n- [Quarkus](frameworks/quarkus.md)\n\nSee the full overview in [Supported Frameworks](frameworks/index.md), including `bridge()` availability and example links.\n\n## What You Can Test\n\nStove composes framework starters with pluggable components, so you can match your test environment to your production architecture.\n\n- APIs through [HTTP](Components/05-http.md) or [gRPC](Components/12-grpc.md)\n- event flows through [Kafka](Components/02-kafka.md)\n- persistence through [PostgreSQL](Components/06-postgresql.md), [MySQL](Components/16-mysql.md), [MongoDB](Components/07-mongodb.md), [Cassandra](Components/17-cassandra.md), [Redis](Components/09-redis.md), and more\n- external integrations through [WireMock](Components/04-wiremock.md) and [gRPC Mock](Components/14-grpc-mock.md)\n- execution diagnostics through [Reporting](Components/13-reporting.md), [Tracing](Components/15-tracing.md), the [Dashboard](Components/18-dashboard.md), and [MCP](Components/21-mcp.md)\n\n## High Level Architecture\n\n![Stove architecture](./assets/stove_architecture.svg)\n\n## Start Here\n\n1. Read [Getting Started](getting-started.md) for the shared setup.\n2. Open your starter guide under [Supported Frameworks](frameworks/index.md).\n3. Add the components you need from [Components](Components/index.md).\n4. Add [Dashboard](Components/18-dashboard.md) and [MCP](Components/21-mcp.md) when you want local observability or AI-agent triage.\n5. Compare against a real project in [examples](https://github.com/Trendyol/stove/tree/main/examples).\n\n## Building From Source\n\nTo build Stove locally you need:\n\n- JDK 17+\n- Docker\n\nThen run:\n\n```shell\n./gradlew build\n```\n\nWant the background and motivation? Read the original [Medium article](https://medium.com/trendyol-tech/a-new-approach-to-the-api-end-to-end-testing-in-kotlin-f743fd1901f5).\n"
  },
  {
    "path": "docs/js/rough-notation-mkdocs.js",
    "content": "/**\n * Declarative RoughNotation for MkDocs\n *\n * Standalone annotation (animates when scrolled into view):\n *   <span data-rn=\"highlight\" data-rn-color=\"#00968855\">text</span>\n *\n * Grouped annotations (animate sequentially when parent scrolls into view):\n *   <div data-rn-group>\n *     <span data-rn=\"highlight\" data-rn-color=\"#ef535055\">first</span>\n *     <span data-rn=\"box\" data-rn-color=\"#ef5350\">second</span>\n *   </div>\n *\n * Supported attributes:\n *   data-rn          - type: highlight, box, underline, circle,\n *                       strike-through, crossed-off, bracket\n *   data-rn-color    - color (default: current theme accent)\n *   data-rn-stroke   - strokeWidth (default: 2)\n *   data-rn-padding  - padding in px\n *   data-rn-duration - animation duration in ms (default: 600)\n *   data-rn-group    - place on a parent element to group child [data-rn] spans\n */\n(function () {\n  'use strict';\n\n  var DEFAULTS = {\n    color: '#009688',\n    strokeWidth: 2,\n    animationDuration: 600,\n    multiline: true\n  };\n\n  var CODE_DEFAULTS = {\n    type: 'highlight',\n    color: '#00968830',\n    strokeWidth: 1,\n    animationDuration: 400,\n    multiline: true,\n    padding: 0\n  };\n\n  function parseOpts(el) {\n    var opts = {\n      type: el.dataset.rn,\n      color: el.dataset.rnColor || DEFAULTS.color,\n      strokeWidth: parseInt(el.dataset.rnStroke) || DEFAULTS.strokeWidth,\n      animationDuration: parseInt(el.dataset.rnDuration) || DEFAULTS.animationDuration,\n      multiline: DEFAULTS.multiline\n    };\n    if (el.dataset.rnPadding !== undefined) {\n      opts.padding = parseInt(el.dataset.rnPadding);\n    }\n    return opts;\n  }\n\n  function observe(target, threshold, callback) {\n    var observer = new IntersectionObserver(function (entries) {\n      entries.forEach(function (entry) {\n        if (entry.isIntersecting) {\n          callback();\n          observer.disconnect();\n        }\n      });\n    }, { threshold: threshold });\n    observer.observe(target);\n  }\n\n  function init() {\n    var RN = window.RoughNotation;\n    if (!RN) return;\n\n    // Grouped annotations: animate sequentially when parent scrolls into view\n    document.querySelectorAll('[data-rn-group]').forEach(function (groupEl) {\n      if (groupEl.dataset.rnInit) return;\n      groupEl.dataset.rnInit = '1';\n\n      var anns = [];\n      groupEl.querySelectorAll('[data-rn]').forEach(function (el) {\n        if (el.dataset.rnInit) return;\n        el.dataset.rnInit = '1';\n        anns.push(RN.annotate(el, parseOpts(el)));\n      });\n\n      if (!anns.length) return;\n      var group = RN.annotationGroup(anns);\n      observe(groupEl, 0.2, function () { group.show(); });\n    });\n\n    // Standalone annotations: animate individually on scroll\n    document.querySelectorAll('[data-rn]').forEach(function (el) {\n      if (el.dataset.rnInit) return;\n      el.dataset.rnInit = '1';\n\n      var ann = RN.annotate(el, parseOpts(el));\n      observe(el, 0.5, function () { ann.show(); });\n    });\n\n    // Code block hl_lines -> RoughNotation highlights\n    document.querySelectorAll('.highlight pre, pre').forEach(function (pre) {\n      if (pre.dataset.rnCodeInit) return;\n      var hlls = pre.querySelectorAll('.hll');\n      if (!hlls.length) return;\n      pre.dataset.rnCodeInit = '1';\n\n      var anns = [];\n      hlls.forEach(function (hll) {\n        hll.style.backgroundColor = 'transparent';\n        anns.push(RN.annotate(hll, {\n          type: CODE_DEFAULTS.type,\n          color: CODE_DEFAULTS.color,\n          strokeWidth: CODE_DEFAULTS.strokeWidth,\n          animationDuration: CODE_DEFAULTS.animationDuration,\n          multiline: CODE_DEFAULTS.multiline,\n          padding: CODE_DEFAULTS.padding\n        }));\n      });\n\n      if (anns.length) {\n        var group = RN.annotationGroup(anns);\n        observe(pre, 0.1, function () { group.show(); });\n      }\n    });\n  }\n\n  // MkDocs Material instant loading support\n  if (typeof document$ !== 'undefined') {\n    document$.subscribe(function () { init(); });\n  }\n\n  // Initial page load\n  if (document.readyState !== 'loading') {\n    init();\n  } else {\n    document.addEventListener('DOMContentLoaded', init);\n  }\n})();\n"
  },
  {
    "path": "docs/other-languages/go-container.md",
    "content": "# Go — Container Mode\n\nRun the Go application as a Docker image instead of a host binary using `stove-container` and the `containerApp()` DSL. This gives you <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">image-level parity with what you ship to production</span> — same Dockerfile, same entrypoint, same runtime — without changing a single line of Stove test code.\n\nFor fast iteration without an image, see [Process Mode](go-process.md). The same Kotlin tests run against either.\n\nThis page is the **Go-specific recipe**. For the language-agnostic `stove-container` reference — full DSL contract, image-source patterns, networking strategies, `configureContainer`, `beforeStarted`, troubleshooting matrix — see [Container AUT (`stove-container`)](../Components/22-container.md). The Go showcase below uses that module; it does not redefine it.\n\n## Why container mode (Go-specific summary)\n\n| Concern | Process mode | Container mode |\n|---------|--------------|----------------|\n| **Iteration speed** | Fast — `go build` only | Slower — image build (or fetch from registry) |\n| **Production parity** | Approximate (host runtime) | Exact (the artifact you ship) |\n| **Glibc / Alpine differences** | Hidden | Surfaced |\n| **CI/CD validation** | Indirect | Direct |\n\nUse container mode in CI to catch image-only regressions (missing CA certs, wrong base image, locale issues, glibc/musl drift). Keep process mode for the inner debug loop.\n\n## What this guide adds on top of Process Mode\n\nThe Go application code, OpenTelemetry setup, Kafka bridge integration, and Stove test DSL are identical to [Process Mode](go-process.md). Container mode only changes:\n\n1. **AUT runner** — `containerApp(...)` instead of `goApp(...)` (see the [container component page](../Components/22-container.md))\n2. **Image source** — a tagged image, from CI / a registry / or an optional local build\n3. **(Optional) Coverage volume** — bind-mount a host directory into the container so coverage data survives container removal\n\nThe Kotlin tests, the Stove DSL, the Stove systems, and the Go source code do not change.\n\n!!! info \"Image build is not Stove's job\"\n    `containerApp(...)` only needs an image reference. Use whatever your CI already produced, pull from a registry, or wire an optional local Gradle build task — see [image source patterns](../Components/22-container.md#image-source-patterns) for the three options. The Dockerfile and `buildContainerImage` task below are the *recipe's* convenience for being self-contained, not a requirement.\n\n## (Optional) Dockerfile for the showcase\n\nThe recipe includes a Dockerfile so the repo is self-contained. In a real Go project, this is whatever your team already ships to production.\n\n```dockerfile title=\"Dockerfile.container\"\nFROM golang:1.26.2 AS build\n\nWORKDIR /workspace\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY *.go ./\n\nARG GO_BUILD_FLAGS=\"\"\nRUN CGO_ENABLED=0 GOOS=linux go build ${GO_BUILD_FLAGS} -o /out/go-showcase .\n\nFROM alpine:3.23\nWORKDIR /app\nCOPY --from=build /out/go-showcase /app/go-showcase\n\nEXPOSE 8090\nENTRYPOINT [\"/app/go-showcase\"]\n```\n\nThe `GO_BUILD_FLAGS` build-arg is what threads `-cover` through the Docker build when coverage is enabled (process mode does this with `go build -cover` directly).\n\n## Gradle Setup\n\nThe minimum: a `Test` task that knows the image tag. The image can come from anywhere.\n\n```kotlin title=\"build.gradle.kts\"\n// Resolve the image tag in priority order: env var → Gradle property → local fallback\nval containerImage = providers.environmentVariable(\"APP_IMAGE\")\n    .orElse(providers.gradleProperty(\"app.image\"))\n    .orElse(\"stove-go-showcase-container:local\")\n\ntasks.register<Test>(\"e2eTest-container\") {\n    description = \"Runs container-based e2e tests.\"\n    group = \"verification\"\n    useJUnitPlatform()\n    systemProperty(\"go.aut.mode\", \"container\")\n    systemProperty(\"go.app.container.image\", containerImage.get())\n    systemProperty(\"kafka.library\", \"sarama\")\n}\n```\n\nIn CI, point `APP_IMAGE` (or `-Papp.image=...`) at the tag your image-build job just produced. No `dependsOn(\"buildContainerImage\")` needed — Stove just runs whatever is at that tag.\n\n### (Optional) Local build convenience\n\nIf you also want a one-command local-build path, wire the Docker build as a separate task and add a *separate* test task that depends on it. Keep the CI-tag path untouched.\n\n```kotlin title=\"build.gradle.kts\"\nval dockerExecutable = providers.environmentVariable(\"DOCKER_EXECUTABLE\").getOrElse(\"docker\")\nval coverageEnabled = providers.gradleProperty(\"go.coverage\")\n    .map { it.toBoolean() }.getOrElse(false)\nval localImageTag = \"stove-go-showcase-container:local\"\n\ntasks.register<Exec>(\"buildContainerImage\") {\n    description = \"Optional convenience: builds the Go showcase Docker image locally.\"\n    group = \"build\"\n    dependsOn(\"goModTidy\")\n    val buildFlags = if (coverageEnabled) \"-cover\" else \"\"\n    commandLine(\n        dockerExecutable, \"build\",\n        \"--file\", projectDir.resolve(\"Dockerfile.container\").absolutePath,\n        \"--tag\", localImageTag,\n        \"--build-arg\", \"GO_BUILD_FLAGS=$buildFlags\",\n        projectDir.absolutePath\n    )\n    inputs.file(project.file(\"Dockerfile.container\"))\n    inputs.files(fileTree(\".\") { include(\"*.go\", \"go.mod\", \"go.sum\") })\n    outputs.upToDateWhen { false }   // Docker is the source of truth\n}\n\ntasks.register<Exec>(\"removeContainerImage\") {\n    description = \"Removes the locally-built image.\"\n    group = \"build\"\n    commandLine(dockerExecutable, \"image\", \"rm\", localImageTag)\n    isIgnoreExitValue = true\n}\n\n// Local-build path — only this task triggers a build\ntasks.register<Test>(\"e2eTest-container-local\") {\n    description = \"Builds the image locally and runs container e2e tests.\"\n    group = \"verification\"\n    dependsOn(\"buildContainerImage\")\n    useJUnitPlatform()\n    systemProperty(\"go.aut.mode\", \"container\")\n    systemProperty(\"go.app.container.image\", localImageTag)\n    systemProperty(\"kafka.library\", \"sarama\")\n    if (coverageEnabled) {\n        systemProperty(\"go.cover.dir\", goCoverDirPath)\n        outputs.cacheIf { false }\n    }\n}\n```\n\n`buildContainerImage` is intentionally not cached — Docker is the source of truth for image freshness. The CI test task (`e2eTest-container`) does **not** depend on it.\n\n## Stove Configuration (Go specifics)\n\nA single `StoveConfig.kt` can serve both modes by branching on a system property. The infrastructure systems (PostgreSQL, Kafka, tracing, dashboard) are identical to process mode — only the AUT runner block changes:\n\n```kotlin title=\"StoveConfig.kt\"\ncontainerApp(\n    image = System.getProperty(\"go.app.container.image\"),\n    target = ContainerTarget.Server(\n        hostPort = APP_PORT,\n        internalPort = APP_PORT,\n        portEnvVar = \"APP_PORT\",\n        bindHostPort = false   // host network — no need to bind\n    ),\n    envProvider = envMapper {\n        // Stove → Go env var mapping (same keys as process mode)\n        \"database.host\" to \"DB_HOST\"\n        \"database.port\" to \"DB_PORT\"\n        \"database.name\" to \"DB_NAME\"\n        \"database.username\" to \"DB_USER\"\n        \"database.password\" to \"DB_PASS\"\n        \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n        env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:$OTLP_PORT\")\n        env(\"KAFKA_LIBRARY\", System.getProperty(\"kafka.library\") ?: \"sarama\")\n        env(\"STOVE_KAFKA_BRIDGE_PORT\", stoveKafkaBridgePortDefault)\n        env(\"GOCOVERDIR\", coverageDirInContainer)\n    },\n    configureContainer = {\n        withNetworkMode(\"host\")\n        if (hostCoverageDir.isNotBlank()) {\n            withFileSystemBind(hostCoverageDir, COVERAGE_DIR_IN_CONTAINER)\n        }\n    }\n)\n```\n\nFor the full list of `containerApp` parameters, `ContainerTarget` variants, networking strategies (`host` vs port-binding), and `configureContainer` capabilities, see the [container component page](../Components/22-container.md).\n\n## Running\n\n```bash\n# CI / registry image — pass the tag in\n./gradlew e2eTest-container -Papp.image=ghcr.io/acme/go-showcase:sha-abc123\n# or\nAPP_IMAGE=ghcr.io/acme/go-showcase:sha-abc123 ./gradlew e2eTest-container\n\n# Optional local-build path (only when you wired buildContainerImage)\n./gradlew e2eTest-container-local\n\n# Container e2e with Go coverage\n./gradlew e2eTest-containerWithCoverage -Pgo.coverage=true\n\n# Remove the locally-built image when done\n./gradlew removeContainerImage\n\n# Use locally-published Stove artifacts (e.g. before a snapshot release)\n./gradlew e2eTest-container -PuseMavenLocal=true\n```\n\nBy default the recipe resolves Stove from Maven Central + Sonatype snapshots so CI validates the same published path that users consume. `mavenLocal()` is opt-in.\n\n## Code Coverage (Go-specific)\n\nContainer coverage works the same way as [process mode](go-process.md#code-coverage), with two extra wiring details unique to Go-in-a-container:\n\n1. The `Dockerfile` passes `${GO_BUILD_FLAGS}` so `-cover` reaches the build inside the image\n2. The host coverage directory is bind-mounted into the container so data survives container teardown\n\n```kotlin\n// In StoveConfig.kt\nprivate const val COVERAGE_DIR_IN_CONTAINER = \"/tmp/go-coverage\"\nval hostCoverageDir = System.getProperty(\"go.cover.dir\").orEmpty()\nval coverageDirInContainer = if (hostCoverageDir.isBlank()) \"\" else COVERAGE_DIR_IN_CONTAINER\n\ncontainerApp(\n    // ...\n    envProvider = envMapper {\n        // ...\n        env(\"GOCOVERDIR\", coverageDirInContainer)\n    },\n    configureContainer = {\n        withNetworkMode(\"host\")\n        if (hostCoverageDir.isNotBlank()) {\n            withFileSystemBind(hostCoverageDir, COVERAGE_DIR_IN_CONTAINER)\n        }\n    }\n)\n```\n\n```bash\n./gradlew e2eTest-containerWithCoverage -Pgo.coverage=true\n# HTML report at build/go-coverage/coverage.html\n```\n\n`signal.Ignore(syscall.SIGPIPE)` in `main()` matters here too — Stove sends SIGTERM to stop the container, and Go must finish flushing coverage data before the process dies.\n\n## Dashboard & MCP\n\nContainer mode emits to the [Stove Dashboard](../Components/18-dashboard.md) and the [MCP server](../Components/21-mcp.md) the same way process mode does. The `appName` you set in `DashboardSystemOptions` is the only label MCP needs to find the right runs:\n\n```text\nAgent calls stove_failures\n  → finds failed runs for app_name=go-showcase\n  → calls stove_failure_detail with run_id + test_id\n  → drills into stove_trace to see Go spans\n```\n\nBecause tracing is `traceparent`-correlated, a Go span captured inside the container shows up in the same trace tree as the originating Stove HTTP call — no additional plumbing required.\n\n## Reference\n\n- Container component page (DSL contract, networking, troubleshooting): [Container AUT (`stove-container`)](../Components/22-container.md)\n- Container module source: `starters/container/stove-container/`\n- Full working example (process **and** container modes in one repo): [`recipes/process/golang/go-showcase`](https://github.com/Trendyol/stove/tree/main/recipes/process/golang/go-showcase)\n- Bridge library source: [`go/stove-kafka`](https://github.com/Trendyol/stove/tree/main/go/stove-kafka)\n- Component docs: [Dashboard](../Components/18-dashboard.md) · [MCP](../Components/21-mcp.md) · [Tracing](../Components/15-tracing.md)\n"
  },
  {
    "path": "docs/other-languages/go-process.md",
    "content": "# Go — Process Mode\n\nRun the Go binary directly as the application under test using `stove-process` and the `goApp()` DSL. This is the fastest iteration loop: no image build, no registry, just `go build` and run.\n\nFor container-based AUT (CI parity with the production image), see [Container Mode](go-container.md).\n\n## What this guide covers\n\nEnd-to-end Go testing with HTTP, PostgreSQL, Kafka (sarama / franz-go / segmentio), distributed tracing, dashboard streaming, MCP triage, and integration coverage.\n\nThe full source is at [`recipes/process/golang/go-showcase`](https://github.com/Trendyol/stove/tree/main/recipes/process/golang/go-showcase).\n\n## Project Structure\n\n```\ngo-showcase/                   # Standalone Gradle project (copy-paste ready)\n  main.go                      # Entry point, env var config, graceful shutdown\n  db.go                        # PostgreSQL queries (auto-traced via otelsql)\n  handlers.go                  # HTTP handlers + Kafka publish (auto-traced via otelhttp)\n  kafka.go                     # KafkaProducer interface, factory, shared consumer handler\n  kafka_sarama.go              # IBM/sarama implementation\n  kafka_franz.go               # twmb/franz-go implementation\n  kafka_segmentio.go           # segmentio/kafka-go implementation\n  tracing.go                   # OpenTelemetry SDK initialization\n  go.mod\n  stovetests/                  # Kotlin Stove tests\n    kotlin/com/.../e2e/\n      setup/\n        StoveConfig.kt              # Single setup file (switches process/container via go.aut.mode)\n        ProductMigration.kt         # Creates products table\n      tests/\n        GoShowcaseTest.kt           # E2E tests\n    resources/\n      kotest.properties\n  build.gradle.kts             # Builds Go + runs Kotlin tests\n  settings.gradle.kts\n\n# Published Go library used by the showcase:\ngo/stove-kafka/                # Stove Kafka bridge for Go applications\n  bridge.go                    # Core bridge (library-agnostic gRPC client)\n  sarama/                      # IBM/sarama interceptors\n  franz/                       # twmb/franz-go hooks\n  segmentio/                   # segmentio/kafka-go helpers\n  stoveobserver/               # Generated gRPC code from messages.proto\n  go.mod\n```\n\n## The Go Application\n\nA minimal HTTP + PostgreSQL service. The key design choice: <span data-rn=\"underline\" data-rn-color=\"#009688\">all tracing is in the infrastructure layer</span>, not in business logic.\n\n### Entry Point\n\n```go title=\"main.go\"\nfunc main() {\n    // Ignore SIGPIPE so log writes to a closed stdout pipe don't kill the process\n    // when running under ProcessBuilder. Critical for graceful shutdown + coverage flush.\n    signal.Ignore(syscall.SIGPIPE)\n\n    ctx := context.Background()\n    port := getEnv(\"APP_PORT\", \"8080\")\n\n    shutdownTracing, _ := initTracing(ctx, \"go-showcase\")\n    defer shutdownTracing(ctx)\n\n    db, _ := initDB(connStr)  // otelsql wraps database/sql automatically\n    defer db.Close()\n\n    bridge, _ := stovekafka.NewBridgeFromEnv()  // nil in production — zero overhead\n    defer bridge.Close()\n\n    kafkaLibrary := getEnv(\"KAFKA_LIBRARY\", \"sarama\")\n    producer, stopKafka, _ := initKafka(kafkaLibrary, brokers, db, bridge)\n    defer stopKafka()\n\n    mux := http.NewServeMux()\n    registerRoutes(mux, db, producer)\n\n    handler := otelhttp.NewHandler(mux, \"http.request\")\n    server := &http.Server{Addr: \":\" + port, Handler: handler}\n    // ... graceful shutdown on SIGTERM\n}\n```\n\nConfiguration comes entirely from environment variables:\n\n| Variable | Purpose | Default |\n|----------|---------|---------|\n| `APP_PORT` | HTTP listen port | `8080` |\n| `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASS` | PostgreSQL connection | `localhost`, `5432`, `stove`, `sa`, `sa` |\n| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP gRPC endpoint for traces | *(disabled if empty)* |\n| `KAFKA_BROKERS` | Comma-separated Kafka broker addresses | *(disabled if empty)* |\n| `KAFKA_LIBRARY` | Kafka client library: `sarama`, `franz`, or `segmentio` | `sarama` |\n| `STOVE_KAFKA_BRIDGE_PORT` | Stove Kafka bridge gRPC port | *(disabled if empty, test-only)* |\n| `GOCOVERDIR` | Directory for Go integration test coverage data | *(disabled if empty, test-only)* |\n\n### Handlers, DB, Tracing\n\nHandlers and DB code are pure business logic — no tracing imports — because `otelhttp` and `otelsql` instrument transparently. See the [container guide](go-container.md) and the showcase repo for the full code; the same files are used in both modes.\n\n!!! tip \"Sync vs Batch Exporter\"\n    Use `sdktrace.WithSyncer(exporter)` for tests so spans are exported immediately when they end. In production, use `WithBatcher(exporter)` for performance. The 5-second default batch interval would cause test assertions to fail because spans wouldn't arrive in time.\n\n!!! info \"W3C Trace Context Propagation\"\n    Setting `propagation.TraceContext{}` is essential. Stove's HTTP client sends a `traceparent` header with each request. The `otelhttp` middleware extracts it, so all spans in the Go app share the same trace ID as the test — and the [Stove Dashboard](../Components/18-dashboard.md) and [MCP](../Components/21-mcp.md) tools can correlate them with the failure.\n\n## Kafka — `stove-kafka` bridge\n\nStove provides a Go bridge library (`stove-kafka`) that enables `shouldBeConsumed` and `shouldBePublished` assertions for Go applications. The bridge forwards produced/consumed messages over gRPC to Stove's `StoveKafkaObserverGrpcServer`. The core is library-agnostic; client-specific subpackages provide interceptors/hooks for popular Go Kafka libraries:\n\n| Library | Subpackage | Integration |\n|---------|-----------|-------------|\n| [IBM/sarama](https://github.com/IBM/sarama) | `sarama` | `ProducerInterceptor` / `ConsumerInterceptor` |\n| [twmb/franz-go](https://github.com/twmb/franz-go) | `franz` | `kgo.WithHooks(&franz.Hook{...})` |\n| [segmentio/kafka-go](https://github.com/segmentio/kafka-go) | `segmentio` | `segmentio.ReportWritten()` / `segmentio.ReportRead()` |\n\n!!! tip \"Using other Kafka libraries (e.g. confluent-kafka-go)\"\n    The subpackages above are conveniences. The core bridge (`PublishedMessage`, `ConsumedMessage`, `Bridge`) has **no Kafka client dependency**. For any library not listed above, import only the core package and call `bridge.ReportPublished()`, `bridge.ReportConsumed()`, and `bridge.ReportCommitted()` directly with your own type conversion.\n\nIn production, `STOVE_KAFKA_BRIDGE_PORT` is not set, so `NewBridgeFromEnv()` returns `nil`. All Bridge methods are nil-safe no-ops — zero overhead.\n\n### Integrating the Bridge\n\n```bash\ngo get github.com/trendyol/stove/go/stove-kafka\n```\n\n```go\nimport stovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n\nbridge, _ := stovekafka.NewBridgeFromEnv()\ndefer bridge.Close()\n```\n\nWire into your client:\n\n=== \"IBM/sarama\"\n\n    ```go\n    import stovesarama \"github.com/trendyol/stove/go/stove-kafka/sarama\"\n\n    config := sarama.NewConfig()\n    config.Producer.Interceptors = []sarama.ProducerInterceptor{\n        &stovesarama.ProducerInterceptor{Bridge: bridge},\n    }\n    config.Consumer.Interceptors = []sarama.ConsumerInterceptor{\n        &stovesarama.ConsumerInterceptor{Bridge: bridge},\n    }\n    ```\n\n=== \"twmb/franz-go\"\n\n    ```go\n    import \"github.com/trendyol/stove/go/stove-kafka/franz\"\n\n    client, err := kgo.NewClient(\n        kgo.SeedBrokers(\"localhost:9092\"),\n        kgo.WithHooks(&franz.Hook{Bridge: bridge}),\n    )\n    ```\n\n=== \"segmentio/kafka-go\"\n\n    ```go\n    import \"github.com/trendyol/stove/go/stove-kafka/segmentio\"\n\n    err := writer.WriteMessages(ctx, msgs...)\n    segmentio.ReportWritten(ctx, bridge, msgs...)\n\n    msg, err := reader.ReadMessage(ctx)\n    segmentio.ReportRead(ctx, bridge, msg)\n    ```\n\nWhen `Bridge` is nil (production), all interceptors/helpers return immediately with zero overhead.\n\n### Test-Friendly Kafka Settings\n\nWhen running against Testcontainers, configure Kafka clients for **fast feedback**:\n\n- **Auto-create topics** — the test container may not have topics pre-created\n- **Small batch size / low batch timeout** — flush produces immediately\n- **Short auto-commit interval** — make consumed offsets visible to Stove quickly\n\n=== \"IBM/sarama\"\n\n    ```go\n    config := sarama.NewConfig()\n    config.Producer.Return.Successes = true\n    config.Consumer.Offsets.Initial = sarama.OffsetOldest\n    config.Consumer.Offsets.AutoCommit.Interval = 100 * time.Millisecond\n    ```\n\n=== \"twmb/franz-go\"\n\n    ```go\n    kgo.AllowAutoTopicCreation(),\n    kgo.AutoCommitInterval(100 * time.Millisecond),\n    kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),\n    ```\n\n=== \"segmentio/kafka-go\"\n\n    ```go\n    writer := &kafka.Writer{\n        BatchSize:              1,\n        BatchTimeout:           10 * time.Millisecond,\n        AllowAutoTopicCreation: true,\n    }\n    reader := kafka.NewReader(kafka.ReaderConfig{\n        CommitInterval: 100 * time.Millisecond,\n        MaxWait:        500 * time.Millisecond,\n    })\n    ```\n\n!!! warning \"Production vs Test settings\"\n    These aggressive settings are optimized for test speed, not throughput. In production, use larger batch sizes, longer commit intervals, and broker-managed topic creation.\n\n### Consumer Groups\n\nEach Kafka library run uses a unique consumer group ID (`\"go-showcase-\" + library`) to prevent offset carryover between sequential test runs.\n\n## Stove Test Setup\n\n### Gradle Build\n\n```kotlin title=\"build.gradle.kts\"\nval goBinary = layout.buildDirectory.file(\"go-app\").get().asFile\nval goExecutable = providers.environmentVariable(\"GO_EXECUTABLE\").getOrElse(\"go\")\nval coverageEnabled = providers.gradleProperty(\"go.coverage\")\n    .map { it.toBoolean() }.getOrElse(false)\n\ntasks.register<Exec>(\"buildGoApp\") {\n    description = \"Compiles the Go application.\"\n    group = \"build\"\n    val args = mutableListOf(goExecutable, \"build\")\n    if (coverageEnabled) args.add(\"-cover\")\n    args.addAll(listOf(\"-o\", goBinary.absolutePath, \".\"))\n    commandLine(args)\n    inputs.files(fileTree(\".\") { include(\"*.go\", \"go.mod\", \"go.sum\") })\n    outputs.file(goBinary)\n}\n\n// Per-library e2e test tasks\nval kafkaLibraries = listOf(\"sarama\", \"franz\", \"segmentio\")\nval kafkaE2eTasks = kafkaLibraries.mapIndexed { index, lib ->\n    tasks.register<Test>(\"e2eTest_$lib\") {\n        dependsOn(\"buildGoApp\")\n        systemProperty(\"go.aut.mode\", \"process\")\n        systemProperty(\"go.app.binary\", goBinary.absolutePath)\n        systemProperty(\"kafka.library\", lib)\n        if (index > 0) mustRunAfter(\"e2eTest_${kafkaLibraries[index - 1]}\")\n    }\n}\ntasks.named<Test>(\"e2eTest\") { dependsOn(kafkaE2eTasks); enabled = false }\n\ndependencies {\n    testImplementation(stoveLibs.stove)\n    testImplementation(stoveLibs.stoveProcess)\n    testImplementation(stoveLibs.stovePostgres)\n    testImplementation(stoveLibs.stoveHttp)\n    testImplementation(stoveLibs.stoveTracing)\n    testImplementation(stoveLibs.stoveDashboard)\n    testImplementation(stoveLibs.stoveKafka)\n    testImplementation(stoveLibs.stoveExtensionsKotest)\n}\n```\n\n### Stove Configuration\n\n```kotlin title=\"StoveConfig.kt\"\nStove()\n    .with {\n        httpClient {\n            HttpClientSystemOptions(baseUrl = \"http://localhost:$APP_PORT\")\n        }\n\n        dashboard {\n            DashboardSystemOptions(appName = \"go-showcase\")\n        }\n\n        tracing {\n            enableSpanReceiver(port = OTLP_PORT)\n        }\n\n        kafka {\n            KafkaSystemOptions(\n                configureExposedConfiguration = { cfg ->\n                    listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n                }\n            )\n        }\n\n        postgresql {\n            PostgresqlOptions(\n                databaseName = \"stove\",\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"database.host=${cfg.host}\",\n                        \"database.port=${cfg.port}\",\n                        \"database.name=stove\",\n                        \"database.username=${cfg.username}\",\n                        \"database.password=${cfg.password}\"\n                    )\n                }\n            ).migrations {\n                register<ProductMigration>()\n            }\n        }\n\n        goApp(\n            target = ProcessTarget.Server(port = APP_PORT, portEnvVar = \"APP_PORT\"),\n            envProvider = envMapper {\n                \"database.host\" to \"DB_HOST\"\n                \"database.port\" to \"DB_PORT\"\n                \"database.name\" to \"DB_NAME\"\n                \"database.username\" to \"DB_USER\"\n                \"database.password\" to \"DB_PASS\"\n                \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n                env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:$OTLP_PORT\")\n                env(\"KAFKA_LIBRARY\") { System.getProperty(\"kafka.library\") ?: \"sarama\" }\n                env(\"STOVE_KAFKA_BRIDGE_PORT\", stoveKafkaBridgePortDefault)\n                env(\"GOCOVERDIR\") {\n                    System.getProperty(\"go.cover.dir\")\n                        ?.also { java.io.File(it).mkdirs() } ?: \"\"\n                }\n            }\n        )\n    }.run()\n```\n\nThe `envMapper` block declaratively maps Stove's exposed configurations to environment variables the Go app expects. Use `\"stoveKey\" to \"ENV_VAR\"` for config-derived values and `env(\"NAME\", \"value\")` for static ones. For apps that prefer CLI arguments, use `argsMapper` instead (or alongside).\n\n### Database Migration\n\n```kotlin title=\"ProductMigration.kt\"\nclass ProductMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n    override val order: Int = 1\n\n    override suspend fun execute(connection: PostgresSqlMigrationContext) {\n        connection.sql.execute(\n            queryOf(\"\"\"\n                CREATE TABLE IF NOT EXISTS products (\n                    id VARCHAR(255) PRIMARY KEY,\n                    name VARCHAR(255) NOT NULL,\n                    price DECIMAL(10, 2) NOT NULL\n                )\n            \"\"\").asExecute\n        )\n    }\n}\n```\n\n## Writing Tests\n\n```kotlin title=\"GoShowcaseTest.kt\"\nclass GoShowcaseTest : FunSpec({\n    test(\"create product, verify HTTP, DB, Kafka, traces\") {\n        stove {\n            var productId: String? = null\n\n            http {\n                postAndExpectBody<ProductResponse>(\n                    uri = \"/api/products\",\n                    body = CreateProductRequest(name = \"Test\", price = 42.99).some()\n                ) { actual ->\n                    actual.status shouldBe 201\n                    productId = actual.body().id\n                }\n            }\n\n            postgresql {\n                shouldQuery<ProductRow>(\n                    query = \"SELECT id, name, price FROM products WHERE id = '$productId'\",\n                    mapper = productRowMapper\n                ) { rows -> rows.size shouldBe 1 }\n            }\n\n            kafka {\n                shouldBePublished<ProductCreatedEvent>(10.seconds) {\n                    actual.name == \"Test\"\n                }\n            }\n\n            tracing {\n                waitForSpans(4, 5000)\n                shouldContainSpan(\"http.request\")\n                shouldNotHaveFailedSpans()\n            }\n        }\n    }\n})\n```\n\nVerify the Go app consumes events and updates state:\n\n```kotlin\ntest(\"consume product update events from Kafka\") {\n    stove {\n        var productId: String? = null\n\n        http {\n            postAndExpectBody<ProductResponse>(\n                uri = \"/api/products\",\n                body = CreateProductRequest(name = \"Original\", price = 10.0).some()\n            ) { actual -> productId = actual.body().id }\n        }\n\n        kafka {\n            publish(\"product.update\", ProductUpdateEvent(id = productId!!, name = \"Updated\", price = 99.99))\n            shouldBeConsumed<ProductUpdateEvent>(10.seconds) {\n                actual.id == productId && actual.name == \"Updated\"\n            }\n        }\n\n        postgresql {\n            shouldQuery<ProductRow>(\n                query = \"SELECT id, name, price FROM products WHERE id = '$productId'\",\n                mapper = productRowMapper\n            ) { rows -> rows.first().name shouldBe \"Updated\" }\n        }\n    }\n}\n```\n\n## Dashboard & MCP\n\nWhen the [`stove` CLI](../Components/18-dashboard.md) is running, the Go run streams to `http://localhost:4040` like any JVM run — timeline, traces, snapshots, Kafka explorer.\n\nFor AI-assisted triage, the same CLI exposes a [Model Context Protocol endpoint](../Components/21-mcp.md) at `http://localhost:4040/mcp`. Agents call `stove_failures` to discover failed Go tests, then `stove_failure_detail`, `stove_timeline`, `stove_trace`, and `stove_snapshot` for compact, structured evidence — no log scraping required.\n\n## Code Coverage\n\nFor both process-mode and container-mode AUT runs, Stove executes your app outside `go test`, so standard `go test -cover` doesn't apply. Go 1.20+ integration coverage fits this model: build with `go build -cover`, set `GOCOVERDIR`, and flush data on graceful shutdown (SIGTERM).\n\n### How It Works\n\n```\n1. go build -cover          → instruments the binary\n2. GOCOVERDIR=/path         → tells the binary where to write coverage data\n3. SIGTERM (Stove stop)     → graceful shutdown triggers coverage flush\n4. go tool covdata textfmt  → converts raw data to standard coverage.out\n5. go tool cover -func/-html → human-readable reports\n```\n\n### Gradle Setup\n\nThe recipe supports coverage via the `-Pgo.coverage=true` Gradle property. When disabled (default), there is zero overhead.\n\n```kotlin title=\"build.gradle.kts\"\nval coverageEnabled = providers.gradleProperty(\"go.coverage\")\n    .map { it.toBoolean() }.getOrElse(false)\nval goCoverDirPath = layout.buildDirectory.dir(\"go-coverage\").get().asFile.absolutePath\n\ntasks.register<Exec>(\"buildGoApp\") {\n    val args = mutableListOf(goExecutable, \"build\")\n    if (coverageEnabled) args.add(\"-cover\")\n    args.addAll(listOf(\"-o\", goBinary.absolutePath, \".\"))\n    commandLine(args)\n}\n\ntasks.register<Test>(\"e2eTest_sarama\") {\n    if (coverageEnabled) {\n        systemProperty(\"go.cover.dir\", goCoverDirPath)\n        outputs.cacheIf { false }  // Coverage data is a side effect\n    }\n}\n\nif (coverageEnabled) {\n    tasks.register<Exec>(\"goCoverageReport\") {\n        mustRunAfter(kafkaE2eTasks)\n        commandLine(goExecutable, \"tool\", \"covdata\", \"textfmt\",\n            \"-i=$goCoverDirPath\", \"-o=$goCoverOutPath\")\n    }\n    tasks.register<Exec>(\"goCoverageSummary\") {\n        dependsOn(\"goCoverageReport\")\n        commandLine(goExecutable, \"tool\", \"cover\", \"-func=$goCoverOutPath\")\n    }\n    tasks.register<Exec>(\"goCoverageHtml\") {\n        dependsOn(\"goCoverageReport\")\n        commandLine(goExecutable, \"tool\", \"cover\", \"-html=$goCoverOutPath\", \"-o=coverage.html\")\n    }\n    tasks.register(\"e2eTestWithCoverage\") {\n        dependsOn(kafkaE2eTasks)\n        finalizedBy(\"goCoverageSummary\", \"goCoverageHtml\")\n    }\n}\n```\n\n### SIGPIPE Handling\n\nWhen a Go process runs under Java's `ProcessBuilder`, the stdout pipe can close before the process exits. If Go writes to the closed pipe (e.g. `log.Println` during shutdown), it receives SIGPIPE and terminates immediately — before the coverage counters are flushed. Add this at the top of `main()`:\n\n```go title=\"main.go\"\nfunc main() {\n    signal.Ignore(syscall.SIGPIPE)\n    // ...\n}\n```\n\nThis is good practice for any long-running Go service managed by an external process, not just for coverage.\n\n### Running\n\n```bash\n# Without coverage (default — zero overhead)\n./gradlew e2eTest_sarama\n\n# With coverage — runs tests + generates reports\n./gradlew e2eTestWithCoverage -Pgo.coverage=true\n```\n\nThe HTML report is written to `build/go-coverage/coverage.html`. Container-mode coverage uses the same flag — see [Container Mode](go-container.md#code-coverage).\n\n!!! tip \"Why no Stove framework changes were needed\"\n    Everything is achievable with existing primitives: the `-cover` build flag is a Gradle concern, `GOCOVERDIR` is just another env var, coverage processing happens after tests, and graceful shutdown is handled by the AUT starter (`stove-process` or `stove-container`).\n\n## How Tracing Flows\n\n```\n1. StoveKotestExtension starts a TraceContext before each test\n2. Stove HTTP client injects `traceparent` header into requests\n3. otelhttp middleware extracts traceparent, creates HTTP span as child\n4. Handler passes r.Context() to DB functions\n5. otelsql creates DB spans as children of the HTTP span\n6. All spans share the same trace ID as the test\n7. Spans are exported via OTLP gRPC to Stove's receiver\n8. tracing { shouldContainSpan(...) } queries spans by trace ID\n```\n\n## Running\n\n```bash\n# From the go-showcase directory — runs all three Kafka libraries\ncd recipes/process/golang/go-showcase\n./gradlew e2eTest\n\n./gradlew e2eTest_sarama\n./gradlew e2eTest_franz\n./gradlew e2eTest_segmentio\n\n./gradlew e2eTestWithCoverage -Pgo.coverage=true\n```\n\n## Go Dependencies\n\n```\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp  # HTTP middleware\ngo.opentelemetry.io/otel                                        # OTel API\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc # OTLP gRPC exporter\ngo.opentelemetry.io/otel/sdk                                    # OTel SDK\ngithub.com/XSAM/otelsql                                         # database/sql auto-instrumentation\ngithub.com/lib/pq                                                # PostgreSQL driver\ngoogle.golang.org/grpc                                           # gRPC (for OTLP + bridge)\n\n# Kafka — pick one client + its bridge subpackage:\ngithub.com/IBM/sarama                                            # + stove-kafka/sarama\ngithub.com/twmb/franz-go/pkg/kgo                                 # + stove-kafka/franz\ngithub.com/segmentio/kafka-go                                    # + stove-kafka/segmentio\ngithub.com/trendyol/stove/go/stove-kafka                        # Core bridge (always needed)\n```\n"
  },
  {
    "path": "docs/other-languages/go.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">Go</span>\n\nStove treats Go as a <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">first-class application under test</span>. The same Stove DSL — `http {}`, `postgresql {}`, `kafka {}`, `tracing {}`, `dashboard {}` — drives a Go service end to end. Distributed traces, dashboard streams, and integration coverage all flow through the standard Stove pipeline.\n\nThe full source is at [`recipes/process/golang/go-showcase`](https://github.com/Trendyol/stove/tree/main/recipes/process/golang/go-showcase). One showcase, two AUT modes — pick the one that matches what you want to test.\n\n## Pick a mode\n\n| Mode | Starter | When to use | Trade-off |\n|------|---------|-------------|-----------|\n| **Process** | `stove-process` (`goApp` / `processApp`) | Fast local iteration, direct binary run, easiest debugging | You manage host runtime/binary alignment |\n| **Container** | `stove-container` (`containerApp`) | CI parity with the production image, environment isolation | Image build adds setup cost |\n\nRule of thumb: start with [process mode](go-process.md) for fast feedback, then add [container mode](go-container.md) when you want image-level confidence in CI. The same Kotlin tests run against either.\n\n<div class=\"grid cards\" markdown>\n\n-   :material-language-go: **Process Mode**\n\n    Run the Go binary directly. Fastest iteration loop.\n\n    [Process Mode guide :material-arrow-right:](go-process.md)\n\n-   :material-docker: **Container Mode**\n\n    Run the production Docker image. CI-grade parity.\n\n    [Container Mode guide :material-arrow-right:](go-container.md)\n\n</div>\n\n## What you get out of the box\n\n- **HTTP, PostgreSQL, Kafka, MongoDB, Redis, …** — every Stove system works against a Go AUT\n- **Distributed tracing** via OpenTelemetry — spans from Go appear in the same trace tree as the test\n- **Dashboard** — the Go run streams to `http://localhost:4040` like any JVM run\n- **MCP triage** — failed Go runs are queryable through the [`stove` CLI MCP server](../Components/21-mcp.md)\n- **Kafka assertions** — `shouldBePublished` / `shouldBeConsumed` work for Go via the [`stove-kafka`](https://github.com/trendyol/stove/tree/main/go/stove-kafka) bridge (sarama, franz-go, segmentio, or any client via the core API)\n- **Integration coverage** — `go build -cover` + `GOCOVERDIR` collected on graceful shutdown, with HTML/summary reports\n\n## Adapting for other languages\n\nThe same model works for any language. Replace the Go-specific parts (build step, OTel SDK, Kafka bridge):\n\n| Part | Go | Python | Node.js | Rust |\n|------|-----|--------|---------|------|\n| **Build step** | `go build` | *(none or pip install)* | `npm install && npm run build` | `cargo build` |\n| **AUT runner** | `goApp()` / `containerApp()` | `processApp()` / `containerApp()` | `processApp()` / `containerApp()` | `processApp()` / `containerApp()` |\n| **OTel HTTP** | `otelhttp.NewHandler` | `opentelemetry-instrumentation-flask` | `@opentelemetry/instrumentation-http` | `tracing-opentelemetry` |\n| **OTel DB** | `otelsql` | `opentelemetry-instrumentation-psycopg2` | `@opentelemetry/instrumentation-pg` | `tracing-opentelemetry` |\n| **Kafka assertions** | `stove-kafka` bridge | *(bridge library needed)* | *(bridge library needed)* | *(bridge library needed)* |\n\nThe Kotlin test side stays exactly the same — only the AUT runner and config mapping differ.\n"
  },
  {
    "path": "docs/other-languages/index.md",
    "content": "# Other Languages & Stacks\n\nStove ships with JVM framework starters (Spring Boot, Ktor, Micronaut, Quarkus), but the core testing model isn't limited to JVM applications. You can use Stove to <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">test any application that speaks HTTP, databases, and messaging</span> --- regardless of the language it's written in.\n\nThe key choice is *how* the application under test runs:\n\n- **`stove-process`** — start a host binary (`processApp` / `goApp`). Fastest iteration loop. Zero infrastructure beyond your compiler.\n- **`stove-container`** — start a Docker image (`containerApp`). CI-grade parity with the artifact you ship to production.\n\nBoth expose the same envelope: env-var or CLI-arg config mapping, readiness strategies, graceful shutdown, and the same `stove { http { … } postgresql { … } kafka { … } tracing { … } }` test DSL.\n\n## How It Works\n\n```mermaid\ngraph LR\n    S[Stove Test<br>Kotlin] -->|starts containers| PG[(PostgreSQL)]\n    S -->|starts containers| K[(Kafka)]\n    S -->|starts OTLP receiver| T[Tracing]\n    S -->|process or container| APP[Your App<br>Go / Python / Rust / ...]\n    APP -->|connects| PG\n    APP -->|connects| K\n    APP -->|exports spans| T\n    S -->|HTTP / gRPC assertions| APP\n    S -->|DB assertions| PG\n    S -->|trace assertions| T\n```\n\nStove starts the infrastructure (databases, message brokers, OTLP receiver), launches your application as either an OS process or a Docker container with the right connection details, and runs tests against it using the same DSL you'd use for JVM apps.\n\n## Supported Languages\n\nAny language that can:\n\n1. **Read environment variables (or CLI args)** — to receive database URLs, ports, and credentials\n2. **Expose a readiness signal** — typically an HTTP `/health` endpoint, but TCP / custom probes / fixed delay are supported\n3. **Shut down on SIGTERM** — for clean test teardown (and for things like Go integration coverage flushing)\n\n<div class=\"grid cards\" markdown>\n\n-   :material-language-go: **Go** — first-class\n\n    Full walkthrough across both modes: HTTP + PostgreSQL + Kafka + OpenTelemetry + Dashboard + MCP + integration coverage.\n\n    [Overview :material-arrow-right:](go.md) · [Process Mode :material-arrow-right:](go-process.md) · [Container Mode :material-arrow-right:](go-container.md)\n\n</div>\n\n## Process vs. Container at a glance\n\n| Concern | `stove-process` | `stove-container` |\n|---------|----------------|-------------------|\n| **Starter** | `goApp()` / `processApp()` | `containerApp()` |\n| **AUT artifact** | Host binary | Docker image |\n| **Iteration speed** | Fast (compile + run) | Slower (image build) |\n| **Production parity** | Approximate (host runtime) | Exact |\n| **CI fit** | Smoke / inner loop | Pre-merge / release validation |\n| **Networking** | Loopback | Host network *or* port binding |\n| **Filesystem isolation** | Host filesystem | Container layer + bind mounts |\n| **Common pitfalls** | Glibc/runtime drift hidden | Network mode + port binding wiring |\n\nA common pattern: `e2eTest` uses process mode for daily development, `e2eTest-container` runs container mode in CI. Same Kotlin tests, same StoveConfig, branched only on a `-Dgo.aut.mode=process|container` system property.\n\n## At A Glance vs. JVM apps\n\n| Concern | JVM App (Spring Boot, etc.) | Non-JVM App (Go, Python, etc.) |\n|---------|---------------------------|-------------------------------|\n| **Application startup** | Framework starter (`springBoot()`, `ktor()`) | `goApp()` / `processApp()` (`stove-process`) or `containerApp()` (`stove-container`) |\n| **Config passing** | JVM system properties / Spring properties | `envMapper` / `argsMapper` |\n| **Infrastructure** | Same (`postgresql {}`, `kafka {}`, `http {}`) | Same |\n| **Test DSL** | Same (`stove { http { ... } postgresql { ... } }`) | Same |\n| **Tracing** | OTel Java Agent (automatic) | OTel SDK for your language (e.g., `otelhttp`, `otelsql`) |\n| **Dashboard** | Same (`dashboard {}`) | Same |\n| **MCP triage** | Same (`stove` CLI, `/mcp`) | Same |\n| **Bridge (`using<T> {}`)** | Yes (access DI container) | No (separate process / container) |\n\n## The Pattern\n\nEvery non-JVM integration follows the same three steps, regardless of language or mode:\n\n### 1. Choose a starter and wire the AUT\n\n```kotlin\n// Process mode\ngoApp(\n    target = ProcessTarget.Server(port = 8090, portEnvVar = \"APP_PORT\"),\n    envProvider = envMapper { /* ... */ }\n)\n\n// Container mode\ncontainerApp(\n    image = \"my-app:local\",\n    target = ContainerTarget.Server(hostPort = 8090, internalPort = 8090, portEnvVar = \"APP_PORT\"),\n    envProvider = envMapper { /* ... */ },\n    configureContainer = { withNetworkMode(\"host\") }\n)\n```\n\n### 2. Instrument your app with OpenTelemetry\n\nUse your language's OTel SDK. Stove starts an OTLP gRPC receiver and passes the endpoint via env vars. Your app exports spans to Stove, and Stove correlates them back to the test via W3C `traceparent` headers.\n\n### 3. Write tests with the standard DSL\n\nTests look identical to JVM tests — `http {}`, `postgresql {}`, `kafka {}`, `tracing {}`, `dashboard {}` all work the same way.\n\n## What You Can't Do\n\nSince the application runs as a separate OS process or container:\n\n- **No `bridge()` / `using<T> {}`** — you can't access the app's internal state or DI container\n\nEverything else works: HTTP assertions, database queries, Kafka publishing and consuming (`shouldBePublished`, `shouldBeConsumed`), tracing, WireMock, gRPC, the [Dashboard](../Components/18-dashboard.md), and the [MCP](../Components/21-mcp.md) triage tools.\n\n!!! info \"Kafka assertions for non-JVM apps\"\n    Stove provides bridge libraries that enable `shouldBeConsumed` and `shouldBePublished` assertions for non-JVM applications. The [`stove-kafka`](https://github.com/trendyol/stove/tree/main/go/stove-kafka) Go library supports IBM/sarama (interceptors), twmb/franz-go (hooks), and segmentio/kafka-go (helpers), and forwards messages via gRPC to Stove's observer. The library-agnostic core also lets you wire any other Kafka client (e.g. confluent-kafka-go) yourself.\n\n## Next Steps\n\n- [Go overview](go.md) — pick the right mode for your needs\n- [Go Process Mode](go-process.md) — fastest iteration, full HTTP + PG + Kafka + tracing + coverage walkthrough\n- [Go Container Mode](go-container.md) — CI-grade parity with the production image\n- [Provided Application](../Components/19-provided-application.md) — for testing already-deployed apps (black-box)\n- [Dashboard](../Components/18-dashboard.md) — live timeline, traces, snapshots, Kafka explorer\n- [MCP](../Components/21-mcp.md) — agent-friendly endpoint for failed-test triage\n- [Writing Custom Systems](../writing-custom-systems.md) — extend Stove with new component types\n"
  },
  {
    "path": "docs/release-notes/0.15.0.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">0.15.0</span>\n\n## From 0.14.x to 0.15.x\n\n### Breaking Changes\n\nThe most notable breaking change is <span data-rn=\"highlight\" data-rn-color=\"#ff980055\" data-rn-duration=\"800\">ser/de operations</span>. The framework was only relying on Jackson for serialization and\ndeserialization. Now, it provides a way to use other serialization libraries. `StoveSerde<TIn, TOut>` is the new interface\nthat you can implement to provide your own serialization and deserialization logic.\n\n`StoveSerde` also provides the access to the other serializers that `com-trendyol:stove-testing-e2e` package has:\n\n* Jackson\n* Gson\n* Kotlinx\n\nSee the component guides for the current serialization configuration examples.\n\n#### Spring Kafka (com-trendyol:stove-spring-testing-e2e-kafka)\n\nThe `TestSystemKafkaInterceptor` now depends on `StoveSerde` to provide the serialization and deserialization logic instead of `ObjectMapper`.\n\nYou can of course use your default Jackson implementation by providing the `ObjectMapperConfig.default()` to the `StoveSerde.jackson.anyByteArraySerde` function.\n\n```kotlin hl_lines=\"3 4\"\nclass TestSystemInitializer : BaseApplicationContextInitializer({\n  bean<TestSystemKafkaInterceptor<*, *>>(isPrimary = true)\n  bean { StoveSerde.jackson.anyByteArraySerde(ObjectMapperConfig.default()) } // or any other serde that is <Any, ByteArray>\n})\n```\n\n### Standalone Kafka\n\n```kotlin hl_lines=\"3\"\nkafka {\n  KafkaSystemOptions(\n    serde = StoveSerde.jackson.anyByteArraySerde(ObjectMapperConfig.default) // or any other serde that is <Any, ByteArray>\n    //...\n  )\n}\n```\n\n### Couchbase\n\n```kotlin hl_lines=\"4\"\ncouchbase {\n  CouchbaseSystemOptions(\n    clusterSerDe = JacksonJsonSerializer(CouchbaseConfiguration.objectMapper), // here you can provide your own serde\n    //...\n  )\n}\n```\n\n### Http\n\n```kotlin\n httpClient {\n  HttpClientSystemOptions(\n    baseUrl = \"http://localhost:8001\",\n    contentConverter = JacksonConverter(ObjectMapperConfig.default)\n  )\n}\n```\n\n### Wiremock\n\n```kotlin\nwiremock {\n  WireMockSystemOptions(\n    port = 9090,\n    serde = StoveSerde.jackson.anyByteArraySerde(ObjectMapperConfiguration.default)\n  )\n```\n\n### Elasticsearch\n\n```kotlin\nelasticsearch {\n  ElasticsearchSystemOptions(\n    jsonpMapper = JacksonJsonpMapper(StoveSerde.jackson.default), // or any JsonpMapper\n  )\n}\n```\n\n### Mongodb\n\n```kotlin\nmongodb {\n  MongoDbSystemOptions(\n    serde = StoveSerde.jackson.default // or any other serde that you implement\n  )\n}\n```\n\nThe default serde is:\n```kotlin\n  val serde: StoveSerde<Any, String> = StoveSerde.jackson.anyJsonStringSerde(\n    StoveSerde.jackson.byConfiguring {\n      disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n      enable(MapperFeature.DEFAULT_VIEW_INCLUSION)\n      addModule(ObjectIdModule())\n      addModule(KotlinModule.Builder().build())\n    }\n  ),\n```\n"
  },
  {
    "path": "docs/release-notes/0.19.0.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">0.19.0</span>\n\n**Release Date:** December 2025\n\nThis release introduces <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">gRPC support, WebSocket testing, and provided instances for external infrastructure</span>:\n\n- **gRPC System**: New component for testing gRPC APIs (grpc-kotlin, Wire)\n- **WebSocket Testing**: Added to HTTP system for real-time communication testing\n- **Partial Mocking**: WireMock now supports `mockPostContaining`, `mockPutContaining`, `mockPatchContaining`\n- **Embedded Kafka**: Run Kafka tests without Docker using `useEmbeddedKafka = true`\n- **Provided Instances**: PostgreSQL, MSSQL, MongoDB, Couchbase, Elasticsearch, Redis, and Kafka support connecting to external infrastructure\n- **Pause/Unpause**: PostgreSQL, MSSQL, MongoDB, Couchbase, Elasticsearch, Redis, and Kafka support container pause/unpause for resilience testing\n- **Response Headers**: WireMock mocks now support custom response headers\n\n---\n\n## New Features\n\n### gRPC Support\n\nStove now supports testing gRPC APIs with a fluent DSL. The new `grpc` system works with multiple gRPC providers including grpc-kotlin, Wire, and standard gRPC stubs.\n\n```kotlin hl_lines=\"3 8\"\n// Using typed channels (grpc-kotlin, Wire stubs)\ngrpc {\n    channel<GreeterServiceStub> {\n        val response = sayHello(HelloRequest(name = \"World\"))\n        response.message shouldBe \"Hello, World!\"\n    }\n}\n\n// Using Wire clients\ngrpc {\n    wireClient<GreeterServiceClient> {\n        val response = SayHello().execute(HelloRequest(name = \"World\"))\n        response.message shouldBe \"Hello, World!\"\n    }\n}\n```\n\nAll streaming types work naturally with Kotlin coroutines:\n\n```kotlin\ngrpc {\n    channel<StreamServiceStub> {\n        // Server streaming\n        serverStream(request).collect { response ->\n            // assertions on each response\n        }\n\n        // Client streaming\n        val response = clientStream(flow { emit(request1); emit(request2) })\n\n        // Bidirectional streaming\n        bidiStream(requestFlow).collect { response ->\n            // assertions\n        }\n    }\n}\n```\n\n**Add the dependency:**\n\n```kotlin\ntestImplementation(\"com.trendyol:stove-testing-e2e-grpc:$version\")\n```\n\n---\n\n### WebSocket Testing\n\nThe HTTP system now supports WebSocket connections for testing real-time communication:\n\n```kotlin hl_lines=\"2 7\"\nhttp {\n    webSocket(\"/chat\") { session ->\n        session.send(\"Hello!\")\n        val response = session.receiveText()\n        response shouldBe \"Echo: Hello!\"\n    }\n}\n\n// With authentication\nhttp {\n    webSocket(\n        uri = \"/secure-chat\",\n        headers = mapOf(\"X-Custom-Header\" to \"value\"),\n        token = \"jwt-token\".some()\n    ) { session ->\n        session.send(\"Authenticated message\")\n    }\n}\n\n// Collect multiple messages\nhttp {\n    webSocketExpect(\"/notifications\") { session ->\n        val messages = session.collectTexts(count = 3)\n        messages.size shouldBe 3\n    }\n}\n```\n\nAvailable methods:\n- `webSocket` - Establish connection and interact\n- `webSocketExpect` - Assertion-focused testing\n- `webSocketRaw` - Direct access to underlying Ktor session\n\n---\n\n### Partial Mocking for WireMock\n\nNew partial matching methods allow mocking requests by matching only specific fields in the request body:\n\n```kotlin hl_lines=\"4 12\"\nwiremock {\n    // Match requests containing specific fields (ignores extra fields)\n    mockPostContaining(\n        url = \"/api/orders\",\n        requestContaining = mapOf(\n            \"productId\" to 123,\n            \"order.customer.id\" to \"cust-456\"  // Dot notation for nested fields\n        ),\n        statusCode = 201,\n        responseBody = OrderResponse(id = \"order-1\").some()\n    )\n}\n```\n\n**Features:**\n- **AND logic**: All specified fields must match\n- **Dot notation**: Access nested fields like `\"order.customer.id\"`\n- **Partial objects**: Nested objects match if they contain at least the specified fields\n- **Methods**: `mockPostContaining`, `mockPutContaining`, `mockPatchContaining`\n\n---\n\n### Embedded Kafka Mode\n\nRun Kafka tests without Docker containers using embedded Kafka:\n\n```kotlin hl_lines=\"4 5\"\nkafka {\n    KafkaSystemOptions(\n        useEmbeddedKafka = true,  // No container needed\n        configureExposedConfiguration = { cfg ->\n            listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n        }\n    )\n}\n```\n\nThis is ideal for:\n- Self-contained integration tests\n- Faster test startup\n- Environments without Docker access\n\n---\n\n## Improvements\n\n### Provided Instances (Testcontainer-less Mode)\n\nThe following components now support connecting to externally managed infrastructure using the `provided()` companion function:\n\n| Component     | Provided Instance Support |\n|---------------|:-------------------------:|\n| PostgreSQL    |             ✅             |\n| MSSQL         |             ✅             |\n| MongoDB       |             ✅             |\n| Couchbase     |             ✅             |\n| Elasticsearch |             ✅             |\n| Redis         |             ✅             |\n| Kafka         |             ✅             |\n\n**PostgreSQL example:**\n\n```kotlin\npostgresql {\n    PostgresqlOptions.provided(\n        host = \"external-db.example.com\",\n        port = 5432,\n        databaseName = \"testdb\",\n        username = \"user\",\n        password = \"pass\",\n        runMigrations = true,\n        cleanup = { client -> client.execute(\"TRUNCATE users\") },\n        configureExposedConfiguration = { cfg ->\n            listOf(\"spring.datasource.url=${cfg.jdbcUrl}\")\n        }\n    )\n}\n```\n\n**Kafka example:**\n\n```kotlin\nkafka {\n    KafkaSystemOptions.provided(\n        bootstrapServers = \"kafka.example.com:9092\",\n        runMigrations = true,\n        cleanup = { admin -> admin.deleteTopics(listOf(\"orders\")) },\n        configureExposedConfiguration = { cfg ->\n            listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n        }\n    )\n}\n```\n\nThis is useful for:\n- CI/CD pipelines with shared infrastructure\n- Reducing startup time by reusing existing instances\n- Lower memory/CPU usage by avoiding container overhead\n\n---\n\n### Pause/Unpause Containers\n\nPostgreSQL, MSSQL, MongoDB, Couchbase, Elasticsearch, Redis, and Kafka now support pausing and unpausing containers for testing resilience scenarios:\n\n```kotlin\npostgresql {\n    // Pause to simulate network issues\n    pause()\n    \n    // Your application should handle the connection failure\n    http { get<Response>(\"/health\") { it.status shouldBe 503 } }\n    \n    // Unpause to restore connectivity\n    unpause()\n}\n```\n\n---\n\n### Response Headers in WireMock\n\nWireMock now supports custom response headers:\n\n```kotlin\nwiremock {\n    mockGet(\n        url = \"/api/users/123\",\n        statusCode = 200,\n        responseBody = user.some(),\n        responseHeaders = mapOf(\n            \"X-Request-Id\" to \"req-123\",\n            \"X-Rate-Limit-Remaining\" to \"99\"\n        )\n    )\n}\n```\n\n---\n\n### Documentation Improvements\n\n- Comprehensive documentation for all components\n- Updated examples matching actual API signatures\n- Added component feature matrix showing migration, cleanup, and provided instance support\n- FAQ section with common questions and answers\n\n---\n\n## Dependency Updates\n\n- Kotlin 2.0.x\n- Kotest 6.0.0.Mx\n- Koin 4.x\n- Arrow 2.x\n- Testcontainers 2.x\n- Ktor 3.x\n- Various other dependency updates for security and compatibility\n\n---\n\n## Migration Guide\n\n### From 0.18.x\n\nThis release is backward compatible. New features are opt-in:\n\n| Feature | How to Enable |\n|---------|---------------|\n| gRPC testing | Add `stove-testing-e2e-grpc` dependency |\n| WebSocket testing | Use `http { webSocket(\"/path\") { ... } }` |\n| Embedded Kafka | Set `useEmbeddedKafka = true` in `KafkaSystemOptions` |\n| Provided instances | Use `SystemOptions.provided(...)` instead of `SystemOptions(...)` |\n| Pause/Unpause | Call `system.pause()` and `system.unpause()` on container-based systems |\n| Partial WireMock mocking | Use `mockPostContaining`, `mockPutContaining`, `mockPatchContaining` |\n| Response headers | Pass `responseHeaders = mapOf(...)` to WireMock mock methods |\n\n### Breaking Changes\n\nNone in this release.\n\n---\n\n## Full Changelog\n\nSee the [GitHub Releases](https://github.com/Trendyol/stove/releases) page for the complete list of commits and contributors.\n\n---\n\n## Contributors\n\nThanks to all contributors who made this release possible!\n\n---\n\n## Getting Started\n\n```kotlin\ndependencies {\n    testImplementation(\"com.trendyol:stove-testing-e2e:0.19.0\")\n    testImplementation(\"com.trendyol:stove-spring-testing-e2e:0.19.0\")  // or ktor, micronaut\n    // Add component-specific dependencies as needed\n    testImplementation(\"com.trendyol:stove-testing-e2e-rdbms-postgres:0.19.0\")\n    testImplementation(\"com.trendyol:stove-testing-e2e-kafka:0.19.0\")\n    testImplementation(\"com.trendyol:stove-testing-e2e-grpc:0.19.0\")  // NEW\n}\n```\n\nFor snapshot versions, add the snapshot repository:\n\n```kotlin\nrepositories {\n    maven(\"https://central.sonatype.com/repository/maven-snapshots\")\n}\n```\n"
  },
  {
    "path": "docs/release-notes/0.20.0.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">0.20.0</span>\n\n**Release Date:** January 2026\n\nThis release introduces <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">simplified module names, a new BOM, and a comprehensive test reporting system</span>:\n\n- **Simplified Module Names**: All modules renamed to remove `testing-e2e` suffix for cleaner artifact names\n- **New BOM (Bill of Materials)**: Centralized version management via `stove-bom`\n- **Package Structure Simplification**: Package names simplified from `com.trendyol.stove.testing.e2e.*` to `com.trendyol.stove.*`\n- **Test Reporting System**: Comprehensive reporting that tracks all actions and assertions during test execution\n- **Spring Boot 4.x Support**: Full support for Spring Boot 4.x and Spring Kafka 4.x\n- **Unified Spring Modules**: Single modules that work across Spring Boot 2.x, 3.x, and 4.x\n- **New Bean Registration DSL**: `stoveSpring4xRegistrar` for Spring Boot 4.x (since `BeanDefinitionDsl` is deprecated)\n- **Runtime Version Checks**: Clear error messages when Spring Boot/Kafka is missing from classpath\n- **Ktor DI Flexibility**: Support for Koin, Ktor-DI, or custom resolvers\n- **Generic Type Resolution**: `using<List<T>>` now works correctly with full generic type preservation\n\n---\n\n## New Features\n\n### Test Reporting System\n\nStove now includes a built-in reporting system that captures everything that happens during your tests. When a test fails, you get a detailed report showing exactly what happened, making debugging much easier.\n\n**Key capabilities:**\n\n- **Automatic tracking** of all system interactions (HTTP, Kafka, database, WireMock, gRPC)\n- **Test failure enrichment** with detailed execution reports embedded in test output\n- **System snapshots** showing internal state (Kafka messages, WireMock stubs) at the time of failure\n- **Multiple renderers** - human-readable console output or machine-readable JSON\n- **Framework integration** with both Kotest and JUnit\n- **Stack trace preservation** - original stack traces are preserved in test failures\n\n#### Quick Start - Kotest\n\nAdd the extension dependency (optional but recommended):\n\n```kotlin hl_lines=\"3\"\ndependencies {\n    testImplementation(\"com.trendyol:stove-extensions-kotest\")\n}\n```\n\nThen configure:\n\n```kotlin hl_lines=\"4 8 11\"\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\n\nclass TestConfig : AbstractProjectConfig() {\n    override val extensions: List<Extension> = listOf(StoveKotestExtension())\n    \n    override suspend fun beforeProject() {\n        Stove()\n            .with { /* your configuration */ }\n            .run()\n    }\n    \n    override suspend fun afterProject() {\n        Stove.stop()\n    }\n}\n```\n\n#### Quick Start - JUnit\n\nAdd the extension dependency (optional but recommended):\n\n```kotlin hl_lines=\"3\"\ndependencies {\n    testImplementation(\"com.trendyol:stove-extensions-junit\")\n}\n```\n\nThen configure:\n\n```kotlin hl_lines=\"4 8\"\nimport com.trendyol.stove.extensions.junit.StoveJUnitExtension\nimport org.junit.jupiter.api.extension.ExtendWith\n\n@ExtendWith(StoveJUnitExtension::class)\nclass MyE2ETest {\n    // your tests\n}\n```\n\nThe JUnit extension works with both JUnit 5 and 6 since they share the Jupiter API.\n\n#### Configuration\n\n```kotlin hl_lines=\"3 5\"\nStove {\n    reporting {\n        enabled()           // Enable reporting (default: true)\n        dumpOnFailure()     // Dump report when tests fail (default: true)\n        failureRenderer(PrettyConsoleRenderer)  // Set the renderer\n    }\n}\n```\n\n#### Example Output\n\nWhen a test fails, you'll see output like:\n\n```\n╔══════════════════════════════════════════════════════════════════════════════╗\n║                           STOVE TEST EXECUTION REPORT                        ║\n║ Test: should save the product                                                ║\n║ Status: FAILED                                                               ║\n╠══════════════════════════════════════════════════════════════════════════════╣\n║ 14:47:38.215 ✓ PASSED [HTTP] POST /api/products                              ║\n║     Input: {\"id\":1234,\"name\":\"Test Product\"}                                 ║\n║                                                                              ║\n║ 14:47:38.341 ✗ FAILED [Kafka] shouldBePublished<ProductCreatedEvent>         ║\n║     Expected: Message matching condition within 5s                           ║\n║     Actual: No matching message found                                        ║\n║     Error: GOT A TIMEOUT: While expecting the publish of 'ProductCreatedEvent'║\n╠══════════════════════════════════════════════════════════════════════════════╣\n║ SYSTEM SNAPSHOTS                                                             ║\n║ ┌─ KAFKA ────────────────────────────────────────────────────────────────────║\n║   Consumed: 0                                                                ║\n║   Produced: 1                                                                ║\n║   State Details:                                                             ║\n║     produced: 1 item(s)                                                      ║\n║       [0] topic: product-events, value: {\"id\":1234,\"name\":\"Test Product\"}    ║\n╚══════════════════════════════════════════════════════════════════════════════╝\n```\n\n#### Available Renderers\n\n- **PrettyConsoleRenderer** (default) - Colorized, box-drawing output for terminals\n- **JsonReportRenderer** - Machine-readable JSON for CI/CD integration\n\nSee the [Reporting documentation](../Components/13-reporting.md) for full details.\n\n---\n\n### Spring Boot 4.x Support\n\nStove now fully supports Spring Boot 4.x and Spring Kafka 4.x. The existing `stove-spring` and `stove-spring-kafka` modules work with all Spring Boot versions (2.x, 3.x, and 4.x).\n\n**Dependencies remain the same:**\n\n```kotlin\ntestImplementation(\"com.trendyol:stove-spring:0.20.0\")\ntestImplementation(\"com.trendyol:stove-spring-kafka:0.20.0\")\n```\n\n---\n\n### New Bean Registration DSL for Spring Boot 4.x\n\nSpring Boot 4.x deprecates `BeanDefinitionDsl` (`beans { }` DSL). Stove provides new extension functions for cleaner bean registration:\n\n**Spring Boot 2.x / 3.x - use `addTestDependencies`:**\n\n```kotlin\nimport com.trendyol.stove.addTestDependencies\n\nspringBoot(\n    runner = { params ->\n        runApplication<MyApp>(args = params) {\n            addTestDependencies {\n                bean<TestSystemKafkaInterceptor<*, *>>()\n                bean<MyService> { MyServiceImpl() }\n            }\n        }\n    }\n)\n```\n\n**Spring Boot 4.x - use `addTestDependencies4x`:**\n\n```kotlin\nimport com.trendyol.stove.addTestDependencies4x\n\nspringBoot(\n    runner = { params ->\n        runApplication<MyApp>(args = params) {\n            addTestDependencies4x {\n                registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)\n                registerBean<MyService> { MyServiceImpl() }\n            }\n        }\n    }\n)\n```\n\n**Alternative: Using `addInitializers` directly:**\n\n```kotlin\n// Spring Boot 2.x / 3.x\naddInitializers(stoveSpringRegistrar { bean<MyService>() })\n\n// Spring Boot 4.x\naddInitializers(stoveSpring4xRegistrar { registerBean<MyService>() })\n```\n\n**Key differences for 4.x:**\n- Use `registerBean<T>()` instead of `bean<T>()`\n- Use `registerBean<T>(primary = true)` for primary beans\n- No `ref()` function - use constructor injection instead\n\n---\n\n### Ktor DI Flexibility\n\nStove's Ktor module now supports multiple dependency injection systems. Previously, Koin was required. Now you can use:\n\n1. **Koin** (existing support)\n2. **Ktor-DI** (new built-in support)\n3. **Custom resolver** (any DI framework)\n\nBoth Koin and Ktor-DI are now `compileOnly` dependencies - you bring your preferred DI system.\n\n**Using Koin:**\n\n```kotlin\ndependencies {\n    testImplementation(\"io.insert-koin:koin-ktor:$koinVersion\")\n}\n\n// In your test setup\nbridge() // Auto-detects Koin\n```\n\n**Using Ktor-DI:**\n\n```kotlin\ndependencies {\n    testImplementation(\"io.ktor:ktor-server-di:$ktorVersion\")\n}\n\n// In your test setup\nbridge() // Auto-detects Ktor-DI\n```\n\n**Using a Custom Resolver:**\n\nFor any other DI framework (Kodein, Dagger, manual, etc.):\n\n```kotlin\nbridge { application, type ->\n    // Your custom resolution logic - type is KType preserving generics\n    myDiContainer.resolve(type)\n}\n```\n\n---\n\n### Generic Type Resolution in Bridge System\n\nThe `using<T>` function now properly preserves generic type information, allowing you to resolve types like `List<PaymentService>`:\n\n```kotlin\n// Register multiple implementations\nprovide<List<PaymentService>> {\n    listOf(StripePaymentService(), PayPalPaymentService())\n}\n\n// Resolve with full generic type preserved\nstove {\n    using<List<PaymentService>> {\n        forEach { service -> service.pay(order) }\n    }\n}\n```\n\nThis works by using `KType` instead of `KClass` internally, which preserves generic type parameters that would otherwise be lost due to JVM type erasure.\n\n**For custom BridgeSystem implementations:** Override `getByType(type: KType)` to support generic types. The default implementation falls back to `get(klass: KClass)`.\n\n---\n\n### Ktor Test Dependency Registration\n\nUnlike Spring Boot's unified `addTestDependencies`, Ktor test dependency registration differs by DI framework:\n\n**Koin:**\n\n```kotlin\n// In your app - accept test modules\nfun run(args: Array<String>, testModules: List<Module> = emptyList()): Application {\n    return embeddedServer(Netty, port = args.getPort()) {\n        install(Koin) { modules(appModule, *testModules.toTypedArray()) }\n    }.start(wait = false).application\n}\n\n// In tests - pass test modules with overrides\nktor(runner = { params ->\n    MyApp.run(params, testModules = listOf(\n        module {\n            single<TimeProvider>(override = true) { FixedTimeProvider() }\n        }\n    ))\n})\n```\n\n**Ktor-DI:**\n\n```kotlin\n// In your app - accept test dependencies\nfun run(args: Array<String>, testDeps: (DependencyRegistrar.() -> Unit)? = null): Application {\n    return embeddedServer(Netty, port = args.getPort()) {\n        install(DI) {\n            dependencies {\n                provide<MyService> { MyServiceImpl() }\n                testDeps?.invoke(this)  // Later provides override earlier ones\n            }\n        }\n    }.start(wait = false).application\n}\n\n// In tests - pass test overrides\nktor(runner = { params ->\n    MyApp.run(params) {\n        provide<TimeProvider> { FixedTimeProvider() }\n    }\n})\n```\n\n---\n\n### Runtime Version Checks\n\nWhen Spring Boot, Spring Kafka, or Ktor DI is missing from the classpath, Stove now provides clear error messages:\n\n```\n═══════════════════════════════════════════════════════════════════════════════\n  Spring Boot Not Found on Classpath!\n═══════════════════════════════════════════════════════════════════════════════\n\n  stove-spring requires Spring Boot to be on your classpath.\n  Spring Boot is declared as a 'compileOnly' dependency, so you must add it\n  to your project.\n\n  Add one of the following to your build.gradle.kts:\n\n  For Spring Boot 2.x:\n    testImplementation(\"org.springframework.boot:spring-boot-starter:2.7.x\")\n\n  For Spring Boot 3.x:\n    testImplementation(\"org.springframework.boot:spring-boot-starter:3.x.x\")\n\n  For Spring Boot 4.x:\n    testImplementation(\"org.springframework.boot:spring-boot-starter:4.x.x\")\n\n═══════════════════════════════════════════════════════════════════════════════\n```\n\n---\n\n## Migration Guide\n\n### From 0.19.x to 0.20.0\n\nThis is a **breaking change release**. Follow these steps to migrate:\n\n#### 1. Update Module Names (Required)\n\nSee the [Breaking Changes - Module and Package Renaming](#module-and-package-renaming) section above for detailed migration steps and regex patterns.\n\n**Quick Summary:**\n- Update all artifact names in build files\n- Replace `stove-testing-e2e` → `stove`\n- Replace `stove-*-testing-e2e` → `stove-*`\n- Update all package imports from `com.trendyol.stove.testing.e2e.*` → `com.trendyol.stove.*`\n\n#### 2. Test Framework Extensions (Optional)\n\nTest framework extensions are now in separate modules. They're optional but recommended for better failure reporting. Add the one that matches your test framework:\n\n```kotlin\ndependencies {\n    // For Kotest\n    testImplementation(\"com.trendyol:stove-extensions-kotest\")\n    \n    // OR for JUnit 5/6\n    testImplementation(\"com.trendyol:stove-extensions-junit\")\n}\n```\n\nUpdate your imports:\n\n```kotlin\n// Kotest\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\n\n// JUnit\nimport com.trendyol.stove.extensions.junit.StoveJUnitExtension\n```\n\n#### 3. Use the New BOM (Recommended)\n\nThe new BOM simplifies version management:\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:0.20.0\"))\n    \n    // No versions needed - managed by BOM\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-spring\")\n    testImplementation(\"com.trendyol:stove-kafka\")\n}\n```\n\n### From 0.19.x (Other Changes)\n\n#### Test Extensions for Better Reporting\n\nThe reporting extensions are optional but make debugging much easier. Add the one for your test framework:\n\n**Kotest:**\n```kotlin\n// Add dependency: testImplementation(\"com.trendyol:stove-extensions-kotest\")\nclass TestConfig : AbstractProjectConfig() {\n    override val extensions = listOf(StoveKotestExtension())\n}\n```\n\n**JUnit:**\n```kotlin\n@ExtendWith(StoveJUnitExtension::class)\nclass MyE2ETest { }\n```\n\n#### For Spring Boot 2.x and 3.x Users\n\n**If using `BaseApplicationContextInitializer`:** Migrate to `addTestDependencies` (see Breaking Changes below).\n\n**If using `beans { }` directly:** Your existing code continues to work. Optionally, use the new cleaner API:\n\n```kotlin\n// Old way (still works)\naddInitializers(beans { bean<MyService>() })\n\n// New way (recommended)\naddTestDependencies { bean<MyService>() }\n```\n\n#### For Spring Boot 4.x Users (New!)\n\nSpring Boot 4.x is newly supported in this release. Use `addTestDependencies4x`:\n\n```kotlin\nimport com.trendyol.stove.addTestDependencies4x\n\nspringBoot(\n    runner = { params ->\n        runApplication<MyApp>(args = params) {\n            addTestDependencies4x {\n                registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)\n                registerBean<MyService> { MyServiceImpl() }\n            }\n        }\n    }\n)\n```\n\nNote: The `beans { }` DSL from Spring is deprecated in 4.x, which is why Stove provides `addTestDependencies4x` with `registerBean<T>()`.\n\n---\n\n## Breaking Changes\n\n### Module and Package Renaming\n\n**⚠️ BREAKING:** All Stove modules have been renamed to simplify artifact names and package structure. This is a breaking change that requires updates to your build files and source code.\n\n#### Module Name Changes\n\n| Old Artifact Name | New Artifact Name |\n|------------------|-------------------|\n| `stove-testing-e2e` | `stove` |\n| `stove-testing-e2e-kafka` | `stove-kafka` |\n| `stove-testing-e2e-http` | `stove-http` |\n| `stove-testing-e2e-couchbase` | `stove-couchbase` |\n| `stove-testing-e2e-elasticsearch` | `stove-elasticsearch` |\n| `stove-testing-e2e-grpc` | `stove-grpc` |\n| `stove-testing-e2e-mongodb` | `stove-mongodb` |\n| `stove-testing-e2e-redis` | `stove-redis` |\n| `stove-testing-e2e-wiremock` | `stove-wiremock` |\n| `stove-testing-e2e-rdbms-postgres` | `stove-postgres` |\n| `stove-testing-e2e-rdbms-mssql` | `stove-mssql` |\n| `stove-spring-testing-e2e` | `stove-spring` |\n| `stove-spring-testing-e2e-kafka` | `stove-spring-kafka` |\n| `stove-ktor-testing-e2e` | `stove-ktor` |\n| `stove-micronaut-testing-e2e` | `stove-micronaut` |\n\n#### Package Name Changes\n\nAll packages have been simplified:\n- `com.trendyol.stove.testing.e2e.*` → `com.trendyol.stove.*`\n- `com.trendyol.stove.testing.e2e.rdbms.postgres` → `com.trendyol.stove.postgres`\n- `com.trendyol.stove.testing.e2e.rdbms.mssql` → `com.trendyol.stove.mssql`\n- `com.trendyol.stove.testing.e2e.standalone.kafka` → `com.trendyol.stove.kafka`\n\n#### Migration Guide\n\n**Step 1: Update Build Files (Gradle/Maven)**\n\n**Recommended: Use the new BOM for version management:**\n\n```kotlin\n// build.gradle.kts\ndependencies {\n    // Import BOM\n    testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n    \n    // Core and framework (no version needed - managed by BOM)\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-spring\")  // or stove-ktor, stove-micronaut\n    \n    // Components (no version needed)\n    testImplementation(\"com.trendyol:stove-kafka\")\n    testImplementation(\"com.trendyol:stove-postgres\")\n    // ... other components\n}\n```\n\n**Or without BOM (specify versions explicitly):**\n\n```kotlin\ndependencies {\n    testImplementation(\"com.trendyol:stove:$version\")\n    testImplementation(\"com.trendyol:stove-spring:$version\")\n    testImplementation(\"com.trendyol:stove-kafka:$version\")\n    testImplementation(\"com.trendyol:stove-postgres:$version\")\n}\n```\n\n**Step 2: Update Package Imports in Source Code**\n\nAll import statements need to be updated. Use the regex patterns below for automated migration.\n\n**Step 3: Automated Migration with Regex**\n\nUse these regex patterns in your IDE's find-and-replace (with regex enabled):\n\n**For Build Files (Gradle/Maven):**\n\n1. **Replace artifact names in dependencies:**\n   ```regex\n   Find:    com\\.trendyol:stove-testing-e2e(?!-)\n   Replace: com.trendyol:stove\n   ```\n\n2. **Replace component artifacts:**\n   ```regex\n   Find:    com\\.trendyol:stove-testing-e2e-([a-z-]+)\n   Replace: com.trendyol:stove-$1\n   ```\n\n3. **Replace RDBMS artifacts:**\n   ```regex\n   Find:    com\\.trendyol:stove-testing-e2e-rdbms-(postgres|mssql)\n   Replace: com.trendyol:stove-$1\n   ```\n\n4. **Replace starter artifacts:**\n   ```regex\n   Find:    com\\.trendyol:stove-(spring|ktor|micronaut)-testing-e2e(-kafka)?\n   Replace: com.trendyol:stove-$1$2\n   ```\n\n**For Source Code (Kotlin/Java):**\n\n1. **Replace package imports:**\n   ```regex\n   Find:    import com\\.trendyol\\.stove\\.testing\\.e2e\\.(.*)\n   Replace: import com.trendyol.stove.$1\n   ```\n\n2. **Replace fully qualified names:**\n   ```regex\n   Find:    com\\.trendyol\\.stove\\.testing\\.e2e\\.rdbms\\.(postgres|mssql)\n   Replace: com.trendyol.stove.$1\n   ```\n\n3. **Replace standalone.kafka:**\n   ```regex\n   Find:    com\\.trendyol\\.stove\\.testing\\.e2e\\.standalone\\.kafka\n   Replace: com.trendyol.stove.kafka\n   ```\n\n4. **Replace remaining testing.e2e references:**\n   ```regex\n   Find:    com\\.trendyol\\.stove\\.testing\\.e2e\\.([a-z.]+)\n   Replace: com.trendyol.stove.$1\n   ```\n\n**Step 4: Manual Verification**\n\nAfter automated replacement, verify:\n\n1. **Build files compile** - Run `./gradlew build` or `mvn compile`\n2. **Imports resolve** - Check that all imports are valid\n3. **Tests compile** - Run `./gradlew compileTestKotlin` or `mvn test-compile`\n4. **Tests pass** - Run your test suite\n\n**Example Migration**\n\n**Before (0.19.0):**\n```kotlin\n// build.gradle.kts\ndependencies {\n    testImplementation(\"com.trendyol:stove-testing-e2e:0.19.0\")\n    testImplementation(\"com.trendyol:stove-spring-testing-e2e:0.19.0\")\n    testImplementation(\"com.trendyol:stove-testing-e2e-kafka:0.19.0\")\n    testImplementation(\"com.trendyol:stove-testing-e2e-rdbms-postgres:0.19.0\")\n}\n\n// Test code\nimport com.trendyol.stove.testing.e2e.system.TestSystem\nimport com.trendyol.stove.testing.e2e.kafka.kafka\nimport com.trendyol.stove.testing.e2e.rdbms.postgres.postgresql\n```\n\n**After (0.20.0):**\n```kotlin\n// build.gradle.kts\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:0.20.0\"))\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-spring\")\n    testImplementation(\"com.trendyol:stove-kafka\")\n    testImplementation(\"com.trendyol:stove-postgres\")\n}\n\n// Test code\nimport com.trendyol.stove.system.TestSystem\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.postgres.postgresql\n```\n\n**Migration Checklist**\n\n- [ ] Update all `build.gradle.kts` / `build.gradle` / `pom.xml` files\n- [ ] Replace all import statements in test source code\n- [ ] Replace all fully qualified package references\n- [ ] Update any documentation or scripts referencing old artifact names\n- [ ] Verify build compiles successfully\n- [ ] Run test suite to ensure everything works\n\n**Need Help?**\n\nIf you encounter issues during migration:\n1. Check the [Migration Guide](#migration-guide) section below\n2. Review the [Getting Started](../getting-started.md) guide for updated examples\n3. Open an issue on [GitHub](https://github.com/Trendyol/stove/issues)\n\n---\n\n### `BaseApplicationContextInitializer` Removed\n\n`BaseApplicationContextInitializer` has been removed. Migrate to `addTestDependencies`:\n\n**Before (0.19.0):**\n\n```kotlin\nclass TestSystemInitializer : BaseApplicationContextInitializer({\n    bean<TestSystemKafkaInterceptor<*, *>>()\n    bean<MyService> { MyServiceImpl() }\n})\n\n// Usage\nrunApplication<MyApp>(args = params) {\n    addInitializers(TestSystemInitializer())\n}\n```\n\n**After (0.20.0):**\n\n```kotlin\nimport com.trendyol.stove.addTestDependencies\n\nrunApplication<MyApp>(args = params) {\n    addTestDependencies {\n        bean<TestSystemKafkaInterceptor<*, *>>()\n        bean<MyService> { MyServiceImpl() }\n    }\n}\n```\n\nThis is simpler - no need to create a separate class.\n\n---\n\n### HttpSystem: `getResponse` renamed to `getResponseBodiless`\n\nThe `getResponse` method in `HttpSystem` has been renamed to `getResponseBodiless` to better reflect its purpose - it returns a response without parsing the body.\n\n**Before:**\n```kotlin\nhttp {\n    getResponse(\"/api/endpoint\") { response ->\n        response.status shouldBe 200\n    }\n}\n```\n\n**After:**\n```kotlin\nhttp {\n    getResponseBodiless(\"/api/endpoint\") { response ->\n        response.status shouldBe 200\n    }\n}\n```\n\n---\n\n## Notes\n\n### BridgeSystem API Enhancement\n\nThe `BridgeSystem` abstract class now has a new method `getByType(type: KType)` which is used by `resolve()` to support generic types. If you have a custom `BridgeSystem` implementation:\n\n- **No action required** if you only use simple types - the default implementation falls back to `get(klass: KClass)`\n- **Override `getByType(type: KType)`** if you want to support generic types like `List<T>`, `Map<K,V>`, etc.\n\n```kotlin\n// Example for custom bridge\noverride fun <D : Any> getByType(type: KType): D {\n    // Use type.classifier for KClass\n    // Use type.arguments for generic parameters\n    return myDiFramework.resolve(type)\n}\n```\n\n### Dead Letter Topic Naming Convention (Spring Kafka)\n\nBe aware that Spring Kafka changed the default DLT (Dead Letter Topic) naming convention between versions:\n\n| Spring Kafka Version | DLT Suffix | Example |\n|---------------------|------------|---------|\n| 2.x | `.DLT` | `my-topic.DLT` |\n| 3.x, 4.x | `-dlt` | `my-topic-dlt` |\n\nThis is not a Stove change, but something to be aware of when writing Kafka tests across different Spring Kafka versions.\n\n### Optional: Disable Reporting\n\nIf you don't want the new reporting feature (not recommended), you can disable it:\n\n```kotlin\nTestSystem {\n    reportingEnabled(false)\n}\n```\n\n---\n\n## Dependency Updates\n\n- Spring Boot 4.x support (4.0.0+)\n- Spring Kafka 4.x support (4.0.0+)\n- Continued support for Spring Boot 2.7.x and 3.x\n- Continued support for Spring Kafka 2.9.x and 3.x\n\n---\n\n## Full Changelog\n\nSee the [GitHub Releases](https://github.com/Trendyol/stove/releases) page for the complete list of commits and contributors.\n\n---\n\n## Contributors\n\nThanks to all contributors who made this release possible!\n\n---\n\n## Getting Started\n\n```kotlin\ndependencies {\n    testImplementation(\"com.trendyol:stove:0.20.0\")\n    testImplementation(\"com.trendyol:stove-spring:0.20.0\")\n    // Add component-specific dependencies as needed\n    testImplementation(\"com.trendyol:stove-spring-kafka:0.20.0\")\n}\n```\n\nFor snapshot versions, add the snapshot repository:\n\n```kotlin\nrepositories {\n    maven(\"https://central.sonatype.com/repository/maven-snapshots\")\n}\n```\n"
  },
  {
    "path": "docs/release-notes/0.21.0.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">0.21.0</span>\n\n**Released:** February 2026\n\nThis release introduces:\n\n<ul data-rn-group>\n<li><strong>Tracing</strong>: See the <span data-rn=\"highlight\" data-rn-color=\"#00968855\">full execution trace</span> of your application when a test fails: every controller, database query, Kafka message, and HTTP call with timing and failure points</li>\n<li><strong>gRPC Mocking</strong>: <span data-rn=\"underline\" data-rn-color=\"#009688\">Mock external gRPC services in your tests with a type-safe DSL</span></li>\n<li><strong>MySQL Support</strong>: <span data-rn=\"underline\" data-rn-color=\"#ff9800\">New <code>stove-mysql</code> module</span> for testing against MySQL databases</li>\n<li><strong>WireMock Test Scoping</strong>: WireMock snapshots are now scoped to the current test for cleaner failure reports</li>\n<li><strong>Dynamic Port Allocation</strong>: WireMock now defaults to <code>port = 0</code> and gRPC Mock supports it, no more port conflicts in CI</li>\n<li><strong>Migration Type Aliases</strong>: <code>PostgresqlMigration</code>, <code>MongodbMigration</code>, etc. instead of verbose <code>DatabaseMigration&lt;XyzContext&gt;</code></li>\n<li><strong>Elasticsearch 9 Support</strong>: Adapted to work with Elasticsearch 9.x</li>\n<li><strong>Spring Showcase Recipe</strong>: A comprehensive example project demonstrating all Stove features together</li>\n</ul>\n\n---\n\n## New Features\n\n### Tracing\n\nWhen a test fails, you no longer have to guess what happened inside your application. Stove captures the <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">entire call chain</span>: every controller method, database query, Kafka message, and HTTP call, and displays it as a trace tree in the failure report:\n\n```\n═══════════════════════════════════════════════════════════════════════════════\nEXECUTION TRACE (Call Chain)\n═══════════════════════════════════════════════════════════════════════════════\n✓ POST (377ms)\n  ✓ POST /api/product/create (361ms)\n    ✓ ProductController.create (141ms)\n      ✓ ProductCreator.create (0ms)\n      ✓ KafkaProducer.send (137ms)\n        ✓ orders.created publish (81ms)\n          ✗ orders.created process (82ms)  ← FAILURE POINT\n```\n\nSetup takes two steps:\n\n1. Enable in your Stove config:\n\n```kotlin hl_lines=\"3-4\"\nStove()\n    .with {\n        tracing {\n            enableSpanReceiver()\n        }\n    }\n```\n\n2. Attach the OpenTelemetry agent in your build. Copy [`StoveTracingConfiguration.kt`](https://github.com/Trendyol/stove/blob/main/buildSrc/src/main/kotlin/com/trendyol/stove/gradle/StoveTracingConfiguration.kt) to your project's `buildSrc/src/main/kotlin/` directory, then add to your `build.gradle.kts`:\n\n```kotlin\nimport com.trendyol.stove.gradle.stoveTracing\n\nstoveTracing {\n    serviceName = \"my-service\"\n}\n```\n\n!!! tip \"Gradle Plugin available since 0.21.2\"\n    Starting with [0.21.2](0.21.2.md), a standalone Gradle plugin is available that eliminates the need to copy this file. See the [0.21.2 release notes](0.21.2.md) for details.\n\n<span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">Everything else is automatic.</span> Trace headers are injected into HTTP, Kafka, and gRPC calls, spans are collected and correlated, and failure reports are enriched with the trace tree.\n\nA validation DSL is also available for asserting on the execution flow:\n\n```kotlin hl_lines=\"2 3 4\"\ntracing {\n    shouldContainSpan(\"OrderService.processOrder\")\n    shouldNotHaveFailedSpans()\n    executionTimeShouldBeLessThan(500.milliseconds)\n}\n```\n\nSee the [Tracing documentation](../Components/15-tracing.md) for full details.\n\n---\n\n### gRPC Mocking\n\nNew `stove-grpc-mock` module for mocking external gRPC services in your tests. This lets you test gRPC client code without running the actual upstream services.\n\n```kotlin\ndependencies {\n    testImplementation(\"com.trendyol:stove-grpc-mock:$stoveVersion\")\n}\n```\n\nConfigure in your Stove setup:\n\n```kotlin hl_lines=\"2\"\ngrpcMock {\n    GrpcMockSystemOptions(port = 0) // Dynamic port allocation\n}\n```\n\nStub responses with a type-safe DSL:\n\n```kotlin hl_lines=\"2-3\"\ngrpcMock {\n    mockUnary(\n        FraudDetectionServiceGrpc.getCheckFraudMethod(),\n        response = FraudCheckResponse.newBuilder()\n            .setIsFraud(false)\n            .setScore(0.1)\n            .build()\n    )\n}\n```\n\nSupports unary, server streaming, and conditional matching. See the [gRPC Mocking documentation](../Components/14-grpc-mock.md) for full details.\n\n---\n\n### MySQL Support\n\nNew `stove-mysql` module with the same familiar DSL as PostgreSQL and MSSQL:\n\n```kotlin\ndependencies {\n    testImplementation(\"com.trendyol:stove-mysql:$stoveVersion\")\n}\n```\n\n```kotlin\nStove()\n    .with {\n        mysql {\n            MysqlOptions(\n                databaseName = \"mydb\",\n                configureExposedConfiguration = { cfg ->\n                    listOf(\n                        \"spring.datasource.url=${cfg.jdbcUrl}\",\n                        \"spring.datasource.username=${cfg.username}\",\n                        \"spring.datasource.password=${cfg.password}\"\n                    )\n                }\n            )\n        }\n    }\n```\n\nSupports migrations, `shouldQuery`, `shouldExecute`, and all the database operations you'd expect. See the [MySQL documentation](../Components/16-mysql.md) for details.\n\n---\n\n### WireMock Test Scoping\n\nWireMock snapshots in failure reports are <span data-rn=\"underline\" data-rn-color=\"#009688\">now scoped to the current test</span>. Previously, all registered stubs and requests across the entire test suite would appear in the snapshot. Now you only see stubs and requests relevant to the test that failed, making failure reports much easier to read.\n\n---\n\n### Dynamic Port Allocation\n\nBoth WireMock and gRPC Mock now support dynamic port allocation with `port = 0`, which prevents port conflicts when running tests in parallel on CI.\n\n**WireMock** now defaults to `port = 0`. You no longer need to pick a port:\n\n```kotlin\nwiremock {\n    WireMockSystemOptions(\n        configureExposedConfiguration = { cfg ->\n            listOf(\"external-apis.inventory.url=${cfg.baseUrl}\")\n        }\n    )\n}\n```\n\n**gRPC Mock** also supports `port = 0`:\n\n```kotlin\ngrpcMock {\n    GrpcMockSystemOptions(port = 0)\n}\n```\n\nIn both cases, the actual port is exposed via `configureExposedConfiguration` so your application receives the correct URL.\n\n---\n\n### Spring Showcase Recipe\n\nA new comprehensive recipe at `recipes/jvm/kotlin-recipes/spring-showcase/` demonstrates all Stove features working together in a realistic Spring Boot application:\n\n- HTTP endpoints with PostgreSQL\n- Kafka producers and consumers\n- gRPC server and client with mocked upstream\n- WireMock for external HTTP APIs\n- Tracing\n- Database migrations\n- db-scheduler integration\n\n<span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">This is the best starting point for understanding how Stove fits into a real project.</span>\n\n---\n\n### Migration Type Aliases\n\nEach module that supports migrations now provides a convenient type alias, so you no longer need to remember `DatabaseMigration<PostgresSqlMigrationContext>` and similar verbose signatures:\n\n```kotlin\n// Before\nclass CreateUsersTable : DatabaseMigration<PostgresSqlMigrationContext> { ... }\n\n// After\nclass CreateUsersTable : PostgresqlMigration { ... }\n```\n\nAvailable aliases:\n\n| Module              | Type Alias               |\n|---------------------|--------------------------|\n| stove-postgres      | `PostgresqlMigration`    |\n| stove-mysql         | `MySqlMigration`         |\n| stove-mssql         | `MsSqlMigration`         |\n| stove-mongodb       | `MongodbMigration`       |\n| stove-couchbase     | `CouchbaseMigration`     |\n| stove-elasticsearch | `ElasticsearchMigration` |\n| stove-redis         | `RedisMigration`         |\n| stove-kafka         | `KafkaMigration`         |\n\nThe generic `DatabaseMigration<T>` interface remains fully supported. The aliases are purely additive.\n\n---\n\n## Improvements\n\n### Elasticsearch 9 Support\n\n`stove-elasticsearch` now works with Elasticsearch 9.x (specifically 9.3.0). The module adapts to the updated client API automatically.\n\n### Improved Failure Reports\n\n- Exception details (type, message, stack trace) are now extracted from OpenTelemetry spans and displayed in trace trees\n- Console renderer output is cleaner and better formatted\n- Kafka report entries now include all relevant state\n\n### Tracing Configuration Cache Compatibility\n\nThe Stove Tracing Gradle plugin and the `stoveTracing` buildSrc helper are both fully compatible with Gradle configuration cache, so builds with `--configuration-cache` work correctly.\n\n### Non-ASCII Test ID Support\n\nTrace context test IDs now handle non-ASCII characters (e.g., Japanese, Korean) correctly by normalizing to ASCII with hash suffixes for uniqueness. This ensures consistent behavior when test names use non-Latin scripts.\n\n### BridgeSystem Suspended\n\n`BridgeSystem` methods are now `suspend` functions, allowing proper coroutine support in bridge implementations.\n\n---\n\n## Bug Fixes\n\n- **HTTP streaming**: Fixed a flow collection issue that could cause streaming responses to hang\n- **Kafka tests**: Fixed flaky test behavior in Kafka system tests\n- **Tracing configuration cache**: Fixed serialization issues when using Gradle configuration cache with the Stove Tracing plugin\n- **ASCII character handling**: Fixed edge cases in test ID sanitization for non-ASCII characters\n\n---\n\n## Dependency Updates\n\n- Elasticsearch 9.3.0\n- Kafka (Confluent) 8.1.1\n- Confluent Platform Kafka 8.0.3\n- gRPC Java 1.79.0\n- Protobuf 4.33.5\n- OpenTelemetry 1.59.0\n- Kotlin (latest patch)\n- Ktor 3.4.0\n- Flyway 12.x\n- HikariCP 7.x\n- Various Spring, Quarkus, and Micronaut updates\n\n---\n\n## Migration Guide\n\n### From 0.20.x to 0.21.0\n\nThis is a <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">non-breaking</span> release. All existing APIs remain compatible.\n\n#### New Features to Opt Into\n\n**Tracing**: Add `stove-tracing` to your dependencies and follow the [setup guide](../Components/15-tracing.md).\n\n**gRPC Mocking**: Add `stove-grpc-mock` to your dependencies if you need to mock external gRPC services. See the [gRPC Mocking documentation](../Components/14-grpc-mock.md).\n\n**MySQL**: Add `stove-mysql` if you're testing against MySQL. See the [MySQL documentation](../Components/16-mysql.md).\n\n#### Test Framework Extensions\n\n`StoveKotestExtension` (`stove-extensions-kotest`) and `StoveJUnitExtension` (`stove-extensions-junit`) are separate packages that must be on your classpath. **Kotest** requires **6.1.3** or later; **JUnit** requires **Jupiter 6.x** if possible.\n\nIn Kotest 6.x, `AbstractProjectConfig` is no longer auto-scanned. Add a `kotest.properties` file in your test resources (e.g. `src/test-e2e/resources/kotest.properties`):\n\n```properties\nkotest.framework.config.fqn=com.myapp.e2e.TestConfig\n```\n\nSet the value to the fully qualified name of your `AbstractProjectConfig` class. See the [Getting Started guide](../getting-started.md#step-3-create-test-configuration) for full details.\n\n#### Recommended Updates\n\n- If using Elasticsearch, verify compatibility with Elasticsearch 9.x\n- If using the Stove Tracing plugin or `stoveTracing`, no changes needed. Configuration cache compatibility is automatic\n- Consider switching `GrpcMockSystemOptions` to `port = 0` for CI-friendly dynamic port allocation\n\n---\n\n## Getting Started\n\n```kotlin hl_lines=\"2 4 5 6 7\"\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:0.21.0\"))\n\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-spring\")\n    testImplementation(\"com.trendyol:stove-tracing\")\n    testImplementation(\"com.trendyol:stove-extensions-kotest\")\n    // Add components as needed\n}\n```\n\nFor snapshot versions:\n\n```kotlin hl_lines=\"2\"\nrepositories {\n    maven(\"https://central.sonatype.com/repository/maven-snapshots\")\n}\n```\n"
  },
  {
    "path": "docs/release-notes/0.21.2.md",
    "content": "# <span data-rn=\"underline\" data-rn-color=\"#ff9800\">0.21.2</span>\n\n**Released:** February 2026\n\nThis release introduces the **Stove Tracing Gradle Plugin**, a standalone plugin that replaces the copy-paste buildSrc approach for configuring OpenTelemetry tracing in your tests.\n\n---\n\n## New Features\n\n### Stove Tracing Gradle Plugin\n\nThe tracing build configuration is now available as a proper Gradle plugin, published to **Maven Central**.\n\n```kotlin\nplugins {\n    id(\"com.trendyol.stove.tracing\") version \"0.21.2\"\n}\n\nstoveTracing {\n    serviceName.set(\"my-service\")\n}\n```\n\nThe plugin handles everything: downloading the OpenTelemetry Java Agent, configuring JVM arguments, attaching the agent to your test tasks, and dynamically assigning ports so parallel test runs don't conflict.\n\n**Why a plugin?**\n\n- No need to copy `StoveTracingConfiguration.kt` into your `buildSrc`\n- Version updates come through normal dependency management\n- Consistent `stoveTracing { }` DSL with Gradle's `Property<T>` conventions\n- Published alongside all other Stove artifacts\n\n**Availability:**\n\n| Channel | Coordinates |\n|---------|-------------|\n| Maven Central | `com.trendyol:stove-tracing-gradle-plugin` |\n| Maven Central Snapshots | `com.trendyol:stove-tracing-gradle-plugin` (snapshot versions) |\n\nAdd `mavenCentral()` to your `pluginManagement` repositories. For snapshot versions, also add the snapshot repository:\n\n```kotlin\n// settings.gradle.kts\npluginManagement {\n    repositories {\n        mavenCentral()\n        maven(\"https://central.sonatype.com/repository/maven-snapshots\") // only for snapshots\n        gradlePluginPortal()\n    }\n}\n```\n\nSee the [Tracing documentation](../Components/15-tracing.md#gradle-plugin) for full configuration options.\n\n---\n\n## Breaking Changes\n\n### `configureStoveTracing` renamed to `stoveTracing`\n\nThe buildSrc copy-paste function has been renamed from `configureStoveTracing` to `stoveTracing` for consistency with the plugin DSL. If you are using the buildSrc approach, update your build scripts:\n\n```kotlin\n// Before\nimport com.trendyol.stove.gradle.configureStoveTracing\n\nconfigureStoveTracing {\n    serviceName = \"my-service\"\n}\n\n// After\nimport com.trendyol.stove.gradle.stoveTracing\n\nstoveTracing {\n    serviceName = \"my-service\"\n}\n```\n\nIf you are migrating to the plugin, the DSL name is the same (`stoveTracing { }`), but properties use Gradle's `Property<T>` API:\n\n```kotlin\n// buildSrc style\nstoveTracing {\n    serviceName = \"my-service\"\n    testTaskNames = listOf(\"integrationTest\")\n}\n\n// Plugin style\nstoveTracing {\n    serviceName.set(\"my-service\")\n    testTaskNames.set(listOf(\"integrationTest\"))\n}\n```\n\n---\n\n## Migration Guide\n\n### From 0.21.x to 0.21.2\n\n#### Migrating to the Gradle plugin (recommended)\n\n1. Remove `StoveTracingConfiguration.kt` from your `buildSrc/src/main/kotlin/` if you copied it\n2. Apply the plugin in your `build.gradle.kts`:\n\n```kotlin\nplugins {\n    id(\"com.trendyol.stove.tracing\") version \"0.21.2\"\n}\n\nstoveTracing {\n    serviceName.set(\"my-service\")\n}\n```\n\n#### Staying with buildSrc\n\nIf you prefer to keep the buildSrc approach, rename the function call:\n\n```kotlin\n// configureStoveTracing { ... }  ->  stoveTracing { ... }\n```\n\nNo other changes are required.\n"
  },
  {
    "path": "docs/release-notes/0.22.2.md",
    "content": "# 0.22.0 – 0.22.2\n\n**Released:** March 2026\n\nThis release introduces **Quarkus support** as a new framework starter, improves console reporting, and fixes Ktor DI detection.\n\n---\n\n## New Features\n\n### Quarkus Support (0.22.0)\n\nNew `stove-quarkus` module brings first-class support for testing Quarkus applications with Stove:\n\n```kotlin\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n    testImplementation(\"com.trendyol:stove\")\n    testImplementation(\"com.trendyol:stove-quarkus\")\n}\n```\n\nConfigure in your Stove setup:\n\n```kotlin\nStove()\n  .with {\n    quarkus(\n      runner = { params -> run(params) },\n      withParameters = listOf(\"quarkus.http.port=8080\")\n    )\n  }\n  .run()\n```\n\nQuarkus support includes:\n\n- Real Quarkus application startup from your entrypoint\n- Full composition with Kafka, PostgreSQL, WireMock, HTTP assertions, and tracing\n\nSee the [Quarkus documentation](../frameworks/quarkus.md) and the [quarkus-example](https://github.com/Trendyol/stove/tree/main/examples/quarkus-example) for details.\n\n---\n\n### Improved Console Reporting (0.22.0)\n\nThe console report renderer has been rewritten using the [Mordant](https://github.com/ajalt/mordant) library, producing cleaner, prettier output with proper wrapping and formatting. Failure reports are now easier to read at a glance.\n\n---\n\n## Bug Fixes\n\n### Ktor Auto DI Detection (0.22.2)\n\nFixed a linkage error that could occur when Ktor's auto DI detection tried to load optional DI framework classes that weren't on the classpath. The detection now uses reflection-based availability checks before attempting typed runtime checks, preventing `NoClassDefFoundError` in projects that use only Koin or only Ktor-DI (but not both).\n\nThis also removes the need for Koin-based Ktor projects to carry Ktor-DI as a transitive dependency.\n\n---\n\n## Dependency Updates\n\n- Kotlin 2.3.20\n- Wire 6.0.0\n- Arrow 2.2.2.1\n- Ktor 3.4.1\n- Kotest 6.1.7\n- Koin 4.2.0\n- Jackson 2.21.1\n- MongoDB Driver 5.6.4\n- Elasticsearch 9.3.3\n- Confluent Kafka Streams Serde 8.2.0\n- Protobuf 4.34.0\n- OpenTelemetry 1.60.1\n- Micronaut 4.10.18\n- Quarkus 3.34.0\n- gRPC Java 1.80.0\n- Spring Boot 3.5.11 / 4.0.3\n- Spring Kafka 3.3.14 / 4.0.4\n- Various other dependency updates\n\n---\n\n## Migration Guide\n\n### From 0.21.2 to 0.22.x\n\nThis is a non-breaking release. All existing APIs remain compatible.\n\n#### New Features to Opt Into\n\n**Quarkus**: Add `stove-quarkus` to your dependencies if you're testing a Quarkus application. See the [Quarkus documentation](../frameworks/quarkus.md).\n\nNo other changes are required.\n"
  },
  {
    "path": "docs/release-notes/0.23.0.md",
    "content": "# 0.23.0\n\n**Released:** March 2026\n\n## New Features\n\n### Cassandra Support\n\nNew `stove-cassandra` module for testing applications that use Apache Cassandra:\n\n```kotlin\ndependencies {\n    testImplementation(\"com.trendyol:stove-cassandra\")\n}\n```\n\nConfigure in your Stove setup:\n\n```kotlin\nStove()\n  .with {\n    cassandra {\n      CassandraSystemOptions(\n        keyspace = \"my_keyspace\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\",\n            \"spring.cassandra.local-datacenter=${cfg.datacenter}\",\n            \"spring.cassandra.keyspace-name=${cfg.keyspace}\"\n          )\n        }\n      ).migrations {\n        register<CreateKeyspaceMigration>()\n      }\n    }\n  }.run()\n```\n\nIncludes:\n\n- CQL statement execution (`shouldExecute`) and query assertions (`shouldQuery`)\n- Prepared/bound statement support\n- Raw `CqlSession` access via `session()`\n- Keyspace-aware session management — sessions are created without a keyspace first, then rebound after migrations create it\n- Migrations via `CassandraMigration` type alias\n- Container pause/unpause for fault injection\n- Provided instance support via `CassandraSystemOptions.provided()`\n- Cleanup hooks\n\nSee the [Cassandra documentation](../Components/17-cassandra.md) for full details.\n\n### Dashboard — Local Observability Dashboard\n\nNew `stove-dashboard` module and a companion CLI (`stove`) that gives you a <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">real-time web dashboard</span> for your e2e test runs.\n\nAdd the dependency, apply the tracing Gradle plugin, and register in your Stove config:\n\n```kotlin\n// build.gradle.kts\nplugins {\n    id(\"com.trendyol.stove.tracing\") version \"<stove-version>\"\n}\n\ndependencies {\n    testImplementation(platform(\"com.trendyol:stove-bom:$version\"))\n    testImplementation(\"com.trendyol:stove-dashboard\")\n    testImplementation(\"com.trendyol:stove-tracing\")\n}\n\nstoveTracing {\n    serviceName.set(\"product-api\")\n}\n```\n\n```kotlin hl_lines=\"3\"\nStove()\n  .with {\n    dashboard { DashboardSystemOptions(appName = \"product-api\") }\n    tracing { enableSpanReceiver() }\n    // ... other systems\n  }.run()\n```\n\nStart the CLI, run your tests, and open [http://localhost:4040](http://localhost:4040):\n\n- **Timeline** — every HTTP call, Kafka message, database query with inputs, outputs, expected vs. actual\n- **Trace** — distributed trace tree from OpenTelemetry spans, with exception details and stack traces\n- **Snapshots** — system state cards captured at test boundaries\n- **Kafka Explorer** — dedicated view with consumed/published/failed counts and expandable JSON payloads\n\nThe dashboard emitter is fault-tolerant: non-blocking queue, auto-disables after 5 consecutive gRPC failures, never breaks your tests. If the CLI isn't running, nothing happens.\n\nInstall the CLI:\n\n```bash\nbrew install Trendyol/trendyol-tap/stove\n```\n\nSee the [Dashboard documentation](../Components/18-dashboard.md) for full details.\n\n---\n\n## Documentation\n\n- Comprehensive documentation improvements across all pages\n- Added Cassandra to provided instances, components index, and README\n- Fixed inaccurate Docker requirement claims — docs now clarify that Docker is only needed in container mode, not when using provided instances\n- Fixed incorrect `TestSystemInterceptor` references → `TestSystemKafkaInterceptor<*, *>` in Kafka docs\n- Expanded Ktor framework docs with DI auto-detection table and tabbed examples\n- Added Spring Boot 4.x `addTestDependencies4x` section\n- Added missing MySQL section to provided instances docs\n- Added 0.21.2 → 0.22.x migration notes to troubleshooting\n- Added Dashboard documentation with architecture, installation, configuration, and REST API reference\n\n---\n\n## Dependency Updates\n\n- Testcontainers 2.0.4\n- Apache Cassandra Java Driver 4.19.2 (new)\n\n---\n\n## Migration Guide\n\n### From 0.22.x to 0.23.0\n\nThis is a non-breaking release. All existing APIs remain compatible.\n\n#### New Features to Opt Into\n\n**Cassandra**: Add `stove-cassandra` to your dependencies if you're testing against Cassandra. See the [Cassandra documentation](../Components/17-cassandra.md).\n\n**Dashboard**: Add `stove-dashboard` and `stove-tracing` to your dependencies, apply the `com.trendyol.stove.tracing` Gradle plugin, and install the CLI via Homebrew (`brew install Trendyol/trendyol-tap/stove`) or the shell script. See the [Dashboard documentation](../Components/18-dashboard.md).\n"
  },
  {
    "path": "docs/release-notes/0.24.0.md",
    "content": "# 0.24.0\n\n**Released:** May 2026\n\nThis release makes Stove a polyglot end-to-end testing framework and rounds out the black-box story. Go applications are now first-class citizens — runnable as host processes or Docker containers, with Kafka, OpenTelemetry, and code coverage support out of the box. `stove-container` works for any image, regardless of language. `providedApplication()` unlocks smoke-testing already-deployed apps. Multiple instances of the same system type can now be registered with typed keys for cross-service verification. And the `stove` CLI gains an MCP server so AI agents can triage failed runs through structured tools instead of raw logs.\n\n---\n\n## New Features\n\n### `providedApplication()` — black-box smoke testing against deployed apps\n\nStove no longer requires it to start the application. `providedApplication()` replaces the framework starter and points Stove at a **remote, already-deployed** app — staging, pre-prod, or any environment where the artifact is running independently.\n\n```kotlin\nStove().with {\n    httpClient {\n        HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\")\n    }\n\n    postgresql {\n        PostgresqlOptions.provided(\n            jdbcUrl = \"jdbc:postgresql://staging-db:5432/myapp\",\n            host = \"staging-db\", port = 5432,\n            configureExposedConfiguration = { emptyList() }\n        )\n    }\n\n    kafka {\n        KafkaSystemOptions.provided(\n            bootstrapServers = \"staging-kafka:9092\",\n            configureExposedConfiguration = { emptyList() }\n        )\n    }\n\n    providedApplication {\n        ProvidedApplicationOptions(\n            readiness = ReadinessStrategy.HttpGet(\n                url = \"https://staging.myapp.com/health\"\n            )\n        )\n    }\n}.run()\n```\n\nIncludes:\n\n- Optional readiness check (`ReadinessStrategy.HttpGet` / `TcpPort` / `Probe` / `FixedDelay`) — Stove waits for the deployed app before running tests\n- Works with **any language or framework** — JVM or otherwise\n- `*.provided(...)` factory methods on systems (PostgreSQL, MySQL, MSSQL, Cassandra, MongoDB, Redis, Elasticsearch, Couchbase, Kafka, …) point Stove at an existing instance instead of a Testcontainer\n- `cleanup` lambdas on system options for managing test data on shared external infrastructure\n- No `Bridge` / `using<T> {}` (no local DI container) — by design\n\nUse case: same Stove tests that drive your local Testcontainers-backed e2e suite can run as post-deployment smoke checks against staging — no new framework, no new DSL.\n\nSee [Provided Application](../Components/19-provided-application.md).\n\n### Multiple instances of the same system (keyed systems)\n\nStove now supports registering **multiple instances of the same system type**, each identified by a typed key (`SystemKey`). Pass the key to both registration and validation DSLs.\n\n```kotlin\nobject OrderService : SystemKey\nobject PaymentService : SystemKey\nobject AppDb : SystemKey\nobject AnalyticsDb : SystemKey\n\nStove().with {\n    httpClient { HttpClientSystemOptions(baseUrl = \"https://myapp.com\") }                       // default\n    httpClient(OrderService) { HttpClientSystemOptions(baseUrl = \"https://order.internal\") }     // keyed\n    httpClient(PaymentService) { HttpClientSystemOptions(baseUrl = \"https://pay.internal\") }     // keyed\n\n    postgresql(AppDb) { /* ... */ }\n    postgresql(AnalyticsDb) { /* ... */ }\n\n    providedApplication()\n}.run()\n```\n\n```kotlin\ntest(\"create order, verify across services and databases\") {\n    stove {\n        http { /* default — your app */ }\n        http(OrderService) { /* downstream order service */ }\n        http(PaymentService) { /* downstream payment service */ }\n        postgresql(AppDb) { /* app's database */ }\n        postgresql(AnalyticsDb) { /* analytics database */ }\n    }\n}\n```\n\nSupported across PostgreSQL, MySQL, MSSQL, MongoDB, Cassandra, Couchbase, Redis, Elasticsearch, HTTP, gRPC, Kafka, WireMock, gRPC Mock. Single-instance systems (Bridge, Tracing, Dashboard) and framework starters do not support keys — there is only one application under test.\n\nDefault and keyed instances of the same type coexist independently. Keyed systems get distinguishable names in dashboard reports and traces (`HTTP [OrderService] > GET /api/orders/123`).\n\nPairs naturally with `providedApplication()` for full black-box microservice integration testing across many services and shared databases.\n\nSee [Multiple Systems](../Components/20-multiple-systems.md).\n\n### `stove-process` — non-JVM applications as host processes\n\nNew `stove-process` module starts any binary as the application under test. Works for any language; ships with a `goApp()` convenience for Go.\n\n```kotlin\ndependencies {\n    testImplementation(\"com.trendyol:stove-process\")\n}\n```\n\n```kotlin\nStove().with {\n    httpClient { HttpClientSystemOptions(baseUrl = \"http://localhost:8090\") }\n    postgresql { PostgresqlOptions(...) }\n\n    goApp(\n        target = ProcessTarget.Server(port = 8090, portEnvVar = \"APP_PORT\"),\n        envProvider = envMapper {\n            \"database.host\" to \"DB_HOST\"\n            \"database.port\" to \"DB_PORT\"\n            env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:4317\")\n        }\n    )\n}.run()\n```\n\nFor other languages, use `processApp { ProcessApplicationOptions(...) }` with an explicit command.\n\nIncludes:\n\n- `ProcessTarget.Server` / `ProcessTarget.Worker` for HTTP servers vs. background workers\n- `ReadinessStrategy.HttpGet` / `TcpPort` / `Probe` / `FixedDelay`\n- `envMapper` and `argsMapper` DSLs to map Stove configs to env vars or CLI flags\n- Graceful shutdown via SIGTERM with configurable timeout\n- Background stdout/stderr forwarding\n\nSee [Go Process Mode](../other-languages/go-process.md) and [Other Languages & Stacks](../other-languages/index.md).\n\n### `stove-container` — applications as Docker images\n\nNew `stove-container` module runs the AUT as a Docker image. Language-agnostic — Go, Python, Node.js, Rust, .NET, JVM, anything that ships in a container. Stove only needs an image tag; **building the image is your pipeline's job**, not Stove's. Use whatever your CI already produces, pull from a registry, or wire an optional local Gradle build task — all three work.\n\n```kotlin\ndependencies {\n    testImplementation(\"com.trendyol:stove-container\")\n}\n```\n\n```kotlin\nStove().with {\n    httpClient { HttpClientSystemOptions(baseUrl = \"http://localhost:8090\") }\n    postgresql { PostgresqlOptions(...) }\n\n    containerApp(\n        image = System.getProperty(\"app.container.image\"),  // tag from CI / registry / local build\n        target = ContainerTarget.Server(\n            hostPort = 8090,\n            internalPort = 8090,\n            portEnvVar = \"APP_PORT\",\n            bindHostPort = false\n        ),\n        envProvider = envMapper {\n            \"database.host\" to \"DB_HOST\"\n            \"database.port\" to \"DB_PORT\"\n        },\n        configureContainer = {\n            withNetworkMode(\"host\")\n        }\n    )\n}.run()\n```\n\nIncludes:\n\n- Same `envMapper` / `argsMapper` model as `stove-process`\n- `ContainerTarget.Server` (port-binding or host-network) and `ContainerTarget.Worker`\n- `configureContainer { ... }` block for Testcontainers `GenericContainer` access (volume mounts, network mode, capabilities, etc.)\n- `beforeStarted { ... }` hook for pre-start setup with resolved configuration\n- Graceful container stop with fallback force-close\n- Pluggable image registry (`registry`, `compatibleSubstitute`)\n\nIn CI, point at the image tag your build pipeline already produced (`-Papp.image=...` or env var). For local iteration, optionally wire a Gradle `Exec` task that runs `docker build`. See [Go Container Mode](../other-languages/go-container.md) for the full walkthrough.\n\n### Go is a first-class citizen\n\nThe Go showcase recipe ([`recipes/process/golang/go-showcase`](https://github.com/Trendyol/stove/tree/main/recipes/process/golang/go-showcase)) demonstrates an HTTP + PostgreSQL + Kafka service with full tracing, dashboard, and coverage. The same StoveConfig switches between process and container mode via `-Dgo.aut.mode=process|container`.\n\nHighlights:\n\n- HTTP and database queries traced via `otelhttp` + `otelsql`\n- W3C `traceparent` propagation correlates Go spans with the originating Stove test\n- `go build -cover` integration coverage with `GOCOVERDIR` and SIGPIPE-safe shutdown\n- Dashboard streams the Go run alongside JVM runs\n\n### `stove-kafka` — Go Kafka bridge library\n\nNew Go library at [`go/stove-kafka`](https://github.com/Trendyol/stove/tree/main/go/stove-kafka) enables `shouldBePublished` and `shouldBeConsumed` assertions for Go applications. The bridge forwards produced/consumed/committed messages over gRPC to Stove's observer.\n\n```bash\ngo get github.com/trendyol/stove/go/stove-kafka\n```\n\n```go\nimport stovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n\nbridge, _ := stovekafka.NewBridgeFromEnv()  // nil in production — zero overhead\ndefer bridge.Close()\n```\n\nThree first-party client integrations:\n\n| Library | Subpackage | Integration |\n|---------|-----------|-------------|\n| [IBM/sarama](https://github.com/IBM/sarama) | `stove-kafka/sarama` | `ProducerInterceptor` / `ConsumerInterceptor` |\n| [twmb/franz-go](https://github.com/twmb/franz-go) | `stove-kafka/franz` | `kgo.WithHooks(&franz.Hook{...})` |\n| [segmentio/kafka-go](https://github.com/segmentio/kafka-go) | `stove-kafka/segmentio` | `ReportWritten()` / `ReportRead()` |\n\nOther clients (e.g. confluent-kafka-go) can use the library-agnostic core: `bridge.ReportPublished()`, `bridge.ReportConsumed()`, `bridge.ReportCommitted()`. All `Bridge` methods are nil-safe — the bridge disappears in production where `STOVE_KAFKA_BRIDGE_PORT` is unset.\n\n### MCP server in `stove` CLI\n\nThe CLI now exposes a local **Model Context Protocol** endpoint so AI agents can triage failed tests through compact, structured tools instead of loading entire logs into context.\n\n```text\n$ stove\nStove CLI v0.24.0 running\nUI:   http://localhost:4040\nREST: http://localhost:4040/api/v1\nMCP:  http://localhost:4040/mcp\ngRPC: localhost:4041\n```\n\nGeneric MCP client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"stove\": {\n      \"transport\": \"streamable-http\",\n      \"url\": \"http://localhost:4040/mcp\"\n    }\n  }\n}\n```\n\nTools (all read-only, local-only):\n\n| Tool | Purpose |\n|------|---------|\n| `stove_apps` | Apps recorded in the dashboard database |\n| `stove_runs` | Runs, filterable by app and status |\n| `stove_failures` | Default entrypoint — failed tests grouped by app and run |\n| `stove_failure_detail` | Compact failure packet for one exact test |\n| `stove_timeline` | Ordered test actions, failure-focused |\n| `stove_trace` | Critical path and exception evidence from correlated spans |\n| `stove_snapshot` | System snapshot summaries with targeted JSON drill-down |\n| `stove_raw_evidence` | Capped raw lookup for one entry, span, or snapshot |\n\nDefaults are token-aware: payloads are truncated deterministically, sensitive keys (`authorization`, `cookie`, `password`, `secret`, `token`, `apiKey`, `credential`) are redacted before return. Use `budget: tiny|compact|full` to dial detail.\n\nThe endpoint accepts loopback only and rejects non-local `Host`/`Origin` headers. See [MCP](../Components/21-mcp.md).\n\n### Go integration test coverage\n\nStove black-box tests can now collect Go integration coverage. Build with `go build -cover`, set `GOCOVERDIR`, and Go writes coverage data on graceful shutdown — fits the `stove-process` and `stove-container` lifecycle.\n\n```bash\n./gradlew e2eTestWithCoverage -Pgo.coverage=true\n./gradlew e2eTest-containerWithCoverage -Pgo.coverage=true\n```\n\nNotes:\n\n- No framework changes — uses existing `envMapper` and Gradle tasks\n- `signal.Ignore(syscall.SIGPIPE)` in `main()` is required so log writes to a closed `ProcessBuilder` stdout pipe do not kill the process before counters flush\n- For container mode, bind-mount a host coverage directory into the container\n\nSee [Go Process Mode — Coverage](../other-languages/go-process.md#code-coverage).\n\n---\n\n## Improvements\n\n- `ReadinessStrategy.HttpGet` replaces the older `HealthCheckOptions` API consistently across the codebase (process, container, provided application). Old callers should migrate to the new strategy.\n- `ProcessApplicationUnderTest` exposes background stdout reading and a configurable graceful shutdown timeout\n- `ContainerApplicationUnderTest` streams container logs through SLF4J with the image as prefix\n\n---\n\n## Documentation\n\n- New \"Other Languages & Stacks\" section split into mode-focused pages: [Process Mode](../other-languages/go-process.md) and [Container Mode](../other-languages/go-container.md)\n- New [Container AUT component page](../Components/22-container.md) — language-agnostic `stove-container` reference\n- New [MCP component page](../Components/21-mcp.md) covering discovery, integration, agent workflow, tools, token budgeting, and security\n- New [Provided Application](../Components/19-provided-application.md) page for black-box / smoke testing\n- New [Multiple Systems](../Components/20-multiple-systems.md) page for keyed system registration\n- Coverage walkthrough (Gradle wiring, `GOCOVERDIR`, SIGPIPE) in the Go pages\n- Refreshed [other-languages/index.md](../other-languages/index.md) with process vs. container guidance\n\n---\n\n## Migration Guide\n\n### From 0.23.0 to 0.24.0\n\nThis is a non-breaking release for JVM users. New modules are opt-in.\n\n#### Adopt black-box / smoke testing\n\nIf you want to run your existing Stove tests against a deployed app (staging, pre-prod), swap your framework starter for `providedApplication()` and use the `*.provided(...)` factory on each external system. No new dependency required — `providedApplication()` ships with `com.trendyol:stove`.\n\n#### Register multiple instances of one system type\n\nDefine `SystemKey` singletons and pass them as the first argument to system DSLs (`postgresql(AppDb) { ... }`, `httpClient(OrderService) { ... }`). Default and keyed instances coexist.\n\n#### Adopt non-JVM testing\n\nFor Go (or any language) applications, add either or both:\n\n```kotlin\ntestImplementation(\"com.trendyol:stove-process\")    // host binary\ntestImplementation(\"com.trendyol:stove-container\")  // Docker image\n```\n\nFor Go Kafka assertions, add the bridge to your Go module:\n\n```bash\ngo get github.com/trendyol/stove/go/stove-kafka\n```\n\n#### Adopt MCP\n\nUpgrade the `stove` CLI to 0.24.0:\n\n```bash\nbrew upgrade stove\n```\n\nPoint your agent runtime at `http://localhost:4040/mcp`. No test code changes required — MCP reads from the same dashboard database that already records your runs.\n\n#### `HealthCheckOptions` → `ReadinessStrategy`\n\nIf you copied internal helpers from earlier snapshots, replace `HealthCheckOptions` with `ReadinessStrategy.HttpGet(url, ...)`. Public APIs already used `ReadinessStrategy`, so most users are unaffected.\n"
  },
  {
    "path": "docs/troubleshooting.md",
    "content": "# Troubleshooting & FAQ\n\nHaving issues? This guide covers the most common problems and how to fix them. If you don't find what you're looking for, feel free to open an issue on GitHub.\n\n## Common Issues\n\n### Docker Issues\n\n!!! tip \"Docker Not Available?\"\n    If Docker is not available in your environment (e.g., some CI/CD pipelines), consider using [provided instances](Components/11-provided-instances.md) to connect to existing infrastructure instead of spinning up containers.\n\n#### Docker Not Found / Not Running\n\n**Symptoms:**\n```\nCould not find a valid Docker environment\n```\n\n**Solutions:**\n\n1. **Verify Docker is installed and running:**\n   ```bash\n   docker --version\n   docker ps\n   ```\n\n2. **Check Docker daemon status:**\n   ```bash\n   # macOS/Linux\n   systemctl status docker\n   \n   # or\n   docker info\n   ```\n\n3. **Restart Docker Desktop** (if using Docker Desktop)\n\n4. **Check Docker socket permissions:**\n   ```bash\n   # Linux\n   sudo chmod 666 /var/run/docker.sock\n   ```\n\n#### Docker Image Pull Failures\n\n**Symptoms:**\n```\nError pulling image: denied: access denied\n```\n\n**Solutions:**\n\n1. **Use a custom registry:**\n   ```kotlin\n   DEFAULT_REGISTRY = \"your-registry.com\"\n   ```\n\n2. **Login to registry:**\n   ```bash\n   docker login your-registry.com\n   ```\n\n3. **Configure per-component registry:**\n   ```kotlin\n   kafka {\n       KafkaSystemOptions(\n           container = KafkaContainerOptions(\n               registry = \"your-registry.com\"\n           )\n       ) { /* config */ }\n   }\n   ```\n\n#### Port Already in Use\n\n**Symptoms:**\n```\nBind for 0.0.0.0:8080 failed: port is already allocated\n```\n\n**Solutions:**\n\n1. **Find and kill the process using the port:**\n   ```bash\n   # macOS/Linux\n   lsof -i :8080\n   kill -9 <PID>\n   \n   # Windows\n   netstat -ano | findstr :8080\n   taskkill /PID <PID> /F\n   ```\n\n2. **Use a different port:**\n   ```kotlin\n   springBoot(\n       runner = { params -> myApp.run(params) },\n       withParameters = listOf(\"server.port=8081\")\n   )\n   ```\n\n3. **Use dynamic ports:** <span data-rn=\"highlight\" data-rn-color=\"#4caf5044\" data-rn-duration=\"800\">Let the framework assign available ports when possible.</span>\n\n### Startup Issues\n\n#### Application Fails to Start\n\n**Symptoms:**\n```\nApplication failed to start\n```\n\n**Solutions:**\n\n1. **Check application logs:**\n   ```kotlin\n   springBoot(\n       withParameters = listOf(\n           \"logging.level.root=debug\",\n           \"logging.level.org.springframework=debug\"\n       )\n   )\n   ```\n\n2. **Verify configuration is being passed correctly:**\n   ```kotlin\n   kafka {\n       KafkaSystemOptions { cfg ->\n           println(\"Kafka config: ${cfg.bootstrapServers}\")  // Debug print\n           listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n       }\n   }\n   ```\n\n3. **Ensure your application accepts CLI arguments:**\n   ```kotlin\n   // Application should parse args\n   fun run(args: Array<String>) {\n       // args should include Stove's configuration\n   }\n   ```\n\n#### Container Startup Timeout\n\n**Symptoms:**\n```\nContainer startup timed out\n```\n\n**Solutions:**\n\n1. **Increase container startup timeout:**\n   ```kotlin\n   couchbase {\n       CouchbaseSystemOptions(\n           container = CouchbaseContainerOptions(\n               containerFn = { container ->\n                   container.withStartupTimeout(Duration.ofMinutes(5))\n               }\n           )\n       ) { /* config */ }\n   }\n   ```\n\n2. **Check container resource requirements:**\n   - Elasticsearch needs at least 2GB RAM\n   - Couchbase needs significant memory\n   - Reduce memory limits in resource-constrained environments\n\n3. **Check Docker resources:**\n   - Increase Docker Desktop memory allocation\n   - Ensure sufficient disk space\n\n### Test Failures\n\n#### Assertion Timeout\n\n**Symptoms:**\n```\nTimed out waiting for condition\n```\n\n**Solutions:**\n\n1. **Increase assertion timeout:**\n   ```kotlin\n   kafka {\n       shouldBePublished<Event>(atLeastIn = 30.seconds) {\n           actual.id == expectedId\n       }\n   }\n   ```\n\n2. **Check if the operation actually completes:**\n   - Add logging to verify the operation is triggered\n   - Check application logs for errors\n\n3. **Verify async processing is working:**\n   ```kotlin\n   // Debug by checking intermediate state\n   using<EventProcessor> {\n       println(\"Pending events: ${pendingCount()}\")\n   }\n   ```\n\n#### <span data-rn=\"highlight\" data-rn-color=\"#ff980055\" data-rn-duration=\"800\">Serialization/Deserialization Errors</span>\n\n**Symptoms:**\n```\nJsonParseException: Unrecognized field\nMismatchedInputException: Cannot deserialize\n```\n\n**Solutions:**\n\n1. **Align ObjectMapper configuration:**\n   ```kotlin\n   val objectMapper = ObjectMapper().apply {\n       registerModule(KotlinModule.Builder().build())\n       registerModule(JavaTimeModule())\n       disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n   }\n   \n   Stove()\n       .with {\n           http {\n               HttpClientSystemOptions(\n                   contentConverter = JacksonConverter(objectMapper)\n               )\n           }\n           kafka {\n               KafkaSystemOptions(\n                   serde = StoveSerde.jackson.anyByteArraySerde(objectMapper)\n               ) { /* config */ }\n           }\n       }\n   ```\n\n2. **Check field name mapping:**\n   ```kotlin\n   data class MyEvent(\n       @JsonProperty(\"eventId\")  // Match exact field name\n       val id: String\n   )\n   ```\n\n3. **Verify data class has default constructor for Jackson:**\n   ```kotlin\n   // Add default values or use @JsonCreator\n   data class MyEvent(\n       val id: String = \"\",\n       val name: String = \"\"\n   )\n   ```\n\n#### Data Not Found\n\n**Symptoms:**\n```\nResource with key (xxx) is not found\nDocument not found\n```\n\n**Solutions:**\n\n1. **Verify data was actually saved:**\n   ```kotlin\n   // Save\n   couchbase {\n       save(collection = \"orders\", id = orderId, instance = order)\n       \n       // Immediately verify\n       shouldGet<Order>(\"orders\", orderId) { o ->\n           println(\"Saved order: $o\")\n       }\n   }\n   ```\n\n2. **Check timing - wait for async operations:**\n   ```kotlin\n   // If save is async, wait for it\n   delay(1.seconds)\n   \n   couchbase {\n       shouldGet<Order>(\"orders\", orderId) { /* verify */ }\n   }\n   ```\n\n3. **Verify collection/index names match:**\n   ```kotlin\n   // Ensure collection names are consistent\n   save(collection = \"orders\", ...)  // Note: \"orders\" not \"order\"\n   shouldGet<Order>(\"orders\", ...)   // Must match!\n   ```\n\n#### Kafka Message Not Found\n\n**Symptoms:**\n```\nMessage was not published within timeout\nMessage was not consumed within timeout\n```\n\n**Solutions:**\n\n1. **Verify Kafka interceptor is configured:**\n   ```kotlin hl_lines=\"3\"\n   // In your Stove setup\n   addTestDependencies {\n       bean<TestSystemKafkaInterceptor<*, *>>(isPrimary = true)\n   }\n   ```\n\n2. **Check topic names:**\n   ```kotlin\n   kafka {\n       shouldBePublished<Event>(atLeastIn = 10.seconds) {\n           println(\"Checking topic: ${metadata.topic}\")  // Debug\n           actual.id == expectedId\n       }\n   }\n   ```\n\n3. **Verify interceptor class is passed to application:**\n   ```kotlin hl_lines=\"6\"\n   kafka {\n       KafkaSystemOptions { cfg ->\n           listOf(\n               \"kafka.bootstrapServers=${cfg.bootstrapServers}\",\n               \"kafka.interceptorClasses=${cfg.interceptorClass}\"  // Important!\n           )\n       }\n   }\n   ```\n\n4. **Check consumer group offset configuration:**\n   ```kotlin\n   springBoot(\n       withParameters = listOf(\n           \"kafka.offset=earliest\",  // Start from beginning\n           \"kafka.autoCreateTopics=true\"\n       )\n   )\n   ```\n\n#### WireMock Stubs Not Being Hit\n\n**Symptoms:**\n```\nConnection refused to external service\nTest timeout when calling mocked endpoint\nMock not found / unexpected request\n```\n\n**Cause:** This is <span data-rn=\"highlight\" data-rn-color=\"#ff980055\" data-rn-duration=\"800\">almost always because your application's external service URLs don't match the WireMock URL</span>.\n\n**Solutions:**\n\n1. <span data-rn=\"box\" data-rn-color=\"#ef5350\">**Ensure ALL external service URLs point to WireMock:**</span>\n   ```kotlin hl_lines=\"11-13\"\n   Stove()\n       .with {\n           wiremock {\n               WireMockSystemOptions(port = 9090)\n           }\n           springBoot(\n               runner = { params -> myApp.run(params) },\n               withParameters = listOf(\n                   // ALL external services must use WireMock URL\n                   \"payment.service.url=http://localhost:9090\",\n                   \"inventory.service.url=http://localhost:9090\",\n                   \"notification.service.url=http://localhost:9090\"\n               )\n           )\n       }\n   ```\n\n2. **Verify your application is reading the URLs from configuration:**\n   ```kotlin\n   // Your application should read URLs from config, not hardcode them\n   @Value(\"\\${payment.service.url}\")\n   private lateinit var paymentServiceUrl: String\n   ```\n\n3. **Check the port matches:**\n   ```kotlin\n   // WireMock port\n   WireMockSystemOptions(port = 9090)\n   \n   // Application parameter must match\n   \"payment.service.url=http://localhost:9090\"  // Same port!\n   ```\n\n4. **Debug by checking WireMock requests:**\n   ```kotlin\n   wiremock {\n       // After test, check what requests WireMock received\n       WireMock.getAllServeEvents().forEach { event ->\n           println(\"Request: ${event.request.url}\")\n       }\n   }\n   ```\n\n### Memory Issues\n\n#### OutOfMemoryError\n\n**Symptoms:** <span data-rn=\"underline\" data-rn-color=\"#ff9800\">OutOfMemoryError</span> (e.g. `Java heap space`)\n\n**Solutions:**\n\n1. **Increase JVM heap for tests:**\n   ```kotlin\n   // build.gradle.kts\n   tasks.test {\n       jvmArgs(\"-Xmx2g\", \"-Xms512m\")\n   }\n   ```\n\n2. **Limit container memory:**\n   ```kotlin\n   elasticsearch {\n       ElasticsearchSystemOptions(\n           container = ElasticContainerOptions(\n               containerFn = { container ->\n                   container.withEnv(\"ES_JAVA_OPTS\", \"-Xms512m -Xmx512m\")\n               }\n           )\n       ) { /* config */ }\n   }\n   ```\n\n3. **Use provided instances instead of containers** for CI environments.\n\n### CI/CD Issues\n\n#### Docker-in-Docker Not Working\n\n**Solutions:**\n\n1. **Use DinD sidecar in CI:**\n   ```yaml\n   # GitLab CI example\n   services:\n     - docker:dind\n   variables:\n     DOCKER_HOST: tcp://docker:2375\n   ```\n\n2. **Use provided instances:**\n   ```kotlin\n   Stove()\n       .with {\n           kafka {\n               KafkaSystemOptions.provided(\n                   bootstrapServers = System.getenv(\"KAFKA_SERVERS\"),\n                   configureExposedConfiguration = { cfg ->\n                       listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n                   }\n               )\n           }\n       }\n   ```\n\n#### Slow CI Builds\n\n**Solutions:**\n\n1. **Use provided instances** for external infrastructure\n2. **Enable container reuse:**\n   ```kotlin\n   Stove {\n       keepDependenciesRunning()  // In development only\n   }\n   ```\n3. **Run tests in parallel** (ensure proper isolation)\n4. **Use smaller container images** when available\n\n#### Intermittent Failures with Shared Infrastructure\n\n**Symptoms:**\n```\nTests pass locally but fail randomly in CI\nData from another test run appears in assertions\n\"Topic already exists\" or \"Index already exists\" errors\nTests fail when multiple builds run in parallel\n```\n\n**Cause:** <span data-rn=\"highlight\" data-rn-color=\"#ff980055\" data-rn-duration=\"800\">Multiple test runs are using the same resource names</span> (databases, topics, indices) in shared infrastructure.\n\n**Solutions:**\n\n1. **Use unique resource prefixes per test run:**\n   ```kotlin hl_lines=\"2-3 5-7\"\n   object TestRunContext {\n       val runId: String = System.getenv(\"CI_JOB_ID\") \n           ?: UUID.randomUUID().toString().take(8)\n       \n       val databaseName = \"testdb_$runId\"\n       val topicPrefix = \"test_${runId}_\"\n       val indexPrefix = \"test_${runId}_\"\n   }\n   ```\n\n2. **Apply prefixes to all resources:**\n   ```kotlin hl_lines=\"3-5\"\n   springBoot(\n       withParameters = listOf(\n           \"spring.datasource.url=jdbc:postgresql://db:5432/${TestRunContext.databaseName}\",\n           \"kafka.topic.orders=${TestRunContext.topicPrefix}orders\",\n           \"elasticsearch.index.products=${TestRunContext.indexPrefix}products\"\n       )\n   )\n   ```\n\n3. **Clean up only your resources:**\n   ```kotlin\n   cleanup = { admin ->\n       val ourTopics = admin.listTopics().names().get()\n           .filter { it.startsWith(TestRunContext.topicPrefix) }\n       if (ourTopics.isNotEmpty()) {\n           admin.deleteTopics(ourTopics).all().get()\n       }\n   }\n   ```\n\n4. **Log the run ID for debugging:**\n   ```kotlin\n   init {\n       println(\"Test Run ID: ${TestRunContext.runId}\")\n   }\n   ```\n\n!!! tip \"Detailed Guide\"\n    See [Provided Instances - Test Isolation](Components/11-provided-instances.md#test-isolation-with-shared-infrastructure) for comprehensive examples.\n\n## FAQ\n\n### General Questions\n\n#### Q: Can I use Stove with Java?\n\n**A:** Yes, you can use Stove in Java projects! However, <span data-rn=\"underline\" data-rn-color=\"#ff9800\">the e2e tests themselves need to be written in Kotlin</span>. Stove's DSL is designed specifically for Kotlin, providing a clean and expressive syntax:\n\n```kotlin\nclass MyE2ETest : FunSpec({\n    test(\"should create order\") {\n        stove {\n            http {\n                postAndExpectBodilessResponse(\n                    uri = \"/orders\",\n                    body = Some(CreateOrderRequest()),\n                    expect = { status shouldBe 201 }\n                )\n            }\n        }\n    }\n})\n```\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">You can still test your Java application with Stove — just write your e2e test files in Kotlin.</span>\n\n#### Q: Can I use JUnit instead of Kotest?\n\n**A:** Yes, Stove works with both JUnit and Kotest. See the [Getting Started](getting-started.md) guide for JUnit examples.\n\n#### Q: How do I debug tests?\n\n**A:** \n\n1. Set breakpoints in your application code\n2. Run tests in debug mode\n3. Use verbose logging:\n   ```kotlin\n   withParameters = listOf(\"logging.level.root=debug\")\n   ```\n4. Access application beans:\n   ```kotlin\n   using<MyService> {\n       println(\"Service state: $this\")\n   }\n   ```\n\n#### Q: Can I run tests in parallel?\n\n**A:** Yes, but ensure proper test isolation:\n\n- Use unique test data (UUIDs)\n- Don't share state between tests\n- Be careful with shared resources\n\n#### Q: How do I test with SSL/TLS?\n\n**A:** Configure the component with security enabled:\n\n```kotlin\nelasticsearch {\n    ElasticsearchSystemOptions(\n        container = ElasticContainerOptions(\n            disableSecurity = false\n        ),\n        configureExposedConfiguration = { cfg ->\n            // Certificate info available in cfg.certificate\n            listOf(...)\n        }\n    )\n}\n```\n\n### Component-Specific Questions\n\n#### Q: Why isn't my Kafka message being intercepted?\n\n**A:** Ensure:\n\n1. `TestSystemKafkaInterceptor` is registered as a bean\n2. `kafka.interceptorClasses` is configured correctly\n3. Your Kafka listener container uses the interceptor\n\n```kotlin\n// Application configuration\n@Bean\nfun containerFactory(\n    interceptor: ConsumerAwareRecordInterceptor<String, String>\n): ConcurrentKafkaListenerContainerFactory<String, String> {\n    return ConcurrentKafkaListenerContainerFactory<String, String>().apply {\n        setRecordInterceptor(interceptor)\n    }\n}\n```\n\n#### Q: How do I test multiple databases?\n\n**A:** Add multiple database components:\n\n```kotlin\nStove()\n    .with {\n        postgresql { PostgresqlOptions(...) }\n        mongodb { MongodbSystemOptions(...) }\n        couchbase { CouchbaseSystemOptions(...) }\n    }\n```\n\n#### Q: Can I use custom container images?\n\n**A:** Yes:\n\n```kotlin\nkafka {\n    KafkaSystemOptions(\n        container = KafkaContainerOptions(\n            registry = \"my-registry.com\",\n            image = \"custom/kafka\",\n            tag = \"3.5.0\"\n        )\n    ) { /* config */ }\n}\n```\n\n#### Q: How do I handle database migrations?\n\n**A:** Use the migrations API:\n\n```kotlin\npostgresql {\n    PostgresqlOptions(...).migrations {\n        register<CreateUserTableMigration>()\n        register<CreateOrderTableMigration>()\n    }\n}\n```\n\n#### Q: Can I access the underlying testcontainer?\n\n**A:** For container operations like pause/unpause:\n\n```kotlin\ncouchbase {\n    pause()    // Pause container\n    unpause()  // Resume container\n}\n```\n\nFor the client:\n```kotlin\nelasticsearch {\n    val client = client()  // Get Elasticsearch client\n    // Use client directly\n}\n```\n\n### Performance Questions\n\n#### Q: How can I speed up test execution?\n\n**A:**\n\n1. **Keep containers running:**\n   ```kotlin\n   Stove { keepDependenciesRunning() }\n   ```\n\n2. **Use provided instances in CI:**\n   ```kotlin\n   kafka { KafkaSystemOptions.provided(bootstrapServers = \"...\", configureExposedConfiguration = { ... }) }\n   ```\n\n3. **Reduce container resource allocation:**\n   ```kotlin\n   withEnv(\"ES_JAVA_OPTS\", \"-Xms256m -Xmx256m\")\n   ```\n\n4. **Run independent tests in parallel**\n\n#### Q: Why is container startup slow?\n\n**A:** Container startup depends on:\n\n- Image pull time (first run)\n- Container initialization time\n- Health check completion\n\nSolutions:\n- Pre-pull images in CI\n- <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">Use `keepDependenciesRunning()` locally</span>\n- Increase startup timeout for slow containers\n\n#### Q: Why can't my AI agent connect to Stove MCP?\n\n**A:** Stove MCP is served by `stove-cli`, not by your test JVM. Start `stove` first and use the endpoint printed in the startup banner, usually `http://localhost:4040/mcp`.\n\nYou can also check `http://localhost:4040/api/v1/meta`; it should include `\"mcp\": { \"enabled\": true, ... }`.\n\nIf MCP still cannot be reached, use the normal failure report, test output, and logs. MCP is a token-saving path for agents, not a required dependency.\n\n### Migration Questions\n\n#### Q: How do I migrate from 0.14.x to 0.15.x?\n\n**A:** See [Migration Notes](release-notes/0.15.0.md) for detailed instructions. Key changes:\n\n- `StoveSerde` replaces direct `ObjectMapper` usage\n- Configure serde for each component that needs it\n\n#### Q: How do I migrate from 0.21.x to 0.21.2?\n\n**A:** See [Migration Notes](release-notes/0.21.2.md) for detailed instructions. Key changes:\n\n- `configureStoveTracing` renamed to `stoveTracing` in buildSrc\n- New Stove Tracing Gradle Plugin available as an alternative to the buildSrc approach\n- If using the plugin, properties use Gradle's `Property<T>` API (e.g., `serviceName.set(\"...\")` instead of `serviceName = \"...\"`)\n\n#### Q: How do I migrate from 0.21.2 to 0.22.x?\n\n**A:** See [Migration Notes](release-notes/0.22.2.md) for detailed instructions. Key changes:\n\n- New `stove-quarkus` module available for Quarkus applications\n- Console reporting rewritten with Mordant for better output\n- No breaking changes — all existing APIs remain compatible\n\n\n## Getting Help\n\nIf you can't find a solution:\n\n1. **Search existing issues:** [GitHub Issues](https://github.com/Trendyol/stove/issues)\n2. **Check examples:** [Examples Directory](https://github.com/Trendyol/stove/tree/main/examples)\n3. **Open a new issue:** Include:\n   - Stove version\n   - JDK version\n   - Docker version\n   - Complete error message\n   - Minimal reproduction code\n\n## Debug Checklist\n\nWhen troubleshooting, check these items:\n\n- [ ] Docker is running and accessible (not needed if using [provided instances](Components/11-provided-instances.md))\n- [ ] Correct Stove version in dependencies\n- [ ] Application main function is properly modified\n- [ ] Configuration is passed to application\n- [ ] Serializers match between Stove and application\n- [ ] Container has enough resources\n- [ ] Ports are not conflicting\n- [ ] Network is accessible (for provided instances)\n- [ ] Timeouts are appropriate for your environment\n"
  },
  {
    "path": "docs/writing-custom-systems.md",
    "content": "# Writing Custom Systems\n\nStove's built-in systems cover databases, Kafka, HTTP, gRPC, and more, but your application is unique. Maybe you use a job scheduler, publish domain events, need to control time in tests, or talk to a service over a custom protocol. Custom systems let you bring **anything** into the Stove DSL so your tests read like this:\n\n```kotlin hl_lines=\"7-10\"\ntest(\"should send welcome email after user signs up\") {\n    stove {\n        http {\n            post<UserResponse>(\"/users\", createUserRequest) { it.status shouldBe 201 }\n        }\n\n        tasks {\n            shouldBeExecuted<SendEmailPayload>(atLeastIn = 10.seconds) {\n                recipientEmail == \"new-user@example.com\"\n            }\n        }\n    }\n}\n```\n\nThat `tasks { }` block is a custom system. Building one is straightforward.\n\n## The Pattern\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">Every custom system has three pieces:</span>\n\n### 1. The System Class\n\nImplement <span data-rn=\"underline\" data-rn-color=\"#ff9800\">PluggedSystem</span> and pick a lifecycle interface that fits your needs:\n\n```kotlin hl_lines=\"1-2 11-16\"\nclass DbSchedulerSystem(\n    override val stove: Stove\n) : PluggedSystem, AfterRunAwareWithContext<ApplicationContext> {\n\n    private lateinit var listener: StoveDbSchedulerListener\n\n    override suspend fun afterRun(context: ApplicationContext) {\n        listener = context.getBean(StoveDbSchedulerListener::class.java)\n    }\n\n    suspend inline fun <reified T : Any> shouldBeExecuted(\n        atLeastIn: Duration = 5.seconds,\n        noinline condition: T.() -> Boolean\n    ): DbSchedulerSystem {\n        listener.waitUntilObserved(atLeastIn, T::class, condition)\n        return this\n    }\n\n    override fun close() {}\n}\n```\n\n<span data-rn=\"underline\" data-rn-color=\"#009688\">The lifecycle interfaces control when your system runs: before the app starts, after it starts, or when configuration is collected.</span>\n\n| Interface | When Called |\n|-----------|------------|\n| `RunAware` | Before application starts |\n| `AfterRunAware` | After application starts |\n| `AfterRunAwareWithContext<T>` | After application starts, with DI context (e.g., Spring `ApplicationContext`) |\n| `ExposesConfiguration` | When collecting configuration to pass to the application |\n\n### 2. DSL Extensions\n\nTwo extension functions wire your system into Stove's DSL:\n\n```kotlin hl_lines=\"1-3 5-8\"\n@StoveDsl\nfun WithDsl.dbScheduler(): Stove =\n    this.stove.getOrRegister(DbSchedulerSystem(this.stove)).let { this.stove }\n\n@StoveDsl\nsuspend fun ValidationDsl.tasks(\n    validation: suspend DbSchedulerSystem.() -> Unit\n): Unit = validation(this.stove.getOrNone<DbSchedulerSystem>().getOrElse {\n    throw SystemNotRegisteredException(DbSchedulerSystem::class)\n})\n```\n\nThe first one registers the system during setup (`.with { dbScheduler() }`). The second one exposes it during tests (`tasks { ... }`).\n\n### 3. Bean Registration\n\nIf your system needs a component inside the application (like a listener), register it as a test bean:\n\n```kotlin hl_lines=\"4-6\"\nspringBoot(\n    runner = { params ->\n        runApplication<MyApp>(*params) {\n            addTestDependencies {\n                bean<StoveDbSchedulerListener>(isPrimary = true)\n            }\n        }\n    }\n)\n```\n\n<span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">That's the whole pattern.</span> The rest is your domain logic.\n\n## Ideas\n\nHere are examples of what you can build. Each shows the test DSL (the part your teammates will see), not the implementation details.\n\n### Scheduled Task Testing\n\nTest that your application scheduled and executed a task with the expected payload:\n\n```kotlin hl_lines=\"3 9\"\nstove {\n    http {\n        postAndExpectBodilessResponse(\"/orders\", body = orderRequest.some()) {\n            it.status shouldBe 200\n        }\n    }\n\n    tasks {\n        shouldBeExecuted<SendOrderConfirmationPayload>(atLeastIn = 10.seconds) {\n            orderId == expectedOrderId && recipientEmail == \"customer@example.com\"\n        }\n    }\n}\n```\n\n!!! note \"Full working example\"\n    See the [spring-showcase recipe](https://github.com/Trendyol/stove/blob/main/recipes/jvm/kotlin-recipes/spring-showcase/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/spring/e2e/setup/DbSchedulerSystem.kt) for the complete `DbSchedulerSystem` implementation with reporting integration.\n\n### Domain Event Capture\n\nCapture Spring application events in memory and assert on them:\n\n```kotlin hl_lines=\"7 11\"\nstove {\n    http {\n        post<UserResponse>(\"/users\", createUserRequest) { it.status shouldBe 201 }\n    }\n\n    domainEvents {\n        shouldBePublished<UserCreatedEvent>(atLeastIn = 5.seconds) {\n            userId == expectedId && name == \"John\"\n        }\n\n        shouldNotBePublished<UserDeletedEvent> {\n            userId == expectedId\n        }\n    }\n}\n```\n\nThe system behind this is a `@EventListener` bean that collects events into a `ConcurrentLinkedQueue`, and a `DomainEventSystem` that polls it with a timeout.\n\n### Time Control\n\nReplace your application's `Clock` with a <span data-rn=\"highlight\" data-rn-color=\"#00968855\" data-rn-duration=\"800\">test-controllable one</span>:\n\n```kotlin hl_lines=\"6-7 12\"\nstove {\n    http {\n        post<SessionResponse>(\"/login\", credentials) { sessionId = it.sessionId }\n    }\n\n    time {\n        advance(31.minutes)\n    }\n\n    http {\n        getResponseBodiless(\"/protected\", headers = mapOf(\"Session-ID\" to sessionId)) {\n            it.status shouldBe 401  // Session expired\n        }\n    }\n}\n```\n\nThe system injects a `StoveTestClock` (extending `java.time.Clock`) as a Spring bean, and the `advance()` / `setTime()` methods manipulate it.\n\n### Exposing Configuration\n\nIf your system starts infrastructure (like a container) and needs to pass connection details to the application:\n\n```kotlin\nclass MySystem(\n    override val stove: Stove,\n    private val options: MySystemOptions\n) : PluggedSystem, RunAware, ExposesConfiguration {\n\n    private lateinit var config: MyExposedConfig\n\n    override suspend fun run() {\n        config = MyExposedConfig(host = \"localhost\", port = startContainer())\n    }\n\n    override fun configuration(): List<String> =\n        options.configureExposedConfiguration(config)\n\n    override fun close() {}\n}\n```\n\n<span data-rn=\"underline\" data-rn-color=\"#ff9800\">Stove collects all `configuration()` outputs and passes them to the application as startup parameters.</span>\n\n## Extending Built-In Systems\n\nYou don't always need a full system. Sometimes an extension function on an existing system is enough:\n\n```kotlin hl_lines=\"1-2 15-17\"\n@StoveDsl\nsuspend fun KafkaSystem.publishWithCorrelationId(\n    topic: String,\n    message: Any,\n    correlationId: String = UUID.randomUUID().toString()\n) {\n    publish(\n        topic = topic,\n        message = message,\n        headers = mapOf(\"X-Correlation-ID\" to correlationId)\n    )\n}\n\n// Usage\nkafka {\n    publishWithCorrelationId(\"orders.created\", orderEvent)\n}\n```\n\nThis works for any built-in system: `HttpSystem`, `KafkaSystem`, `PostgresqlSystem`, etc. Use `@StoveDsl` for IDE auto-completion support.\n"
  },
  {
    "path": "examples/build.gradle.kts",
    "content": "subprojects {\n  configurations.configureEach {\n    this.resolutionStrategy {\n      eachDependency {\n        if (requested.group == \"com.google.protobuf\" && requested.name.startsWith(\"protobuf-\")) {\n          useVersion(libs.versions.google.protobuf.get())\n          because(\"Align protobuf runtime with generated code version\")\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/build.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nimport com.trendyol.stove.gradle.stoveTracing\n\nplugins {\n  kotlin(\"jvm\") version libs.versions.kotlin\n  application\n  idea\n  kotlin(\"plugin.serialization\") version libs.versions.kotlin\n  alias(libs.plugins.protobuf)\n}\n\napplication {\n  val groupId = rootProject.group.toString()\n  val artifactId = project.name\n  mainClass.set(\"$groupId.$artifactId.ApplicationKt\")\n\n  val isDevelopment: Boolean = project.ext.has(\"development\")\n  applicationDefaultJvmArgs = listOf(\"-Dio.ktor.development=$isDevelopment\")\n}\n\nstoveTracing {\n  serviceName = \"ktor-example\"\n  otelAgentVersion = libs.versions.opentelemetry.instrumentation.get()\n}\n\ndependencies {\n  implementation(libs.ktor.server)\n  implementation(libs.ktor.server.cio)\n  implementation(libs.ktor.serialization.kotlinx.json)\n  implementation(libs.ktor.server.call.logging)\n  implementation(libs.koin.ktor)\n  implementation(libs.koin.logger.slf4j)\n  implementation(libs.kotlinx.reactor)\n  implementation(libs.kotlinx.core)\n  implementation(libs.r2dbc.postgresql)\n  implementation(libs.kafka)\n  implementation(libs.hoplite.yaml)\n  implementation(libs.jackson.kotlin)\n  implementation(libs.jackson.databind)\n\n  // OpenTelemetry API for manual span recording (exceptions in catch blocks)\n  implementation(libs.opentelemetry.api)\n\n  // gRPC service clients (FeatureToggle, Pricing)\n  implementation(libs.io.grpc)\n  implementation(libs.io.grpc.stub)\n  implementation(libs.io.grpc.protobuf)\n  implementation(libs.io.grpc.netty)\n  implementation(libs.io.grpc.kotlin)\n  implementation(libs.google.protobuf.kotlin)\n\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.logback.classic)\n  testImplementation(projects.stove.lib.stoveHttp)\n  testImplementation(projects.stove.lib.stoveWiremock)\n  testImplementation(projects.stove.lib.stovePostgres)\n  testImplementation(projects.stove.lib.stoveKafka)\n  testImplementation(projects.stove.lib.stoveDashboard)\n  testImplementation(projects.stove.lib.stoveGrpc)\n  testImplementation(projects.stove.lib.stoveGrpcMock)\n  testImplementation(projects.stove.starters.ktor.stoveKtor)\n}\n\nrepositories {\n  mavenCentral()\n  maven { url = uri(\"https://oss.sonatype.org/content/repositories/snapshots\") }\n}\n\nprotobuf {\n  protoc {\n    artifact = libs.protoc.get().toString()\n  }\n\n  plugins {\n    create(\"grpc\").apply {\n      artifact = libs.grpc.protoc.gen.java.get().toString()\n    }\n    create(\"grpckt\").apply {\n      artifact = \"${libs.grpc.protoc.gen.kotlin.get()}:jdk8@jar\"\n    }\n  }\n\n  generateProtoTasks {\n    all().forEach { task ->\n      task.plugins {\n        create(\"grpc\")\n        create(\"grpckt\")\n      }\n      task.builtins {\n        create(\"kotlin\")\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/Application.kt",
    "content": "@file:Suppress(\"ExtractKtorModule\")\n\npackage stove.ktor.example\n\nimport io.ktor.serialization.kotlinx.json.*\nimport io.ktor.server.application.*\nimport io.ktor.server.cio.*\nimport io.ktor.server.engine.*\nimport io.ktor.server.plugins.contentnegotiation.*\nimport org.koin.core.module.Module\nimport org.koin.dsl.module\nimport org.koin.ktor.ext.get\nimport org.koin.ktor.plugin.Koin\nimport org.koin.logger.SLF4JLogger\nimport stove.ktor.example.app.*\nimport stove.ktor.example.application.ExampleAppConsumer\n\nconst val CONNECT_TIMEOUT_SECONDS = 10L\n\nfun main(args: Array<String>) {\n  run(args, shouldWait = true)\n}\n\nfun run(\n  args: Array<String>,\n  shouldWait: Boolean = false,\n  applicationOverrides: () -> Module = { module { } }\n): Application {\n  val config = loadConfiguration<AppConfiguration>(args)\n\n  val applicationEngine = embeddedServer(CIO, port = config.port, host = \"localhost\") {\n    mainModule(config, applicationOverrides)\n  }\n\n  applicationEngine.monitor.subscribe(ApplicationStarted) {\n    it.get<ExampleAppConsumer<String, Any>>().start()\n  }\n\n  applicationEngine.monitor.subscribe(ApplicationStopping) {\n    it.get<ExampleAppConsumer<String, Any>>().stop()\n  }\n\n  applicationEngine.start(wait = shouldWait)\n  return applicationEngine.application\n}\n\nfun Application.mainModule(config: AppConfiguration, applicationOverrides: () -> Module) {\n  install(ContentNegotiation) {\n    json()\n  }\n\n  install(Koin) {\n    SLF4JLogger()\n    modules(\n      module { single { config } },\n      kafka(),\n      postgresql(),\n      app(config),\n      applicationOverrides()\n    )\n  }\n\n  configureRouting()\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/app/app.kt",
    "content": "package stove.ktor.example.app\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport org.koin.dsl.*\nimport stove.ktor.example.application.*\nimport stove.ktor.example.domain.ProductRepository\nimport stove.ktor.example.infrastructure.FeatureToggleClient\nimport stove.ktor.example.infrastructure.PricingClient\n\nval objectMapperRef: ObjectMapper = ObjectMapper().apply {\n  findAndRegisterModules()\n}\n\nfun app(cfg: AppConfiguration) = module {\n  // External gRPC clients - both can point to the same mock server in tests\n  single { FeatureToggleClient(cfg.featureToggle.host, cfg.featureToggle.port) }\n  single { PricingClient(cfg.pricing.host, cfg.pricing.port) }\n\n  single { ProductRepository(get()) }\n  single { ProductService(get(), get(), get(), get(), get()) }\n  single { MutexLockProvider() }.bind<LockProvider>()\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/app/configuration.kt",
    "content": "package stove.ktor.example.app\n\nimport com.sksamuel.hoplite.*\nimport com.sksamuel.hoplite.env.Environment\n\n@OptIn(ExperimentalHoplite::class)\ninline fun <reified T : Any> loadConfiguration(args: Array<String> = arrayOf()): T = ConfigLoaderBuilder\n  .default()\n  .addEnvironmentSource()\n  .addCommandLineSource(args)\n  .withExplicitSealedTypes()\n  .withEnvironment(AppEnv.toEnv())\n  .apply {\n    when (AppEnv.current()) {\n      AppEnv.Local -> {\n        addResourceSource(\"/application.yaml\", optional = true)\n      }\n\n      AppEnv.Prod -> {\n        addResourceSource(\"/application-prod.yaml\", optional = true)\n        addResourceSource(\"/application.yaml\", optional = true)\n      }\n\n      else -> {\n        addResourceSource(\"/application.yaml\", optional = true)\n      }\n    }\n  }.build()\n  .loadConfigOrThrow<T>()\n\ndata class AppConfiguration(\n  val port: Int,\n  val database: DatabaseConfiguration,\n  val kafka: KafkaConfiguration,\n  val featureToggle: FeatureToggleConfiguration = FeatureToggleConfiguration(),\n  val pricing: PricingConfiguration = PricingConfiguration()\n)\n\ndata class FeatureToggleConfiguration(\n  val host: String = \"localhost\",\n  val port: Int = 9090\n)\n\ndata class PricingConfiguration(\n  val host: String = \"localhost\",\n  val port: Int = 9090\n)\n\ndata class DatabaseConfiguration(\n  val host: String,\n  val port: Int,\n  val name: String,\n  val jdbcUrl: String,\n  val username: String,\n  val password: String\n)\n\ndata class KafkaConfiguration(\n  val bootstrapServers: String,\n  val groupId: String,\n  val clientId: String,\n  val interceptorClasses: List<String>,\n  val topics: Map<String, TopicConfiguration>\n)\n\ndata class TopicConfiguration(\n  val topic: String,\n  val retry: String,\n  val error: String\n)\n\nenum class AppEnv(\n  val env: String\n) {\n  Unspecified(\"\"),\n  Local(Environment.local.name),\n  Prod(Environment.prod.name)\n  ;\n\n  companion object {\n    fun current(): AppEnv = when (System.getenv(\"ENVIRONMENT\")) {\n      Unspecified.env -> Unspecified\n      Local.env -> Local\n      Prod.env -> Prod\n      else -> Local\n    }\n\n    fun toEnv(): Environment = when (current()) {\n      Local -> Environment.local\n      Prod -> Environment.prod\n      else -> Environment.local\n    }\n  }\n\n  fun isLocal(): Boolean = this === Local\n\n  fun isProd(): Boolean = this === Prod\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/app/database.kt",
    "content": "package stove.ktor.example.app\n\nimport io.r2dbc.postgresql.*\nimport org.koin.core.context.GlobalContext.get\nimport org.koin.dsl.module\nimport stove.ktor.example.CONNECT_TIMEOUT_SECONDS\nimport java.time.Duration\n\nfun postgresql() = module {\n  single {\n    val config = get<AppConfiguration>()\n    val builder = PostgresqlConnectionConfiguration.builder().apply {\n      host(config.database.host)\n      database(config.database.name)\n      port(config.database.port)\n      password(config.database.password)\n      username(config.database.username)\n    }\n\n    PostgresqlConnectionFactory(builder.connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS)).build())\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/app/kafka.kt",
    "content": "package stove.ktor.example.app\n\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport org.apache.kafka.clients.consumer.*\nimport org.apache.kafka.clients.producer.*\nimport org.apache.kafka.common.serialization.*\nimport org.koin.core.module.Module\nimport org.koin.dsl.module\nimport stove.ktor.example.application.*\nimport kotlin.time.Duration.Companion.seconds\n\nfun kafka(): Module = module {\n  single { createConsumer<Any>(get()) }\n  single { createProducer(get()) }\n  single { ExampleAppConsumer<String, Any>(get(), get()) }\n}\n\n@Suppress(\"MagicNumber\")\nprivate fun <V : Any> createConsumer(config: AppConfiguration): KafkaConsumer<String, V> {\n  val pollTimeoutSec = POLL_TIMEOUT_SECONDS\n  val heartbeatSec = pollTimeoutSec + 1\n  return KafkaConsumer(\n    mapOf(\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to config.kafka.bootstrapServers,\n      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,\n      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to ExampleAppKafkaValueDeserializer::class.java,\n      ConsumerConfig.GROUP_ID_CONFIG to config.kafka.groupId,\n      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to \"earliest\",\n      ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG to config.kafka.interceptorClasses,\n      ConsumerConfig.CLIENT_ID_CONFIG to config.kafka.clientId,\n      ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG to true,\n      ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to heartbeatSec.seconds.inWholeSeconds.toInt(),\n      ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG to true\n    )\n  )\n}\n\nprivate fun createProducer(config: AppConfiguration): KafkaProducer<String, Any> = KafkaProducer(\n  mapOf(\n    ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to config.kafka.bootstrapServers,\n    ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,\n    ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to ExampleAppKafkaValueSerializer::class.java,\n    ProducerConfig.INTERCEPTOR_CLASSES_CONFIG to config.kafka.interceptorClasses,\n    ProducerConfig.CLIENT_ID_CONFIG to config.kafka.clientId\n  )\n)\n\n@Suppress(\"UNCHECKED_CAST\")\nclass ExampleAppKafkaValueDeserializer<T : Any> : Deserializer<T> {\n  override fun deserialize(\n    topic: String,\n    data: ByteArray\n  ): T = objectMapperRef.readValue<Any>(data) as T\n}\n\nclass ExampleAppKafkaValueSerializer<T : Any> : Serializer<T> {\n  override fun serialize(\n    topic: String,\n    data: T\n  ): ByteArray = objectMapperRef.writeValueAsBytes(data)\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/app/routing.kt",
    "content": "package stove.ktor.example.app\n\nimport io.ktor.http.*\nimport io.ktor.server.application.*\nimport io.ktor.server.request.*\nimport io.ktor.server.response.*\nimport io.ktor.server.routing.*\nimport io.opentelemetry.api.trace.Span\nimport io.opentelemetry.api.trace.StatusCode\nimport org.koin.ktor.ext.get\nimport stove.ktor.example.application.*\n\nfun Application.configureRouting() {\n  routing {\n    post(\"/products/{id}\") {\n      try {\n        val id = call.parameters[\"id\"]!!.toInt()\n        val request = call.receive<UpdateProductRequest>()\n        call.get<ProductService>().update(id, request)\n        call.respond(HttpStatusCode.OK)\n      } catch (\n        @Suppress(\"TooGenericExceptionCaught\") ex: Exception\n      ) {\n        // Record exception in span for tracing visibility\n        Span.current().apply {\n          recordException(ex)\n          setStatus(StatusCode.ERROR, ex.message ?: \"Unknown error\")\n        }\n        ex.printStackTrace()\n        call.respond(HttpStatusCode.BadRequest)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/application/ExampleAppConsumer.kt",
    "content": "package stove.ktor.example.application\n\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport org.apache.kafka.clients.consumer.*\nimport stove.ktor.example.app.AppConfiguration\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.toJavaDuration\n\nconst val POLL_TIMEOUT_SECONDS = 2\n\nclass ExampleAppConsumer<K, V>(\n  config: AppConfiguration,\n  kafkaConsumer: KafkaConsumer<K, V>\n) {\n  private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n  private val topics = config.kafka.topics.values\n    .fold(listOf<String>()) { acc, topic -> acc + topic.topic + topic.error + topic.retry }\n\n  private val subscription = kafkaConsumer\n    .apply { subscribe(topics) }\n\n  fun start() {\n    loop()\n  }\n\n  private fun loop() {\n    channelFlow {\n      while (isActive) {\n        val records = subscription.poll(POLL_TIMEOUT_SECONDS.seconds.toJavaDuration())\n        for (record in records) {\n          send(record)\n        }\n      }\n    }.onEach { consume(it) }\n      .catch { exception -> throw exception }\n      .launchIn(scope)\n  }\n\n  fun stop() = scope.cancel()\n\n  private fun consume(message: ConsumerRecord<K, V>) {\n    println(\"Consumed message: $message\")\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/application/LockProvider.kt",
    "content": "package stove.ktor.example.application\n\nimport kotlinx.coroutines.sync.Mutex\nimport java.time.Duration\n\ninterface LockProvider {\n  suspend fun acquireLock(\n    name: String,\n    duration: Duration\n  ): Boolean\n\n  suspend fun releaseLock(name: String)\n}\n\nclass MutexLockProvider : LockProvider {\n  private val mutex = Mutex()\n\n  override suspend fun acquireLock(\n    name: String,\n    duration: Duration\n  ): Boolean = mutex.tryLock(this)\n\n  override suspend fun releaseLock(name: String) {\n    mutex.unlock(this)\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/application/ProductService.kt",
    "content": "package stove.ktor.example.application\n\nimport org.apache.kafka.clients.producer.*\nimport stove.ktor.example.domain.*\nimport stove.ktor.example.infrastructure.FeatureToggleClient\nimport stove.ktor.example.infrastructure.PricingClient\nimport java.time.Duration\nimport kotlin.coroutines.*\n\nclass ProductService(\n  private val repository: ProductRepository,\n  private val lockProvider: LockProvider,\n  private val kafkaProducer: KafkaProducer<String, Any>,\n  private val featureToggleClient: FeatureToggleClient,\n  private val pricingClient: PricingClient\n) {\n  companion object {\n    private const val DURATION = 30L\n    private const val FEATURE_PRODUCT_UPDATE = \"product-update-enabled\"\n  }\n\n  suspend fun update(id: Int, request: UpdateProductRequest) {\n    // 1. Check if product update feature is enabled (Feature Toggle Service)\n    val featureCheck = featureToggleClient.isFeatureEnabled(\n      featureName = FEATURE_PRODUCT_UPDATE,\n      context = request.userId ?: \"anonymous\"\n    )\n\n    if (!featureCheck.enabled) {\n      error(\"Product update feature is currently disabled\")\n    }\n\n    // 2. Get pricing information (Pricing Service)\n    val priceInfo = pricingClient.calculatePrice(\n      productId = id.toString(),\n      quantity = 1,\n      currency = \"USD\",\n      customerTier = \"standard\"\n    )\n\n    val acquireLock = lockProvider.acquireLock(::ProductService.name, Duration.ofSeconds(DURATION))\n\n    if (!acquireLock) {\n      print(\"lock could not be acquired\")\n      return\n    }\n\n    try {\n      repository.transaction {\n        val product = it.findById(id)\n        product.name = request.name\n        it.update(product)\n      }\n\n      // Publish event with price info\n      suspendCoroutine {\n        kafkaProducer\n          .send(\n            ProducerRecord(\n              \"product\",\n              id.toString(),\n              DomainEvents.ProductUpdated(id, request.name, priceInfo.finalPrice)\n            )\n          ) { _, exception ->\n            if (exception != null) {\n              it.resumeWithException(exception)\n            } else {\n              it.resume(Unit)\n            }\n          }\n      }\n    } finally {\n      lockProvider.releaseLock(::ProductService.name)\n    }\n  }\n\n  /**\n   * Get product with calculated price.\n   */\n  suspend fun getProductWithPrice(productId: Int, customerId: String): ProductWithPrice {\n    val product = repository.findById(productId)\n\n    // Get discount from Pricing Service\n    val discount = pricingClient.getDiscount(customerId, \"electronics\")\n\n    // Calculate final price\n    val price = pricingClient.calculatePrice(\n      productId = productId.toString(),\n      quantity = 1,\n      currency = \"USD\",\n      customerTier = if (discount.isApplicable) \"premium\" else \"standard\"\n    )\n\n    return ProductWithPrice(\n      id = product.id,\n      name = product.name,\n      basePrice = price.basePrice,\n      discount = price.discount,\n      finalPrice = price.finalPrice\n    )\n  }\n}\n\ndata class ProductWithPrice(\n  val id: Int,\n  val name: String,\n  val basePrice: Double,\n  val discount: Double,\n  val finalPrice: Double\n)\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/application/UpdateProductRequest.kt",
    "content": "package stove.ktor.example.application\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class UpdateProductRequest(\n  val name: String,\n  val userId: String? = null\n)\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/domain/Product.kt",
    "content": "package stove.ktor.example.domain\n\ndata class Product(\n  val id: Int,\n  var name: String\n)\n\nobject DomainEvents {\n  data class ProductUpdated(\n    val id: Int,\n    val name: String,\n    val price: Double = 0.0\n  )\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/domain/ProductRepository.kt",
    "content": "package stove.ktor.example.domain\n\nimport io.r2dbc.postgresql.PostgresqlConnectionFactory\nimport io.r2dbc.postgresql.api.PostgresqlConnection\nimport kotlinx.coroutines.reactive.awaitFirst\nimport kotlinx.coroutines.reactive.awaitFirstOrNull\nimport kotlinx.coroutines.reactive.awaitSingle\n\nclass ProductRepository(\n  private val postgresqlConnectionFactory: PostgresqlConnectionFactory\n) {\n  private lateinit var connection: PostgresqlConnection\n\n  suspend fun findById(id: Int): Product = connection\n    .createStatement(\n      \"SELECT * FROM Products WHERE id=$id\"\n    ).execute()\n    .awaitFirst()\n    .map { r, rm ->\n      Product(\n        (r.get(Product::id.name, rm.getColumnMetadata(Product::id.name).javaType!!) as Int),\n        r.get(Product::name.name, rm.getColumnMetadata(Product::name.name).javaType!!) as String\n      )\n    }.awaitSingle()\n\n  suspend fun update(product: Product) {\n    connection\n      .createStatement(\"UPDATE Products SET ${Product::name.name}=('${product.name}') WHERE ${Product::id.name}=${product.id}\")\n      .execute()\n      .awaitFirstOrNull()\n  }\n\n  suspend fun transaction(invoke: suspend (ProductRepository) -> Unit) {\n    connection = this.postgresqlConnectionFactory.create().awaitFirst()\n    connection.beginTransaction().awaitFirstOrNull()\n    try {\n      invoke(this)\n      connection.commitTransaction().awaitFirstOrNull()\n    } catch (\n      @Suppress(\"TooGenericExceptionCaught\") ex: Exception\n    ) {\n      connection.rollbackTransaction().awaitFirstOrNull()\n      throw ex\n    }\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/infrastructure/FeatureToggleClient.kt",
    "content": "package stove.ktor.example.infrastructure\n\nimport io.grpc.ManagedChannel\nimport io.grpc.ManagedChannelBuilder\nimport stove.ktor.example.grpc.*\nimport java.util.concurrent.TimeUnit\n\n/**\n * gRPC client for the external Feature Toggle service.\n *\n * This client calls a hypothetical external Feature Toggle microservice to:\n * - Check if specific features are enabled\n * - Get all feature flags for a context\n */\nclass FeatureToggleClient(\n  private val host: String,\n  private val port: Int\n) : AutoCloseable {\n  private val channel: ManagedChannel = ManagedChannelBuilder\n    .forAddress(host, port)\n    .usePlaintext()\n    .build()\n\n  private val stub: FeatureToggleServiceGrpcKt.FeatureToggleServiceCoroutineStub =\n    FeatureToggleServiceGrpcKt.FeatureToggleServiceCoroutineStub(channel)\n\n  /**\n   * Check if a feature is enabled for a given context.\n   */\n  suspend fun isFeatureEnabled(featureName: String, context: String): IsFeatureEnabledResponse {\n    val request = isFeatureEnabledRequest {\n      this.featureName = featureName\n      this.context = context\n    }\n    return stub.isFeatureEnabled(request)\n  }\n\n  /**\n   * Get all feature flags for a context.\n   */\n  suspend fun getFeatures(context: String): GetFeaturesResponse {\n    val request = getFeaturesRequest {\n      this.context = context\n    }\n    return stub.getFeatures(request)\n  }\n\n  @Suppress(\"MagicNumber\")\n  override fun close() {\n    channel.shutdown().awaitTermination(5, TimeUnit.SECONDS)\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/kotlin/stove/ktor/example/infrastructure/PricingClient.kt",
    "content": "package stove.ktor.example.infrastructure\n\nimport io.grpc.ManagedChannel\nimport io.grpc.ManagedChannelBuilder\nimport stove.ktor.example.grpc.*\nimport java.util.concurrent.TimeUnit\n\n/**\n * gRPC client for the external Pricing service.\n *\n * This client calls a hypothetical external Pricing microservice to:\n * - Calculate prices for products\n * - Get applicable discounts for customers\n */\nclass PricingClient(\n  private val host: String,\n  private val port: Int\n) : AutoCloseable {\n  private val channel: ManagedChannel = ManagedChannelBuilder\n    .forAddress(host, port)\n    .usePlaintext()\n    .build()\n\n  private val stub: PricingServiceGrpcKt.PricingServiceCoroutineStub =\n    PricingServiceGrpcKt.PricingServiceCoroutineStub(channel)\n\n  /**\n   * Calculate price for a product.\n   */\n  suspend fun calculatePrice(\n    productId: String,\n    quantity: Int,\n    currency: String = \"USD\",\n    customerTier: String = \"standard\"\n  ): CalculatePriceResponse {\n    val request = calculatePriceRequest {\n      this.productId = productId\n      this.quantity = quantity\n      this.currency = currency\n      this.customerTier = customerTier\n    }\n    return stub.calculatePrice(request)\n  }\n\n  /**\n   * Get applicable discount for a customer.\n   */\n  suspend fun getDiscount(customerId: String, productCategory: String): GetDiscountResponse {\n    val request = getDiscountRequest {\n      this.customerId = customerId\n      this.productCategory = productCategory\n    }\n    return stub.getDiscount(request)\n  }\n\n  @Suppress(\"MagicNumber\")\n  override fun close() {\n    channel.shutdown().awaitTermination(5, TimeUnit.SECONDS)\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/proto/feature_toggle.proto",
    "content": "syntax = \"proto3\";\n\npackage featuretoggle;\n\noption java_package = \"stove.ktor.example.grpc\";\noption java_multiple_files = true;\n\n// Request to check if a feature is enabled\nmessage IsFeatureEnabledRequest {\n  string feature_name = 1;\n  string context = 2;  // e.g., user_id, tenant_id, etc.\n}\n\n// Response with feature status\nmessage IsFeatureEnabledResponse {\n  bool enabled = 1;\n  string variant = 2;  // Optional variant name for A/B testing\n}\n\n// Request to get all features for a context\nmessage GetFeaturesRequest {\n  string context = 1;\n}\n\n// Response with all feature flags\nmessage GetFeaturesResponse {\n  map<string, bool> features = 1;\n}\n\n// External Feature Toggle service (hypothetical dependency)\nservice FeatureToggleService {\n  // Check if a specific feature is enabled\n  rpc IsFeatureEnabled(IsFeatureEnabledRequest) returns (IsFeatureEnabledResponse);\n  \n  // Get all feature flags for a context\n  rpc GetFeatures(GetFeaturesRequest) returns (GetFeaturesResponse);\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/proto/pricing.proto",
    "content": "syntax = \"proto3\";\n\npackage pricing;\n\noption java_package = \"stove.ktor.example.grpc\";\noption java_multiple_files = true;\n\n// Request to calculate price\nmessage CalculatePriceRequest {\n  string product_id = 1;\n  int32 quantity = 2;\n  string currency = 3;\n  string customer_tier = 4;  // e.g., \"standard\", \"premium\", \"vip\"\n}\n\n// Price calculation response\nmessage CalculatePriceResponse {\n  double base_price = 1;\n  double discount = 2;\n  double final_price = 3;\n  string currency = 4;\n}\n\n// Request to get discount for a customer\nmessage GetDiscountRequest {\n  string customer_id = 1;\n  string product_category = 2;\n}\n\n// Discount response\nmessage GetDiscountResponse {\n  double discount_percentage = 1;\n  string discount_code = 2;\n  bool is_applicable = 3;\n}\n\n// External Pricing service (hypothetical dependency)\nservice PricingService {\n  // Calculate price for a product\n  rpc CalculatePrice(CalculatePriceRequest) returns (CalculatePriceResponse);\n  \n  // Get applicable discount for a customer\n  rpc GetDiscount(GetDiscountRequest) returns (GetDiscountResponse);\n}\n"
  },
  {
    "path": "examples/ktor-example/src/main/resources/application.yaml",
    "content": "port: 8080\ndatabase:\n  jdbcUrl: \"jdbc:postgresql://localhost:5432/stove\"\n  host: \"localhost\"\n  port: 1234\n  name: \"stove\"\n  username: \"\"\n  password: \"\"\n\nkafka:\n  bootstrapServers: \"localhost:9092\"\n  groupId: \"test-group\"\n  clientId: \"test-client\"\n  interceptorClasses: []\n  topics:\n    product:\n      topic: \"product\"\n      retry: \"product.retry\"\n      error: \"product.error\"\n\n"
  },
  {
    "path": "examples/ktor-example/src/main/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n    <root level=\"trace\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.eclipse.jetty\" level=\"INFO\"/>\n    <logger name=\"io.netty\" level=\"INFO\"/>\n</configuration>\n"
  },
  {
    "path": "examples/ktor-example/src/test/kotlin/com/stove/ktor/example/e2e/ExampleTest.kt",
    "content": "package com.stove.ktor.example.e2e\n\nimport arrow.core.*\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.system.using\nimport com.trendyol.stove.testing.grpcmock.grpcMock\nimport io.grpc.Status\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport stove.ktor.example.application.*\nimport stove.ktor.example.domain.*\nimport stove.ktor.example.grpc.*\nimport kotlin.random.Random\nimport kotlin.time.Duration.Companion.seconds\n\nclass ExampleTest :\n  FunSpec({\n    data class ProductOfTest(\n      val id: Long,\n      val name: String\n    )\n\n    test(\"should save product when both Feature Toggle and Pricing services respond successfully\") {\n      stove {\n        val givenId = Random.nextInt()\n        val givenName = \"T-Shirt, Red, M\"\n        val givenUserId = \"user-123\"\n        val expectedPrice = 29.99\n\n        // =====================================================\n        // Mock MULTIPLE gRPC services in the SAME grpcMock block\n        // All services are handled by the same mock server\n        // =====================================================\n        grpcMock {\n          // Mock #1: Feature Toggle Service - enable the feature\n          mockUnary(\n            serviceName = \"featuretoggle.FeatureToggleService\",\n            methodName = \"IsFeatureEnabled\",\n            response = IsFeatureEnabledResponse\n              .newBuilder()\n              .setEnabled(true)\n              .setVariant(\"default\")\n              .build()\n          )\n\n          // Mock #2: Pricing Service - return calculated price\n          mockUnary(\n            serviceName = \"pricing.PricingService\",\n            methodName = \"CalculatePrice\",\n            response = CalculatePriceResponse\n              .newBuilder()\n              .setBasePrice(34.99)\n              .setDiscount(5.00)\n              .setFinalPrice(expectedPrice)\n              .setCurrency(\"USD\")\n              .build()\n          )\n        }\n\n        postgresql {\n          shouldExecute(\n            \"\"\"\n            DROP TABLE IF EXISTS Products;\n            CREATE TABLE IF NOT EXISTS Products (\n            \tid serial PRIMARY KEY,\n            \tname VARCHAR (50)  NOT NULL\n            );\n            \"\"\".trimIndent()\n          )\n          shouldExecute(\"INSERT INTO Products (id, name) VALUES ('$givenId', 'T-Shirt, Red, S')\")\n        }\n\n        http {\n          postAndExpectBodilessResponse(\n            \"/products/$givenId\",\n            body = UpdateProductRequest(givenName, givenUserId).some(),\n            token = None\n          ) { actual ->\n            actual.status shouldBe 200\n          }\n        }\n\n        postgresql {\n          shouldQuery<ProductOfTest>(\"Select * FROM Products WHERE id=$givenId\", mapper = { row ->\n            ProductOfTest(row.long(\"id\"), row.string(\"name\"))\n          }) {\n            it.count() shouldBe 1\n            it.first().name shouldBe givenName\n          }\n        }\n\n        kafka {\n          shouldBePublished<DomainEvents.ProductUpdated>(5.seconds) {\n            actual.id == givenId && actual.name == givenName && actual.price == expectedPrice\n          }\n          shouldBeConsumed<DomainEvents.ProductUpdated>(20.seconds) {\n            actual.id == givenId && actual.name == givenName\n          }\n        }\n      }\n    }\n\n    test(\"should reject update when Feature Toggle is disabled (Pricing not called)\") {\n      stove {\n        val givenId = Random.nextInt()\n        val givenName = \"T-Shirt, Blue, L\"\n        val givenUserId = \"user-456\"\n\n        grpcMock {\n          // Feature Toggle disabled - Pricing service won't be called\n          mockUnary(\n            serviceName = \"featuretoggle.FeatureToggleService\",\n            methodName = \"IsFeatureEnabled\",\n            response = IsFeatureEnabledResponse\n              .newBuilder()\n              .setEnabled(false)\n              .build()\n          )\n          // Note: No Pricing mock needed - feature check fails first\n        }\n\n        postgresql {\n          shouldExecute(\n            \"\"\"\n            DROP TABLE IF EXISTS Products;\n            CREATE TABLE IF NOT EXISTS Products (\n            \tid serial PRIMARY KEY,\n            \tname VARCHAR (50)  NOT NULL\n            );\n            \"\"\".trimIndent()\n          )\n          shouldExecute(\"INSERT INTO Products (id, name) VALUES ('$givenId', 'T-Shirt, Blue, S')\")\n        }\n\n        http {\n          postAndExpectBodilessResponse(\n            \"/products/$givenId\",\n            body = UpdateProductRequest(givenName, givenUserId).some(),\n            token = None\n          ) { actual ->\n            actual.status shouldBe 400\n          }\n        }\n\n        // Verify product was NOT updated\n        postgresql {\n          shouldQuery<ProductOfTest>(\"Select * FROM Products WHERE id=$givenId\", mapper = { row ->\n            ProductOfTest(row.long(\"id\"), row.string(\"name\"))\n          }) {\n            it.first().name shouldBe \"T-Shirt, Blue, S\"\n          }\n        }\n      }\n    }\n\n    test(\"should handle Pricing Service failure gracefully\") {\n      stove {\n        val givenId = Random.nextInt()\n        val givenName = \"T-Shirt, Green, XL\"\n        val givenUserId = \"user-789\"\n\n        grpcMock {\n          // Feature Toggle enabled\n          mockUnary(\n            serviceName = \"featuretoggle.FeatureToggleService\",\n            methodName = \"IsFeatureEnabled\",\n            response = IsFeatureEnabledResponse\n              .newBuilder()\n              .setEnabled(true)\n              .build()\n          )\n\n          // Pricing Service returns error\n          mockError(\n            serviceName = \"pricing.PricingService\",\n            methodName = \"CalculatePrice\",\n            status = Status.Code.UNAVAILABLE,\n            message = \"Pricing service is temporarily unavailable\"\n          )\n        }\n\n        postgresql {\n          shouldExecute(\n            \"\"\"\n            DROP TABLE IF EXISTS Products;\n            CREATE TABLE IF NOT EXISTS Products (\n            \tid serial PRIMARY KEY,\n            \tname VARCHAR (50)  NOT NULL\n            );\n            \"\"\".trimIndent()\n          )\n          shouldExecute(\"INSERT INTO Products (id, name) VALUES ('$givenId', 'T-Shirt, Green, S')\")\n        }\n\n        http {\n          postAndExpectBodilessResponse(\n            \"/products/$givenId\",\n            body = UpdateProductRequest(givenName, givenUserId).some(),\n            token = None\n          ) { actual ->\n            // Should fail because pricing service is unavailable\n            actual.status shouldBe 400\n          }\n        }\n      }\n    }\n\n    test(\"should mock different responses for same service based on request matching\") {\n      stove {\n        val givenId = Random.nextInt()\n        val givenName = \"Premium T-Shirt\"\n        val givenUserId = \"vip-user\"\n\n        grpcMock {\n          // Feature Toggle - enabled\n          mockUnary(\n            serviceName = \"featuretoggle.FeatureToggleService\",\n            methodName = \"IsFeatureEnabled\",\n            response = IsFeatureEnabledResponse\n              .newBuilder()\n              .setEnabled(true)\n              .setVariant(\"premium\")\n              .build()\n          )\n\n          // Pricing - VIP price with bigger discount\n          mockUnary(\n            serviceName = \"pricing.PricingService\",\n            methodName = \"CalculatePrice\",\n            response = CalculatePriceResponse\n              .newBuilder()\n              .setBasePrice(99.99)\n              .setDiscount(30.00) // VIP gets 30% off\n              .setFinalPrice(69.99)\n              .setCurrency(\"USD\")\n              .build()\n          )\n        }\n\n        postgresql {\n          shouldExecute(\n            \"\"\"\n            DROP TABLE IF EXISTS Products;\n            CREATE TABLE IF NOT EXISTS Products (\n              id serial PRIMARY KEY,\n              name VARCHAR (50) NOT NULL\n            );\n            \"\"\".trimIndent()\n          )\n          shouldExecute(\"INSERT INTO Products (id, name) VALUES ('$givenId', 'Old Name')\")\n        }\n\n        http {\n          postAndExpectBodilessResponse(\n            \"/products/$givenId\",\n            body = UpdateProductRequest(givenName, givenUserId).some(),\n            token = None\n          ) { actual ->\n            actual.status shouldBe 200\n          }\n        }\n\n        kafka {\n          shouldBePublished<DomainEvents.ProductUpdated>(5.seconds) {\n            actual.price == 69.99 // VIP price\n          }\n        }\n      }\n    }\n\n    test(\"stove should be able to override the test deps\") {\n      stove {\n        using<LockProvider> {\n          (this is NoOpLockProvider) shouldBe true\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/ktor-example/src/test/kotlin/com/stove/ktor/example/e2e/StoveConfig.kt",
    "content": "package com.stove.ktor.example.e2e\n\nimport com.trendyol.stove.dashboard.*\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.ktor.*\nimport com.trendyol.stove.postgres.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.testing.grpcmock.*\nimport com.trendyol.stove.tracing.tracing\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport stove.ktor.example.app.objectMapperRef\nimport stove.ktor.example.run\n\nclass StoveConfig : AbstractProjectConfig() {\n  companion object {\n    private val appPort = PortFinder.findAvailablePort()\n\n    init {\n      stoveKafkaBridgePortDefault = PortFinder.findAvailablePortAsString()\n      System.setProperty(STOVE_KAFKA_BRIDGE_PORT, stoveKafkaBridgePortDefault)\n    }\n  }\n\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  @Suppress(\"LongMethod\")\n  override suspend fun beforeProject() = Stove()\n    .with {\n      httpClient {\n        HttpClientSystemOptions(\n          baseUrl = \"http://localhost:$appPort\"\n        )\n      }\n      bridge()\n      tracing { enableSpanReceiver() }\n      dashboard { DashboardSystemOptions(appName = \"ktor-example\") }\n      postgresql {\n        PostgresqlOptions(configureExposedConfiguration = { cfg ->\n          listOf(\n            \"database.jdbcUrl=${cfg.jdbcUrl}\",\n            \"database.host=${cfg.host}\",\n            \"database.port=${cfg.port}\",\n            \"database.username=${cfg.username}\",\n            \"database.password=${cfg.password}\"\n          )\n        })\n      }\n      kafka {\n        KafkaSystemOptions(\n          serde = StoveSerde.jackson.anyByteArraySerde(objectMapperRef),\n          containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n        ) {\n          listOf(\n            \"kafka.bootstrapServers=${it.bootstrapServers}\",\n            \"kafka.interceptorClasses=${it.interceptorClass}\"\n          )\n        }\n      }\n\n      // =====================================================\n      // Single gRPC mock server for ALL external gRPC services\n      // Uses dynamic port (0) to avoid CI conflicts\n      // =====================================================\n      grpcMock {\n        GrpcMockSystemOptions(\n          // port = 0 by default (dynamic port)\n          removeStubAfterRequestMatched = true,\n          configureExposedConfiguration = { cfg ->\n            // Both gRPC clients in the app point to the SAME mock server\n            listOf(\n              \"featureToggle.host=${cfg.host}\",\n              \"featureToggle.port=${cfg.port}\",\n              \"pricing.host=${cfg.host}\",\n              \"pricing.port=${cfg.port}\"\n            )\n          }\n        )\n      }\n\n      ktor(\n        withParameters = listOf(\n          \"port=$appPort\"\n          // gRPC settings are now auto-injected via grpcMock's configureExposedConfiguration\n        ),\n        runner = { parameters ->\n          run(parameters) {\n            addTestSystemDependencies()\n          }\n        }\n      )\n    }.run()\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n}\n"
  },
  {
    "path": "examples/ktor-example/src/test/kotlin/com/stove/ktor/example/e2e/TestStub.kt",
    "content": "package com.stove.ktor.example.e2e\n\nimport org.koin.core.module.Module\nimport org.koin.dsl.*\nimport stove.ktor.example.application.LockProvider\nimport java.time.Duration\n\nfun addTestSystemDependencies(): Module =\n  module {\n    single { NoOpLockProvider() }.bind<LockProvider>()\n  }\n\nclass NoOpLockProvider : LockProvider {\n  override suspend fun acquireLock(\n    name: String,\n    duration: Duration\n  ): Boolean {\n    println(\"from NoOpLockProvider\")\n    return true\n  }\n\n  override suspend fun releaseLock(name: String) = Unit\n}\n"
  },
  {
    "path": "examples/ktor-example/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.stove.ktor.example.e2e.StoveConfig\n"
  },
  {
    "path": "examples/ktor-example/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "examples/micronaut-example/build.gradle.kts",
    "content": "import com.trendyol.stove.gradle.stoveTracing\n\nplugins {\n  kotlin(\"jvm\") version libs.versions.kotlin\n  kotlin(\"plugin.serialization\") version libs.versions.kotlin\n  alias(libs.plugins.google.ksp)\n  alias(libs.plugins.micronaut.application)\n  alias(libs.plugins.micronaut.aot)\n  application\n  idea\n}\n\ndependencies {\n  runtimeOnly(libs.snakeyaml)\n  ksp(platform(libs.micronaut.platform))\n  ksp(libs.micronaut.inject.kotlin)\n  implementation(libs.micronaut.kotlin.runtime)\n  implementation(libs.micronaut.serde.jackson)\n  implementation(libs.micronaut.http.client)\n  implementation(libs.micronaut.http.server.netty)\n  implementation(libs.micronaut.inject)\n  implementation(libs.micronaut.core)\n  implementation(libs.micronaut.micrometer.core)\n  implementation(libs.micronaut.data.r2dbc)\n  implementation(libs.jackson.kotlin)\n  implementation(libs.kafka)\n  implementation(libs.kotlinx.reactor)\n  implementation(libs.kotlinx.core)\n  implementation(libs.kotlinx.reactive)\n  implementation(libs.r2dbc.postgresql)\n  implementation(libs.postgresql)\n  implementation(libs.jackson.kotlin)\n  implementation(libs.kotlinx.slf4j)\n}\n\ndependencies {\n  testImplementation(projects.stove.lib.stoveHttp)\n  testImplementation(projects.stove.lib.stoveWiremock)\n  testImplementation(projects.stove.lib.stovePostgres)\n  testImplementation(projects.stove.lib.stoveElasticsearch)\n  testImplementation(projects.stove.lib.stoveDashboard)\n  testImplementation(projects.stove.lib.stoveTracing)\n  testImplementation(projects.stove.starters.micronaut.stoveMicronaut)\n  testImplementation(projects.testExtensions.stoveExtensionsKotest)\n}\n\napplication {\n  mainClass = \"stove.micronaut.example.ApplicationKt\"\n}\n\ngraalvmNative.toolchainDetection = false\n\njava {\n  sourceCompatibility = JavaVersion.toVersion(\"17\")\n}\n\nstoveTracing {\n  serviceName = \"micronaut-example\"\n  otelAgentVersion = libs.versions.opentelemetry.instrumentation.get()\n}\n\nmicronaut {\n  version(libs.versions.micronaut.platform.get())\n  runtime(\"netty\")\n  testRuntime(\"kotest5\")\n  processing {\n    incremental(true)\n    annotations(\"stove.micronaut.example.*\")\n  }\n  aot {\n    optimizeServiceLoading = false\n    convertYamlToJava = false\n    precomputeOperations = true\n    cacheEnvironment = true\n    optimizeClassLoading = true\n    deduceEnvironment = true\n    optimizeNetty = true\n    replaceLogbackXml = true\n  }\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/Application.kt",
    "content": "package stove.micronaut.example\n\nimport io.micronaut.context.ApplicationContext\nimport io.micronaut.runtime.EmbeddedApplication\n\nfun main(args: Array<String>) {\n  run(args)\n}\n\nfun run(\n  args: Array<String>,\n  init: ApplicationContext.() -> Unit = {}\n): ApplicationContext {\n  val context = ApplicationContext\n    .builder()\n    .args(*args)\n    .build()\n    .also(init)\n    .start()\n\n  context.findBean(EmbeddedApplication::class.java).ifPresent { app ->\n    if (!app.isRunning) {\n      app.start()\n    }\n  }\n\n  return context\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/domain/Product.kt",
    "content": "package stove.micronaut.example.application.domain\n\nimport io.micronaut.serde.annotation.Serdeable\nimport java.util.*\n\n@Serdeable\ndata class Product(\n  val id: String,\n  val name: String,\n  val supplierId: Long,\n  val isBlacklist: Boolean,\n  val createdDate: Date\n) {\n  companion object {\n    fun new(id: String, name: String, supplierId: Long, isBlacklist: Boolean): Product = Product(\n      id = id,\n      name = name,\n      supplierId = supplierId,\n      createdDate = Date(),\n      isBlacklist = isBlacklist\n    )\n  }\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/repository/ProductRepository.kt",
    "content": "package stove.micronaut.example.application.repository\n\nimport stove.micronaut.example.application.domain.Product\n\ninterface ProductRepository {\n  suspend fun save(product: Product): Product\n\n  suspend fun findById(id: Long): Product?\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/services/ProductService.kt",
    "content": "package stove.micronaut.example.application.services\n\nimport jakarta.inject.Singleton\nimport stove.micronaut.example.application.domain.Product\nimport stove.micronaut.example.application.repository.ProductRepository\nimport stove.micronaut.example.infrastructure.http.SupplierHttpService\n\n@Singleton\nclass ProductService(\n  private val productRepository: ProductRepository,\n  private val supplierHttpService: SupplierHttpService\n) {\n  suspend fun createProduct(id: String, productName: String, supplierId: Long): Product {\n    val supplier = supplierHttpService.getSupplierPermission(supplierId)\n    val product = Product.new(id, productName, supplierId, supplier!!.isBlacklisted)\n    productRepository.save(product)\n    return product\n  }\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/application/services/SupplierService.kt",
    "content": "package stove.micronaut.example.application.services\n\nimport io.micronaut.serde.annotation.Serdeable\n\n@Serdeable\ndata class SupplierPermission(\n  val id: Long,\n  val isBlacklisted: Boolean\n)\n\ninterface SupplierService {\n  suspend fun getSupplierPermission(supplierId: Long): SupplierPermission?\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/ObjectMapperConfig.kt",
    "content": "package stove.micronaut.example.infrastructure\n\nimport com.fasterxml.jackson.annotation.JsonInclude\nimport com.fasterxml.jackson.databind.DeserializationFeature\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.databind.module.SimpleModule\nimport com.fasterxml.jackson.module.kotlin.KotlinModule\nimport io.micronaut.context.annotation.Bean\nimport io.micronaut.context.annotation.Factory\n\n@Factory\nclass ObjectMapperConfig {\n  companion object {\n    fun createObjectMapperWithDefaults(): ObjectMapper {\n      val isoInstantModule = SimpleModule()\n      return ObjectMapper()\n        .registerModule(KotlinModule.Builder().build())\n        .registerModule(isoInstantModule)\n        .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)\n        .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n    }\n  }\n\n  @Bean\n  fun objectMapper(): ObjectMapper = createObjectMapperWithDefaults()\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/api/ProductController.kt",
    "content": "package stove.micronaut.example.infrastructure.api\n\nimport io.micronaut.http.annotation.*\nimport stove.micronaut.example.application.domain.Product\nimport stove.micronaut.example.application.services.ProductService\nimport stove.micronaut.example.infrastructure.api.model.request.CreateProductRequest\n\n@Controller(\"/products\")\nclass ProductController(\n  private val productService: ProductService\n) {\n  @Get(\"/index\")\n  fun get(\n    @QueryValue keyword: String = \"default\"\n  ): String = \"Hi from Stove framework with $keyword\"\n\n  @Post(\"/create\")\n  suspend fun createProduct(\n    @Body request: CreateProductRequest\n  ): Product = productService.createProduct(\n    id = request.id,\n    productName = request.name,\n    supplierId = request.supplierId\n  )\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/api/model/request/CreateProductRequest.kt",
    "content": "package stove.micronaut.example.infrastructure.api.model.request\n\nimport io.micronaut.serde.annotation.Serdeable\n\n@Serdeable\ndata class CreateProductRequest(\n  val id: String,\n  val name: String,\n  val supplierId: Long\n)\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/http/SupplierHttpService.kt",
    "content": "package stove.micronaut.example.infrastructure.http\n\nimport io.micronaut.http.annotation.Get\nimport io.micronaut.http.client.annotation.Client\nimport io.micronaut.websocket.exceptions.WebSocketClientException\nimport jakarta.inject.Singleton\nimport stove.micronaut.example.application.services.SupplierPermission\nimport stove.micronaut.example.application.services.SupplierService\n\n@Singleton\nclass SupplierHttpService(\n  private val supplierHttpClient: SupplierHttpClient\n) : SupplierService {\n  override suspend fun getSupplierPermission(supplierId: Long): SupplierPermission? = try {\n    val response = supplierHttpClient.getSupplierPermission(supplierId)\n    println(\"API Response: $response\")\n    response\n  } catch (e: WebSocketClientException) {\n    println(\"Error fetching supplier permission: ${e.message}\")\n    null\n  }\n}\n\n@Client(id = \"lookup-api\")\ninterface SupplierHttpClient {\n  @Get(\"/v2/suppliers/{supplierId}?storeFrontId=1\")\n  suspend fun getSupplierPermission(supplierId: Long): SupplierPermission\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/persistence/ProductJdbcRepository.kt",
    "content": "package stove.micronaut.example.infrastructure.persistence\n\nimport io.r2dbc.spi.ConnectionFactory\nimport jakarta.inject.Singleton\nimport kotlinx.coroutines.reactive.awaitFirst\nimport kotlinx.coroutines.reactive.awaitFirstOrNull\nimport reactor.core.publisher.Flux\nimport reactor.core.publisher.Mono\nimport stove.micronaut.example.application.domain.Product\nimport stove.micronaut.example.application.repository.ProductRepository\nimport java.util.*\n\n@Singleton\nclass ProductJdbcRepository(\n  private val connectionFactory: ConnectionFactory\n) : ProductRepository {\n  override suspend fun save(product: Product): Product {\n    Mono\n      .from(connectionFactory.create())\n      .flatMap { connection ->\n        Mono\n          .from(\n            connection\n              .createStatement(\n                \"\"\"\n            INSERT INTO products (id, name, supplier_id, is_blacklist, created_date) \n            VALUES ($1, $2, $3, $4, $5)\n            \"\"\"\n              ).bind(INDEX_ID, product.id)\n              .bind(INDEX_NAME, product.name)\n              .bind(INDEX_SUPPLIER_ID, product.supplierId)\n              .bind(INDEX_IS_BLACKLIST, product.isBlacklist)\n              .bind(INDEX_CREATED_DATE, product.createdDate)\n              .execute()\n          ).doFinally { connection.close() }\n      }.flatMap { result -> Mono.from(result.rowsUpdated) }\n      .awaitFirst()\n\n    return product\n  }\n\n  override suspend fun findById(id: Long): Product? =\n    Mono\n      .from(connectionFactory.create())\n      .flatMapMany { connection ->\n        Flux\n          .from(\n            connection\n              .createStatement(\"SELECT * FROM products WHERE id = $1\")\n              .bind(INDEX_ID, id.toString())\n              .execute()\n          ).flatMap { result ->\n            result.map { row, _ ->\n              Product(\n                id = row.get(\"id\", String::class.java)!!,\n                name = row.get(\"name\", String::class.java)!!,\n                supplierId = row.get(\"supplier_id\", Long::class.java)!!,\n                isBlacklist = row.get(\"is_blacklist\", Boolean::class.java)!!,\n                createdDate = row.get(\"created_date\", Date::class.java)!!\n              )\n            }\n          }.doFinally { connection.close() }\n      }.next()\n      .awaitFirstOrNull()\n\n  companion object {\n    private const val INDEX_ID = 0\n    private const val INDEX_NAME = 1\n    private const val INDEX_SUPPLIER_ID = 2\n    private const val INDEX_IS_BLACKLIST = 3\n    private const val INDEX_CREATED_DATE = 4\n  }\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/kotlin/stove/micronaut/example/infrastructure/postgres/PostgresConfiguration.kt",
    "content": "package stove.micronaut.example.infrastructure.postgres\n\nimport io.micronaut.context.annotation.Factory\nimport io.r2dbc.spi.ConnectionFactory\nimport jakarta.inject.Singleton\n\n@Factory\nclass PostgresConfiguration {\n  @Singleton\n  fun connectionFactory(connectionFactory: ConnectionFactory): ConnectionFactory = connectionFactory\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/main/resources/application.yml",
    "content": "micronaut:\n  application:\n    name: micronaut-example\n  server:\n    port: 8080\n  http:\n    services:\n      lookup-api:\n        url: http://localhost:7079\n        connect-timeout: 2s\n        read-timeout: 22s\n\nmicrometer:\n  metrics:\n    enabled: true\n    common-tags:\n      application: \"micronaut-example\"\n\nr2dbc:\n  datasources:\n    default:\n      url: r2dbc:postgresql://localhost:5432/stove\n      username: postgres\n      password: postgres\n\n"
  },
  {
    "path": "examples/micronaut-example/src/main/resources/logback.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <!-- encoders are assigned the type\n             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->\n        <encoder>\n            <pattern>%cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"info\">\n        <appender-ref ref=\"STDOUT\" />\n    </root>\n</configuration>\n"
  },
  {
    "path": "examples/micronaut-example/src/test/kotlin/com/stove/micronaut/example/e2e/CreateProductsTableMigration.kt",
    "content": "package com.stove.micronaut.example.e2e\n\nimport com.trendyol.stove.postgres.PostgresSqlMigrationContext\nimport com.trendyol.stove.postgres.PostgresqlMigration\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\nclass CreateProductsTableMigration : PostgresqlMigration {\n  private val logger: Logger = LoggerFactory.getLogger(CreateProductsTableMigration::class.java)\n\n  override val order: Int = 1\n\n  override suspend fun execute(connection: PostgresSqlMigrationContext) {\n    logger.info(\"Creating products table\")\n    connection.operations.execute(\n      \"\"\"\n      DROP TABLE IF EXISTS products;\n      CREATE TABLE IF NOT EXISTS products (\n        id VARCHAR(255) PRIMARY KEY,\n        name VARCHAR(255) NOT NULL,\n        supplier_id BIGINT NOT NULL,\n        is_blacklist BOOLEAN NOT NULL DEFAULT false,\n        created_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n      );\n      \"\"\".trimIndent()\n    )\n    logger.info(\"Products table created\")\n  }\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/test/kotlin/com/stove/micronaut/example/e2e/ProductControllerTest.kt",
    "content": "package com.stove.micronaut.example.e2e\n\nimport arrow.core.some\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.shouldContain\nimport io.r2dbc.spi.ConnectionFactory\nimport stove.micronaut.example.application.domain.Product\nimport stove.micronaut.example.application.services.SupplierPermission\nimport stove.micronaut.example.infrastructure.api.model.request.CreateProductRequest\nimport java.util.*\n\nclass ProductControllerTest :\n  FunSpec({\n\n    test(\"index should be reachable\") {\n      stove {\n        http {\n          get<String>(\"/products/index\", queryParams = mapOf(\"keyword\" to \"index\")) { actual ->\n            actual shouldContain \"Hi from Stove framework with index\"\n            println(actual)\n          }\n        }\n      }\n    }\n\n    test(\"should save product to PostgreSQL when product creation request is sent\") {\n      val id = UUID.randomUUID().toString()\n      val request = CreateProductRequest(id = id, name = \"product name\", supplierId = 120688)\n      val supplierMock = SupplierPermission(id = 120688, isBlacklisted = false)\n\n      stove {\n\n        wiremock {\n          mockGet(\n            \"/v2/suppliers/${supplierMock.id}?storeFrontId=1\",\n            statusCode = 200,\n            responseBody = supplierMock.some()\n          )\n        }\n        http {\n          postAndExpectJson<Product>(\"/products/create\", body = request.some()) { actual ->\n            actual.supplierId shouldBe 120688\n            actual.name shouldBe \"product name\"\n          }\n        }\n        postgresql {\n          shouldQuery<Product>(\n            \"SELECT * FROM products WHERE id = '${request.id}'\",\n            mapper = { row ->\n              Product(\n                id = row.string(\"id\"),\n                name = row.string(\"name\"),\n                supplierId = row.long(\"supplier_id\"),\n                isBlacklist = row.boolean(\"is_blacklist\"),\n                createdDate = Date(row.sqlTimestamp(\"created_date\").time)\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first().name shouldBe request.name\n            products.first().id shouldBe request.id\n            products.first().supplierId shouldBe request.supplierId\n            products.first().isBlacklist shouldBe false\n          }\n        }\n      }\n    }\n\n    test(\"a bean from application should be reachable\") {\n      stove {\n        using<ConnectionFactory> {\n          this shouldNotBe null\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/micronaut-example/src/test/kotlin/com/stove/micronaut/example/e2e/StoveConfig.kt",
    "content": "package com.stove.micronaut.example.e2e\n\nimport com.trendyol.stove.dashboard.DashboardSystemOptions\nimport com.trendyol.stove.dashboard.dashboard\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.micronaut.*\nimport com.trendyol.stove.postgres.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.tracing.tracing\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.slf4j.*\nimport stove.micronaut.example.run as runMicronautApp\n\nclass StoveConfig : AbstractProjectConfig() {\n  private val appPort = PortFinder.findAvailablePort()\n\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val logger: Logger = LoggerFactory.getLogger(\"WireMockMonitor\")\n\n  @Suppress(\"LongMethod\")\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:$appPort\"\n          )\n        }\n        postgresql {\n          PostgresqlOptions(\n            databaseName = \"stove\",\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"r2dbc.datasources.default.url=r2dbc:postgresql://${cfg.host}:${cfg.port}/stove\",\n                \"r2dbc.datasources.default.username=${cfg.username}\",\n                \"r2dbc.datasources.default.password=${cfg.password}\"\n              )\n            }\n          ).migrations {\n            register<CreateProductsTableMigration>()\n          }\n        }\n        bridge()\n        tracing { enableSpanReceiver() }\n        dashboard { DashboardSystemOptions(appName = \"micronaut-example\") }\n        wiremock {\n          WireMockSystemOptions(\n            port = 0,\n            removeStubAfterRequestMatched = true,\n            afterRequest = { e, _ ->\n              logger.info(e.request.toString())\n            },\n            configureExposedConfiguration = { cfg ->\n              listOf(\"micronaut.http.services.lookup-api.url=${cfg.baseUrl}\")\n            }\n          )\n        }\n        micronaut(\n          runner = { parameters ->\n            runMicronautApp(parameters) {\n            }\n          },\n          withParameters = listOf(\n            \"micronaut.server.port=$appPort\",\n            \"logging.level.root=info\",\n            \"logging.level.org.micronaut.web=info\"\n          )\n        )\n      }.run()\n  }\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n"
  },
  {
    "path": "examples/micronaut-example/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.stove.micronaut.example.e2e.StoveConfig\n"
  },
  {
    "path": "examples/micronaut-example/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "examples/quarkus-example/build.gradle.kts",
    "content": "import com.trendyol.stove.gradle.stoveTracing\n\n@DisableCachingByDefault(because = \"Creates an empty classes directory required by Quarkus code generation\")\nabstract class EnsureDirectoryTask : DefaultTask() {\n  @get:OutputDirectory\n  abstract val outputDirectory: DirectoryProperty\n\n  @TaskAction\n  fun createDirectory() {\n    outputDirectory.get().asFile.mkdirs()\n  }\n}\n\nplugins {\n  alias(libs.plugins.quarkus)\n  alias(libs.plugins.allopen)\n  idea\n  application\n}\n\ndependencies {\n  implementation(enforcedPlatform(libs.quarkus))\n  implementation(libs.quarkus.rest)\n  implementation(libs.quarkus.rest.jackson)\n  implementation(libs.quarkus.arc)\n  implementation(libs.quarkus.kotlin)\n  implementation(libs.quarkus.agroal)\n  implementation(libs.quarkus.jdbc.postgresql)\n  implementation(libs.quarkus.flyway)\n  implementation(libs.quarkus.messaging.kafka)\n  implementation(libs.jackson.kotlin)\n  implementation(libs.opentelemetry.instrumentation.annotations)\n}\n\ndependencies {\n  testImplementation(projects.stove.testExtensions.stoveExtensionsKotest)\n  testImplementation(projects.stove.lib.stoveHttp)\n  testImplementation(projects.stove.lib.stoveWiremock)\n  testImplementation(projects.stove.lib.stovePostgres)\n  testImplementation(projects.stove.lib.stoveKafka)\n  testImplementation(projects.stove.lib.stoveDashboard)\n  testImplementation(projects.stove.lib.stoveTracing)\n  testImplementation(projects.stove.starters.quarkus.stoveQuarkus)\n}\n\nallOpen {\n  annotation(\"jakarta.ws.rs.Path\")\n  annotation(\"jakarta.enterprise.context.ApplicationScoped\")\n}\n\napplication {\n  mainClass.set(\"stove.quarkus.example.QuarkusMainApp\")\n}\n\nval ensureJavaMainClassesDir by tasks.registering(EnsureDirectoryTask::class) {\n  outputDirectory.set(layout.buildDirectory.dir(\"classes/java/main\"))\n}\n\ntasks.matching { it.name == \"quarkusGenerateCode\" || it.name == \"quarkusGenerateCodeTests\" }.configureEach {\n  dependsOn(ensureJavaMainClassesDir)\n}\n\ntasks.named(\"test\") {\n  dependsOn(\"quarkusBuild\")\n}\n\nkotlin {\n  compilerOptions {\n    javaParameters = true\n  }\n}\n\nstoveTracing {\n  serviceName = \"quarkus-example\"\n  otelAgentVersion = libs.versions.opentelemetry.instrumentation.get()\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/QuarkusMainApp.kt",
    "content": "package stove.quarkus.example\n\nimport io.quarkus.runtime.Quarkus\nimport io.quarkus.runtime.annotations.QuarkusMain\n\n@QuarkusMain\nobject QuarkusMainApp {\n  @JvmStatic\n  fun main(args: Array<String>) {\n    Quarkus.run(*args)\n  }\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/StoveStartupSignal.kt",
    "content": "package stove.quarkus.example\n\nimport io.quarkus.runtime.ShutdownEvent\nimport io.quarkus.runtime.StartupEvent\nimport jakarta.enterprise.context.ApplicationScoped\nimport jakarta.enterprise.event.Observes\n\n@Suppress(\"unused\")\n@ApplicationScoped\nclass StoveStartupSignal {\n  fun onStart(\n    @Observes event: StartupEvent\n  ) {\n    System.setProperty(READY_PROPERTY, \"true\")\n  }\n\n  fun onStop(\n    @Observes event: ShutdownEvent\n  ) {\n    System.clearProperty(READY_PROPERTY)\n  }\n\n  companion object {\n    const val READY_PROPERTY: String = \"stove.quarkus.ready\"\n  }\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/api/ProductResource.kt",
    "content": "package stove.quarkus.example.api\n\nimport jakarta.ws.rs.*\nimport jakarta.ws.rs.core.MediaType\nimport stove.quarkus.example.application.ProductCreateRequest\nimport stove.quarkus.example.application.ProductCreator\n\n@Path(\"/api\")\n@Produces(MediaType.TEXT_PLAIN)\nclass ProductResource(\n  private val productCreator: ProductCreator\n) {\n  @GET\n  @Path(\"/index\")\n  fun index(\n    @QueryParam(\"keyword\") keyword: String?\n  ): String = \"Hi from Stove Quarkus example with $keyword\"\n\n  @POST\n  @Path(\"/product/create\")\n  @Consumes(MediaType.APPLICATION_JSON)\n  fun createProduct(request: ProductCreateRequest): String = productCreator.create(request)\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/application/Models.kt",
    "content": "package stove.quarkus.example.application\n\ndata class ProductCreateRequest(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n\ndata class ProductCreatedEvent(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n\ndata class CreateProductCommand(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n\ndata class SupplierPermission(\n  val supplierId: Long,\n  val isAllowed: Boolean\n)\n\nfun CreateProductCommand.toCreateRequest(): ProductCreateRequest = ProductCreateRequest(\n  id = id,\n  name = name,\n  supplierId = supplierId\n)\n\nfun ProductCreateRequest.toProductCreatedEvent(): ProductCreatedEvent = ProductCreatedEvent(\n  id = id,\n  name = name,\n  supplierId = supplierId\n)\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/application/ProductCreator.kt",
    "content": "package stove.quarkus.example.application\n\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport jakarta.enterprise.context.ApplicationScoped\nimport stove.quarkus.example.infrastructure.http.SupplierHttpService\nimport stove.quarkus.example.infrastructure.kafka.ProductEventPublisher\nimport stove.quarkus.example.infrastructure.postgres.ProductRepository\n\n@ApplicationScoped\nclass ProductCreator(\n  private val supplierHttpService: SupplierHttpService,\n  private val productRepository: ProductRepository,\n  private val productEventPublisher: ProductEventPublisher\n) {\n  @WithSpan(\"ProductCreator.create\")\n  fun create(request: ProductCreateRequest): String {\n    val supplierPermission = supplierHttpService.getSupplierPermission(request.supplierId)\n    if (!supplierPermission.isAllowed) {\n      return \"Supplier with the given id(${request.supplierId}) is not allowed for product creation\"\n    }\n\n    productRepository.save(request)\n    productEventPublisher.publish(request.toProductCreatedEvent())\n    return \"OK\"\n  }\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/infrastructure/http/SupplierHttpService.kt",
    "content": "package stove.quarkus.example.infrastructure.http\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport jakarta.enterprise.context.ApplicationScoped\nimport org.eclipse.microprofile.config.inject.ConfigProperty\nimport stove.quarkus.example.application.SupplierPermission\nimport java.net.URI\nimport java.net.http.HttpClient\nimport java.net.http.HttpRequest\nimport java.net.http.HttpResponse\nimport java.time.Duration\n\n@ApplicationScoped\nclass SupplierHttpService(\n  private val objectMapper: ObjectMapper\n) {\n  @ConfigProperty(name = \"clients.supplier.url\")\n  lateinit var supplierBaseUrl: String\n\n  private val httpClient: HttpClient = HttpClient\n    .newBuilder()\n    .connectTimeout(Duration.ofSeconds(SUPPLIER_CONNECT_TIMEOUT_SECONDS))\n    .build()\n\n  @WithSpan(\"SupplierHttpService.getSupplierPermission\")\n  fun getSupplierPermission(id: Long): SupplierPermission {\n    val request = HttpRequest\n      .newBuilder(URI.create(\"$supplierBaseUrl/suppliers/$id/allowed\"))\n      .header(\"Accept\", \"application/json\")\n      .GET()\n      .build()\n\n    val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())\n    check(response.statusCode() == HTTP_OK_STATUS) {\n      \"Supplier service returned ${response.statusCode()} for supplier $id\"\n    }\n\n    return objectMapper.readValue(response.body(), SupplierPermission::class.java)\n  }\n}\n\nprivate const val SUPPLIER_CONNECT_TIMEOUT_SECONDS = 2L\nprivate const val HTTP_OK_STATUS = 200\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/infrastructure/kafka/CustomProducerInterceptor.kt",
    "content": "package stove.quarkus.example.infrastructure.kafka\n\nimport org.apache.kafka.clients.producer.ProducerInterceptor\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.apache.kafka.clients.producer.RecordMetadata\n\nconst val USER_EMAIL_HEADER = \"X-UserEmail\"\nconst val DEFAULT_USER_EMAIL_HEADER_VALUE = \"stove@trendyol.com\"\n\nclass CustomProducerInterceptor : ProducerInterceptor<String, Any> {\n  override fun onSend(record: ProducerRecord<String, Any>): ProducerRecord<String, Any> {\n    if (record.headers().lastHeader(USER_EMAIL_HEADER) == null) {\n      record.headers().add(USER_EMAIL_HEADER, DEFAULT_USER_EMAIL_HEADER_VALUE.toByteArray())\n    }\n    return record\n  }\n\n  override fun configure(configs: MutableMap<String, *>?) = Unit\n\n  override fun onAcknowledgement(\n    metadata: RecordMetadata?,\n    exception: Exception?\n  ) = Unit\n\n  override fun close() = Unit\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/infrastructure/kafka/KafkaClientConfiguration.kt",
    "content": "package stove.quarkus.example.infrastructure.kafka\n\nimport io.smallrye.common.annotation.Identifier\nimport jakarta.enterprise.context.ApplicationScoped\nimport jakarta.enterprise.inject.Produces\nimport org.eclipse.microprofile.config.inject.ConfigProperty\n\n@ApplicationScoped\nclass KafkaClientConfiguration {\n  @ConfigProperty(name = \"app.kafka.bridge-interceptor-class\", defaultValue = \"\")\n  lateinit var bridgeInterceptorClass: String\n\n  @Produces\n  @Identifier(\"product-create\")\n  fun productCreateConfiguration(): Map<String, Any> = buildMap {\n    put(\"auto.offset.reset\", \"earliest\")\n    put(\"allow.auto.create.topics\", true)\n    bridgeInterceptorClass.takeIf { it.isNotBlank() }?.let {\n      put(\"interceptor.classes\", it)\n    }\n  }\n\n  @Produces\n  @Identifier(\"product-created\")\n  fun productCreatedConfiguration(): Map<String, Any> = buildMap {\n    put(\"acks\", \"1\")\n    put(\"interceptor.classes\", producerInterceptors())\n  }\n\n  private fun producerInterceptors(): String = buildList {\n    add(CustomProducerInterceptor::class.java.name)\n    bridgeInterceptorClass.takeIf { it.isNotBlank() }?.let(::add)\n  }.joinToString(\",\")\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/infrastructure/kafka/KafkaSerde.kt",
    "content": "package stove.quarkus.example.infrastructure.kafka\n\nimport io.quarkus.kafka.client.serialization.ObjectMapperDeserializer\nimport io.quarkus.kafka.client.serialization.ObjectMapperSerializer\nimport stove.quarkus.example.application.CreateProductCommand\nimport stove.quarkus.example.application.ProductCreatedEvent\n\nclass CreateProductCommandDeserializer : ObjectMapperDeserializer<CreateProductCommand>(CreateProductCommand::class.java)\n\nclass ProductCreatedEventSerializer : ObjectMapperSerializer<ProductCreatedEvent>()\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/infrastructure/kafka/ProductCommandConsumer.kt",
    "content": "package stove.quarkus.example.infrastructure.kafka\n\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport io.smallrye.common.annotation.Blocking\nimport jakarta.enterprise.context.ApplicationScoped\nimport org.eclipse.microprofile.reactive.messaging.Incoming\nimport stove.quarkus.example.application.CreateProductCommand\nimport stove.quarkus.example.application.ProductCreator\nimport stove.quarkus.example.application.toCreateRequest\n\n@ApplicationScoped\nclass ProductCommandConsumer(\n  private val productCreator: ProductCreator\n) {\n  @Incoming(\"product-create\")\n  @Blocking\n  @WithSpan(\"ProductCommandConsumer.consume\")\n  fun consume(command: CreateProductCommand) {\n    productCreator.create(command.toCreateRequest())\n  }\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/infrastructure/kafka/ProductEventPublisher.kt",
    "content": "package stove.quarkus.example.infrastructure.kafka\n\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport io.smallrye.reactive.messaging.MutinyEmitter\nimport io.smallrye.reactive.messaging.kafka.KafkaRecord\nimport jakarta.enterprise.context.ApplicationScoped\nimport org.eclipse.microprofile.reactive.messaging.Channel\nimport stove.quarkus.example.application.ProductCreatedEvent\n\n@ApplicationScoped\nclass ProductEventPublisher(\n  @param:Channel(\"product-created\")\n  private val emitter: MutinyEmitter<ProductCreatedEvent>\n) {\n  @WithSpan(\"ProductEventPublisher.publish\")\n  fun publish(event: ProductCreatedEvent) {\n    emitter.sendMessageAndAwait(\n      KafkaRecord\n        .of(event.id.toString(), event)\n        .withHeader(\"X-EventType\", ProductCreatedEvent::class.simpleName!!)\n    )\n  }\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/main/kotlin/stove/quarkus/example/infrastructure/postgres/ProductRepository.kt",
    "content": "package stove.quarkus.example.infrastructure.postgres\n\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport jakarta.enterprise.context.ApplicationScoped\nimport stove.quarkus.example.application.ProductCreateRequest\nimport javax.sql.DataSource\n\n@ApplicationScoped\nclass ProductRepository(\n  private val dataSource: DataSource\n) {\n  @WithSpan(\"ProductRepository.save\")\n  fun save(request: ProductCreateRequest) {\n    dataSource.connection.use { connection ->\n      connection\n        .prepareStatement(\n          \"\"\"\n          INSERT INTO products (id, name, supplier_id)\n          VALUES (?, ?, ?)\n          \"\"\".trimIndent()\n        ).use { statement ->\n          statement.setLong(ID_PARAMETER_INDEX, request.id)\n          statement.setString(NAME_PARAMETER_INDEX, request.name)\n          statement.setLong(SUPPLIER_ID_PARAMETER_INDEX, request.supplierId)\n          statement.executeUpdate()\n        }\n    }\n  }\n}\n\nprivate const val ID_PARAMETER_INDEX = 1\nprivate const val NAME_PARAMETER_INDEX = 2\nprivate const val SUPPLIER_ID_PARAMETER_INDEX = 3\n"
  },
  {
    "path": "examples/quarkus-example/src/main/resources/application.properties",
    "content": "quarkus.console.enabled=false\nquarkus.class-loading.parent-first-artifacts=org.apache.kafka:kafka-clients\nquarkus.http.port=8080\nquarkus.datasource.db-kind=postgresql\nquarkus.datasource.devservices.enabled=false\nquarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/stove\nquarkus.datasource.username=postgres\nquarkus.datasource.password=postgres\nquarkus.flyway.migrate-at-start=true\n\nkafka.bootstrap.servers=localhost:9092\napp.kafka.bridge-interceptor-class=\nmp.messaging.incoming.product-create.connector=smallrye-kafka\nmp.messaging.incoming.product-create.topic=trendyol.stove.service.product.create.0\nmp.messaging.incoming.product-create.group.id=stove-quarkus-example\nmp.messaging.incoming.product-create.value.deserializer=stove.quarkus.example.infrastructure.kafka.CreateProductCommandDeserializer\nmp.messaging.incoming.product-create.kafka-configuration=product-create\nmp.messaging.outgoing.product-created.connector=smallrye-kafka\nmp.messaging.outgoing.product-created.topic=trendyol.stove.service.product.created.0\nmp.messaging.outgoing.product-created.value.serializer=stove.quarkus.example.infrastructure.kafka.ProductCreatedEventSerializer\nmp.messaging.outgoing.product-created.kafka-configuration=product-created\n\nclients.supplier.url=http://localhost:7078\n"
  },
  {
    "path": "examples/quarkus-example/src/main/resources/db/migration/V1__create_products.sql",
    "content": "CREATE TABLE IF NOT EXISTS products (\n  id BIGINT PRIMARY KEY,\n  name VARCHAR(255) NOT NULL,\n  supplier_id BIGINT NOT NULL\n);\n"
  },
  {
    "path": "examples/quarkus-example/src/test/kotlin/com/stove/quarkus/example/e2e/ExampleTest.kt",
    "content": "package com.stove.quarkus.example.e2e\n\nimport arrow.core.some\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.tracing.tracing\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.delay\nimport stove.quarkus.example.application.CreateProductCommand\nimport stove.quarkus.example.application.ProductCreateRequest\nimport stove.quarkus.example.application.ProductCreatedEvent\nimport stove.quarkus.example.application.SupplierPermission\nimport stove.quarkus.example.infrastructure.kafka.DEFAULT_USER_EMAIL_HEADER_VALUE\nimport stove.quarkus.example.infrastructure.kafka.USER_EMAIL_HEADER\nimport kotlin.time.Duration.Companion.seconds\n\nclass ExampleTest :\n  FunSpec({\n    val textPlainHeaders = mapOf(\"Accept\" to \"text/plain\")\n\n    data class PersistedProduct(\n      val id: Long,\n      val name: String,\n      val supplierId: Long\n    )\n\n    test(\"index should be reachable\") {\n      stove {\n        http {\n          get<String>(\n            \"/api/index\",\n            queryParams = mapOf(\"keyword\" to testCase.name.name),\n            headers = textPlainHeaders\n          ) { actual ->\n            actual shouldContain \"Hi from Stove Quarkus example with ${testCase.name.name}\"\n          }\n        }\n      }\n    }\n\n    test(\"should create new product when send product create request from api for the allowed supplier\") {\n      stove {\n        val productCreateRequest = ProductCreateRequest(1L, name = \"product name\", supplierId = 99L)\n        val supplierPermission = SupplierPermission(productCreateRequest.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${productCreateRequest.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        http {\n          postAndExpectBody<String>(\n            \"/api/product/create\",\n            body = productCreateRequest.some(),\n            headers = textPlainHeaders\n          ) { actual ->\n            actual.status shouldBe 200\n            actual.body() shouldBe \"OK\"\n          }\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent>(5.seconds) {\n            actual.id == productCreateRequest.id &&\n              actual.name == productCreateRequest.name &&\n              actual.supplierId == productCreateRequest.supplierId &&\n              metadata.headers[USER_EMAIL_HEADER] == DEFAULT_USER_EMAIL_HEADER_VALUE\n          }\n        }\n\n        postgresql {\n          shouldQuery<PersistedProduct>(\n            \"SELECT * FROM products WHERE id = ${productCreateRequest.id}\",\n            mapper = { row ->\n              PersistedProduct(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.long(\"supplier_id\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first() shouldBe PersistedProduct(\n              id = productCreateRequest.id,\n              name = productCreateRequest.name,\n              supplierId = productCreateRequest.supplierId\n            )\n          }\n        }\n      }\n    }\n\n    test(\"should return validation message when supplier is not allowed\") {\n      stove {\n        val productCreateRequest = ProductCreateRequest(2L, name = \"product name\", supplierId = 98L)\n        val supplierPermission = SupplierPermission(productCreateRequest.supplierId, isAllowed = false)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${productCreateRequest.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        http {\n          postAndExpectBody<String>(\n            \"/api/product/create\",\n            body = productCreateRequest.some(),\n            headers = textPlainHeaders\n          ) { actual ->\n            actual.status shouldBe 200\n            actual.body() shouldBe\n              \"Supplier with the given id(${productCreateRequest.supplierId}) is not allowed for product creation\"\n          }\n        }\n      }\n    }\n\n    test(\"should create new product when send product create event for the allowed supplier\") {\n      stove {\n        val createProductCommand = CreateProductCommand(4L, name = \"product name\", supplierId = 96L)\n        val supplierPermission = SupplierPermission(createProductCommand.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${createProductCommand.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        kafka {\n          publish(\"trendyol.stove.service.product.create.0\", createProductCommand)\n          shouldBeConsumed<CreateProductCommand>(10.seconds) {\n            actual.id == createProductCommand.id &&\n              actual.name == createProductCommand.name &&\n              actual.supplierId == createProductCommand.supplierId\n          }\n        }\n\n        postgresql {\n          shouldQuery<PersistedProduct>(\n            \"SELECT * FROM products WHERE id = ${createProductCommand.id}\",\n            mapper = { row ->\n              PersistedProduct(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.long(\"supplier_id\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first() shouldBe PersistedProduct(\n              id = createProductCommand.id,\n              name = createProductCommand.name,\n              supplierId = createProductCommand.supplierId\n            )\n          }\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent>(10.seconds) {\n            actual.id == createProductCommand.id &&\n              actual.name == createProductCommand.name &&\n              actual.supplierId == createProductCommand.supplierId\n          }\n        }\n      }\n    }\n\n    test(\"tracing should capture quarkus request flow\") {\n      stove {\n        val productCreateRequest = ProductCreateRequest(5L, name = \"traced product\", supplierId = 95L)\n        val supplierPermission = SupplierPermission(productCreateRequest.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${productCreateRequest.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        http {\n          postAndExpectBody<String>(\n            \"/api/product/create\",\n            body = productCreateRequest.some(),\n            headers = textPlainHeaders\n          ) { actual ->\n            actual.status shouldBe 200\n            actual.body() shouldBe \"OK\"\n          }\n        }\n\n        tracing {\n          waitForExpectedSpans(\n            expectedOperationNames = listOf(\n              \"ProductCreator.create\",\n              \"SupplierHttpService.getSupplierPermission\",\n              \"ProductEventPublisher.publish\"\n            ),\n            timeoutMs = 15_000\n          )\n          shouldContainSpan(\"ProductCreator.create\")\n          shouldContainSpan(\"SupplierHttpService.getSupplierPermission\")\n          shouldContainSpan(\"ProductEventPublisher.publish\")\n          spanCountShouldBeAtLeast(4)\n        }\n      }\n    }\n  })\n\nprivate suspend fun com.trendyol.stove.tracing.TracingValidationScope.waitForExpectedSpans(\n  expectedOperationNames: List<String>,\n  timeoutMs: Long\n) {\n  val deadline = System.currentTimeMillis() + timeoutMs\n\n  while (System.currentTimeMillis() < deadline) {\n    val spans = collector.getTrace(traceId)\n    val operationNames = spans.map { it.operationName }\n    val allExpectedSpansArePresent = expectedOperationNames.all { expectedOperationName ->\n      operationNames.any { operationName -> operationName.contains(expectedOperationName) }\n    }\n\n    if (allExpectedSpansArePresent) {\n      return\n    }\n\n    delay(250)\n  }\n\n  error(\n    \"Timeout waiting for spans: ${expectedOperationNames.joinToString()} in ${collector.getTrace(traceId).map { it.operationName }}\"\n  )\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/test/kotlin/com/stove/quarkus/example/e2e/StoveConfig.kt",
    "content": "package com.stove.quarkus.example.e2e\n\nimport com.trendyol.stove.dashboard.DashboardSystemOptions\nimport com.trendyol.stove.dashboard.dashboard\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.postgres.*\nimport com.trendyol.stove.quarkus.quarkus\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.tracing.tracing\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.apache.kafka.clients.admin.NewTopic\nimport stove.quarkus.example.QuarkusMainApp\n\nclass StoveConfig : AbstractProjectConfig() {\n  companion object {\n    private val appPort = PortFinder.findAvailablePort()\n  }\n\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  @Suppress(\"LongMethod\")\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        tracing { enableSpanReceiver() }\n        dashboard { DashboardSystemOptions(appName = \"quarkus-example\") }\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:$appPort\"\n          )\n        }\n        postgresql {\n          PostgresqlOptions(\n            databaseName = \"stove\",\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"quarkus.datasource.jdbc.url=${cfg.jdbcUrl}\",\n                \"quarkus.datasource.username=${cfg.username}\",\n                \"quarkus.datasource.password=${cfg.password}\"\n              )\n            }\n          )\n        }\n        kafka {\n          KafkaSystemOptions(\n            serde = StoveSerde.jackson.anyByteArraySerde(),\n            containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n          ) {\n            listOf(\n              \"kafka.bootstrap.servers=${it.bootstrapServers}\",\n              \"app.kafka.bridge-interceptor-class=${it.interceptorClass}\"\n            )\n          }.migrations {\n            register<CreateQuarkusExampleTopicsMigration>()\n          }\n        }\n        wiremock {\n          WireMockSystemOptions(\n            port = 0,\n            removeStubAfterRequestMatched = true,\n            configureExposedConfiguration = { cfg ->\n              listOf(\"clients.supplier.url=${cfg.baseUrl}\")\n            }\n          )\n        }\n        quarkus(\n          runner = { params ->\n            QuarkusMainApp.main(params)\n          },\n          withParameters = listOf(\"quarkus.http.port=$appPort\")\n        )\n      }.run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n}\n\nclass CreateQuarkusExampleTopicsMigration : KafkaMigration {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: KafkaMigrationContext) {\n    connection.admin\n      .createTopics(\n        listOf(\n          NewTopic(\"trendyol.stove.service.product.create.0\", 1, 1),\n          NewTopic(\"trendyol.stove.service.product.created.0\", 1, 1)\n        )\n      ).all()\n      .get()\n  }\n}\n"
  },
  {
    "path": "examples/quarkus-example/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.stove.quarkus.example.e2e.StoveConfig\n"
  },
  {
    "path": "examples/quarkus-example/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "examples/spring-4x-example/build.gradle.kts",
    "content": "import com.trendyol.stove.gradle.stoveTracing\n\nplugins {\n  alias(libs.plugins.spring.plugin)\n  alias(libs.plugins.spring.boot.four)\n  idea\n  application\n}\n\ndependencies {\n  implementation(libs.spring.boot.four)\n  implementation(libs.spring.boot.four.autoconfigure)\n  implementation(libs.spring.boot.four.webflux)\n  implementation(libs.spring.boot.four.actuator)\n  annotationProcessor(libs.spring.boot.four.annotationProcessor)\n  implementation(libs.spring.boot.four.kafka)\n  implementation(libs.kotlinx.reactor)\n  implementation(libs.kotlinx.core)\n  implementation(libs.kotlinx.reactive)\n  implementation(libs.kotlinx.slf4j)\n\n  // OpenTelemetry instrumentation API for @WithSpan annotation\n  implementation(libs.opentelemetry.instrumentation.annotations)\n}\n\ndependencies {\n  testImplementation(projects.stove.testExtensions.stoveExtensionsKotest)\n  testImplementation(libs.jackson3.kotlin)\n  testImplementation(projects.stove.lib.stoveHttp)\n  testImplementation(projects.stove.lib.stoveWiremock)\n  testImplementation(projects.stove.lib.stoveTracing)\n  testImplementation(projects.stove.lib.stoveDashboard)\n  testImplementation(projects.stove.starters.spring.stoveSpring)\n  testImplementation(projects.stove.starters.spring.stoveSpringKafka)\n}\n\napplication { mainClass.set(\"stove.spring.example4x.ExampleAppkt\") }\n\n// ============================================================================\n// TRACING SETUP - OpenTelemetry Java Agent (buildSrc)\n// ============================================================================\nstoveTracing {\n  serviceName = \"spring-4x-example\"\n  otelAgentVersion = libs.versions.opentelemetry.instrumentation.get()\n}\n\ntasks.test {\n  testLogging {\n    events(\"passed\", \"skipped\", \"failed\", \"standardOut\", \"standardError\")\n    showStandardStreams = true\n  }\n}\n"
  },
  {
    "path": "examples/spring-4x-example/src/main/kotlin/stove/spring/example4x/ExampleApp.kt",
    "content": "package stove.spring.example4x\n\nimport org.springframework.boot.SpringApplication\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.runApplication\nimport org.springframework.context.ConfigurableApplicationContext\n\n@SpringBootApplication\nclass ExampleApp\n\nfun main(args: Array<String>) {\n  run(args)\n}\n\n/**\n * This is the point where spring application gets run.\n * run(args, init) method is the important point for the testing configuration.\n * init allows us to override any dependency from the testing side that is being time related or configuration related.\n * Spring itself opens this configuration higher order function to the outside.\n */\nfun run(\n  args: Array<String>,\n  init: SpringApplication.() -> Unit = {}\n): ConfigurableApplicationContext = runApplication<ExampleApp>(*args, init = init)\n"
  },
  {
    "path": "examples/spring-4x-example/src/main/kotlin/stove/spring/example4x/application/handlers/ProductCreator.kt",
    "content": "package stove.spring.example4x.application.handlers\n\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport org.springframework.stereotype.Service\nimport stove.spring.example4x.infrastructure.api.ProductCreateRequest\n\n@Service\nclass ProductCreator {\n  @WithSpan(\"ProductCreator.create\")\n  suspend fun create(request: ProductCreateRequest) {\n    // In a real application, this would persist the product\n    println(\"Creating product: ${request.name} with id ${request.id}\")\n  }\n}\n\ndata class ProductCreatedEvent(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n"
  },
  {
    "path": "examples/spring-4x-example/src/main/kotlin/stove/spring/example4x/infrastructure/api/ProductController.kt",
    "content": "package stove.spring.example4x.infrastructure.api\n\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport org.springframework.http.ResponseEntity\nimport org.springframework.web.bind.annotation.*\nimport stove.spring.example4x.application.handlers.*\nimport stove.spring.example4x.infrastructure.messaging.kafka.KafkaProducer\n\n@RestController\n@RequestMapping(\"/api\")\nclass ProductController(\n  private val productCreator: ProductCreator,\n  private val kafkaProducer: KafkaProducer\n) {\n  @GetMapping(\"/index\")\n  suspend fun index(\n    @RequestParam(required = false) keyword: String?\n  ): ResponseEntity<String> = ResponseEntity.ok(\"Hi from Stove framework with ${keyword ?: \"no keyword\"}\")\n\n  @WithSpan(\"ProductController.create\")\n  @PostMapping(\"/product/create\")\n  suspend fun create(\n    @RequestBody request: ProductCreateRequest\n  ): ResponseEntity<Any> {\n    productCreator.create(request)\n    kafkaProducer.send(ProductCreatedEvent(request.id, request.name, request.supplierId))\n    return ResponseEntity.ok().build()\n  }\n}\n\ndata class ProductCreateRequest(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n"
  },
  {
    "path": "examples/spring-4x-example/src/main/kotlin/stove/spring/example4x/infrastructure/messaging/kafka/KafkaConfiguration.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage stove.spring.example4x.infrastructure.messaging.kafka\n\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.*\nimport org.springframework.boot.context.properties.*\nimport org.springframework.context.annotation.*\nimport org.springframework.kafka.annotation.EnableKafka\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.RecordInterceptor\nimport org.springframework.kafka.support.serializer.*\n\n@Configuration\n@EnableKafka\n@EnableConfigurationProperties(KafkaProperties::class)\nclass KafkaConfiguration {\n  @Bean\n  fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, String>,\n    interceptor: RecordInterceptor<String, String>?\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.setConsumerFactory(consumerFactory)\n    interceptor?.let { factory.setRecordInterceptor(it) }\n    return factory\n  }\n\n  @Bean\n  @Suppress(\"MagicNumber\")\n  fun consumerFactory(\n    config: KafkaProperties\n  ): ConsumerFactory<String, String> = DefaultKafkaConsumerFactory(\n    mapOf(\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n      ConsumerConfig.GROUP_ID_CONFIG to config.groupId,\n      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to config.offset,\n      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,\n      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to ErrorHandlingDeserializer::class.java,\n      ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS to StringDeserializer::class.java,\n      ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to config.heartbeatInSeconds * 1000,\n      ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG to config.heartbeatInSeconds * 3000,\n      ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to config.heartbeatInSeconds * 3000\n    )\n  )\n\n  @Bean\n  fun kafkaTemplate(\n    config: KafkaProperties\n  ): KafkaTemplate<String, Any> = KafkaTemplate(\n    DefaultKafkaProducerFactory(\n      mapOf(\n        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java,\n        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JacksonJsonSerializer::class.java,\n        ProducerConfig.ACKS_CONFIG to config.acks\n      )\n    )\n  )\n}\n\n@ConfigurationProperties(prefix = \"kafka\")\ndata class KafkaProperties(\n  val bootstrapServers: String,\n  val groupId: String = \"spring-4x-example\",\n  val offset: String = \"earliest\",\n  val acks: String = \"1\",\n  val heartbeatInSeconds: Int = 3,\n  val topicPrefix: String = \"trendyol.stove.service\"\n)\n"
  },
  {
    "path": "examples/spring-4x-example/src/main/kotlin/stove/spring/example4x/infrastructure/messaging/kafka/KafkaProducer.kt",
    "content": "package stove.spring.example4x.infrastructure.messaging.kafka\n\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport kotlinx.coroutines.future.await\nimport org.springframework.kafka.core.KafkaTemplate\nimport org.springframework.stereotype.Component\nimport stove.spring.example4x.application.handlers.ProductCreatedEvent\n\n@Component\nclass KafkaProducer(\n  private val kafkaTemplate: KafkaTemplate<String, Any>,\n  private val kafkaProperties: KafkaProperties\n) {\n  @WithSpan(\"KafkaProducer.send\")\n  suspend fun send(event: ProductCreatedEvent) {\n    val topic = \"${kafkaProperties.topicPrefix}.productCreated.1\"\n    kafkaTemplate.send(topic, event.id.toString(), event).await()\n  }\n}\n"
  },
  {
    "path": "examples/spring-4x-example/src/main/kotlin/stove/spring/example4x/infrastructure/messaging/kafka/ProductCreateConsumer.kt",
    "content": "package stove.spring.example4x.infrastructure.messaging.kafka\n\nimport org.slf4j.*\nimport org.springframework.kafka.annotation.KafkaListener\nimport org.springframework.messaging.handler.annotation.*\nimport org.springframework.stereotype.Component\nimport stove.spring.example4x.application.handlers.ProductCreator\nimport stove.spring.example4x.infrastructure.api.ProductCreateRequest\nimport tools.jackson.databind.json.JsonMapper\n\n@Component\nclass ProductCreateConsumer(\n  private val productCreator: ProductCreator,\n  private val jsonMapper: JsonMapper\n) {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  @KafkaListener(topics = [\"trendyol.stove.service.product.create.0\"], groupId = \"\\${kafka.groupId}\")\n  suspend fun consume(\n    @Payload message: String,\n    @Header(\"X-UserEmail\", required = false) userEmail: String?\n  ) {\n    logger.info(\"Received message: $message with userEmail: $userEmail\")\n    val command = jsonMapper.readValue(message, CreateProductCommand::class.java)\n    productCreator.create(ProductCreateRequest(command.id, command.name, command.supplierId))\n  }\n}\n\n@Component\nclass ProductEventsConsumer {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  @KafkaListener(\n    topics = [\"trendyol.stove.service.productCreated.1\"],\n    groupId = \"\\${kafka.groupId}\",\n    containerFactory = \"kafkaListenerContainerFactory\"\n  )\n  fun consumeProductCreatedEvent(\n    @Payload message: String\n  ) {\n    logger.info(\"Received message: $message\")\n  }\n}\n\ndata class CreateProductCommand(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n"
  },
  {
    "path": "examples/spring-4x-example/src/main/resources/application.yml",
    "content": "spring:\n  application:\n    name: \"stove-spring-4x-example\"\n\nserver:\n  port: 8001\n\nkafka:\n  bootstrapServers: localhost:9092\n  topicPrefix: trendyol.stove.service\n  acks: \"1\"\n  offset: \"latest\"\n  heartbeatInSeconds: 30\n  groupId: spring-4x-example\n"
  },
  {
    "path": "examples/spring-4x-example/src/test/kotlin/com/stove/spring/example4x/e2e/ExampleTest.kt",
    "content": "package com.stove.spring.example4x.e2e\n\nimport arrow.core.some\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport stove.spring.example4x.application.handlers.ProductCreatedEvent\nimport stove.spring.example4x.infrastructure.api.ProductCreateRequest\nimport stove.spring.example4x.infrastructure.messaging.kafka.CreateProductCommand\nimport kotlin.time.Duration.Companion.seconds\n\nclass ExampleTest :\n  FunSpec({\n    test(\"index should be reachable\") {\n      stove {\n        http {\n          get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to testCase.name.name)) { actual ->\n            actual shouldContain \"Hi from Stove framework with ${testCase.name.name}\"\n            println(actual)\n          }\n          get<String>(\"/api/index\") { actual ->\n            actual shouldContain \"Hi from Stove framework with\"\n            println(actual)\n          }\n        }\n      }\n    }\n\n    test(\"should create new product when send product create request from api\") {\n      stove {\n        val productCreateRequest = ProductCreateRequest(1L, name = \"product name\", 99L)\n\n        http {\n          postAndExpectBodilessResponse(uri = \"/api/product/create\", body = productCreateRequest.some()) { actual ->\n            actual.status shouldBe 200\n          }\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent>(5.seconds) {\n            actual.id == productCreateRequest.id &&\n              actual.name == productCreateRequest.name &&\n              actual.supplierId == productCreateRequest.supplierId\n          }\n\n          shouldBeConsumed<ProductCreatedEvent>(5.seconds) {\n            actual.id == productCreateRequest.id &&\n              actual.name == productCreateRequest.name &&\n              actual.supplierId == productCreateRequest.supplierId\n          }\n        }\n      }\n    }\n\n    test(\"should consume product create command from kafka\") {\n      stove {\n        val createProductCommand = CreateProductCommand(2L, name = \"product from kafka\", 100L)\n\n        kafka {\n          publish(\"trendyol.stove.service.product.create.0\", createProductCommand)\n          shouldBeConsumed<CreateProductCommand>(10.seconds) {\n            actual.id == createProductCommand.id &&\n              actual.name == createProductCommand.name &&\n              actual.supplierId == createProductCommand.supplierId\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/spring-4x-example/src/test/kotlin/com/stove/spring/example4x/e2e/StoveConfig.kt",
    "content": "package com.stove.spring.example4x.e2e\n\nimport com.trendyol.stove.dashboard.DashboardSystemOptions\nimport com.trendyol.stove.dashboard.dashboard\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.tracing.tracing\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.slf4j.*\nimport stove.spring.example4x.run\nimport tools.jackson.databind.json.JsonMapper\n\nclass StoveConfig : AbstractProjectConfig() {\n  private val appPort = PortFinder.findAvailablePort()\n\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val logger: Logger = LoggerFactory.getLogger(\"WireMockMonitor\")\n\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:$appPort\"\n          )\n        }\n        kafka {\n          KafkaSystemOptions(\n            containerOptions = KafkaContainerOptions(tag = \"8.0.3\"),\n            configureExposedConfiguration = {\n              listOf(\n                \"kafka.bootstrapServers=${it.bootstrapServers}\",\n                \"kafka.groupId=spring-4x-example\"\n              )\n            }\n          )\n        }\n        bridge()\n\n        // Enable tracing - starts OTLP gRPC receiver on port 4317\n        // Service name is automatically extracted from incoming spans (set by OTel agent)\n        tracing { enableSpanReceiver() }\n        dashboard { DashboardSystemOptions(appName = \"spring-4x-example\") }\n\n        wiremock {\n          WireMockSystemOptions(\n            port = 0,\n            removeStubAfterRequestMatched = true,\n            afterRequest = { e, _ ->\n              logger.info(e.request.toString())\n            }\n          )\n        }\n        springBoot(\n          runner = { parameters ->\n            // The application will be auto-instrumented by OTel agent\n            // configured in build.gradle.kts tasks.test { }\n            run(parameters) {\n              addTestDependencies4x {\n                registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)\n                registerBean {\n                  val jsonMapper = this.bean<JsonMapper>()\n                  StoveJackson3ThroughIfStringSerde(jsonMapper)\n                }\n              }\n            }\n          },\n          withParameters = listOf(\n            \"server.port=$appPort\",\n            \"logging.level.root=info\",\n            \"logging.level.org.springframework.web=info\",\n            \"spring.profiles.active=default\",\n            \"kafka.heartbeatInSeconds=2\",\n            \"kafka.offset=earliest\"\n          )\n        )\n      }.run()\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n"
  },
  {
    "path": "examples/spring-4x-example/src/test/kotlin/com/stove/spring/example4x/e2e/jackson3.kt",
    "content": "package com.stove.spring.example4x.e2e\n\nimport com.trendyol.stove.serialization.StoveSerde\nimport org.slf4j.LoggerFactory\nimport tools.jackson.databind.json.JsonMapper\n\nclass StoveJackson3ThroughIfStringSerde(\n  private val jsonMapper: JsonMapper\n) : StoveSerde<Any, ByteArray> {\n  private val logger = LoggerFactory.getLogger(javaClass)\n\n  override fun serialize(value: Any): ByteArray = when (value) {\n    is ByteArray -> {\n      logger.info(\"Value is already a ByteArray, returning as is.\")\n      value\n    }\n\n    is String -> {\n      logger.info(\"Serializing String value.\")\n      val byteArray = value.toByteArray()\n      byteArray\n    }\n\n    else -> {\n      logger.info(\"Serializing value of type: {}\", value::class.java.name)\n      val byteArray = runCatching { jsonMapper.writeValueAsBytes(value) }\n        .onFailure { logger.error(\"Serialization failed\", it) }\n        .getOrThrow()\n      byteArray\n    }\n  }\n\n  override fun <T : Any> deserialize(value: ByteArray, clazz: Class<T>): T {\n    logger.info(\"Deserializing to class: {}\", clazz.name)\n    val value = runCatching {\n      jsonMapper.readValue(value, clazz)\n    }.onFailure { logger.error(\"Deserialization failed\", it) }.getOrThrow()\n    logger.info(\"Deserialized value: {}\", value)\n    return value\n  }\n}\n"
  },
  {
    "path": "examples/spring-4x-example/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.stove.spring.example4x.e2e.StoveConfig\n"
  },
  {
    "path": "examples/spring-4x-example/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "examples/spring-example/build.gradle.kts",
    "content": "import com.trendyol.stove.gradle.stoveTracing\n\nplugins {\n  alias(libs.plugins.spring.plugin)\n  alias(libs.plugins.spring.boot.three)\n  idea\n  application\n}\n\nstoveTracing {\n  serviceName = \"spring-example\"\n  otelAgentVersion = libs.versions.opentelemetry.instrumentation.get()\n}\n\ndependencies {\n  implementation(libs.spring.boot.three)\n  implementation(libs.spring.boot.three.autoconfigure)\n  implementation(libs.spring.boot.three.webflux)\n  implementation(libs.ktor.client.core)\n  implementation(libs.ktor.client.okhttp)\n  implementation(libs.ktor.client.content.negotiation)\n  implementation(libs.ktor.serialization.jackson.json)\n  implementation(libs.spring.boot.three.actuator)\n  annotationProcessor(libs.spring.boot.three.annotationProcessor)\n  implementation(libs.spring.boot.three.kafka)\n  implementation(libs.exposed.core)\n  implementation(libs.exposed.jdbc)\n  implementation(libs.exposed.java.time)\n  implementation(libs.kotlinx.reactor)\n  implementation(libs.kotlinx.core)\n  implementation(libs.kotlinx.reactive)\n  implementation(libs.postgresql)\n  implementation(libs.jackson.kotlin)\n  implementation(libs.kotlinx.slf4j)\n  implementation(libs.hikari)\n}\n\ndependencies {\n  testImplementation(projects.stove.testExtensions.stoveExtensionsKotest)\n  testImplementation(projects.stove.testExtensions.stoveExtensionsJunit)\n  testImplementation(projects.stove.lib.stoveHttp)\n  testImplementation(projects.stove.lib.stoveWiremock)\n  testImplementation(projects.stove.lib.stovePostgres)\n  testImplementation(projects.stove.lib.stoveElasticsearch)\n  testImplementation(projects.stove.starters.spring.stoveSpring)\n  testImplementation(projects.stove.starters.spring.stoveSpringKafka)\n  testImplementation(projects.stove.lib.stoveDashboard)\n  testImplementation(projects.stove.lib.stoveTracing)\n}\n\napplication { mainClass.set(\"stove.spring.example.ExampleAppKt\") }\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/ExampleApp.kt",
    "content": "package stove.spring.example\n\nimport org.springframework.boot.SpringApplication\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.properties.ConfigurationPropertiesScan\nimport org.springframework.boot.runApplication\nimport org.springframework.context.ConfigurableApplicationContext\n\n@SpringBootApplication\n@ConfigurationPropertiesScan\nclass ExampleApp\n\nfun main(args: Array<String>) {\n  run(args)\n}\n\n/**\n * This is the point where spring application gets run.\n * run(args, init) method is the important point for the testing configuration.\n * init allows us to override any dependency from the testing side that is being time related or configuration related.\n * Spring itself opens this configuration higher order function to the outside.\n */\nfun run(\n  args: Array<String>,\n  init: SpringApplication.() -> Unit = {}\n): ConfigurableApplicationContext = runApplication<ExampleApp>(*args, init = init)\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/application/handlers/ProductCreator.kt",
    "content": "package stove.spring.example.application.handlers\n\nimport org.jetbrains.exposed.v1.jdbc.insert\nimport org.jetbrains.exposed.v1.jdbc.transactions.suspendTransaction\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Component\nimport stove.spring.example.domain.Products\nimport stove.spring.example.infrastructure.Headers\nimport stove.spring.example.infrastructure.http.SupplierHttpService\nimport stove.spring.example.infrastructure.messaging.kafka.*\nimport stove.spring.example.infrastructure.messaging.kafka.consumers.CreateProductCommand\nimport java.time.Instant\n\n@Component\nclass ProductCreator(\n  private val supplierHttpService: SupplierHttpService,\n  private val kafkaProducer: KafkaProducer\n) {\n  @Value(\"\\${kafka.producer.product-created.topic-name}\")\n  lateinit var productCreatedTopic: String\n\n  suspend fun create(req: ProductCreateRequest): String = suspendTransaction {\n    val supplierPermission = supplierHttpService.getSupplierPermission(req.supplierId)\n    if (!supplierPermission.isAllowed) {\n      return@suspendTransaction \"Supplier with the given id(${req.supplierId}) is not allowed for product creation\"\n    }\n\n    Products.insert {\n      it[id] = req.id\n      it[name] = req.name\n      it[supplierId] = req.supplierId\n      it[Products.createdDate] = Instant.now()\n    }\n\n    kafkaProducer.send(\n      KafkaOutgoingMessage(\n        topic = productCreatedTopic,\n        key = req.id.toString(),\n        headers = mapOf(Headers.EVENT_TYPE to ProductCreatedEvent::class.simpleName!!),\n        partition = 0,\n        payload = req.mapToProductCreatedEvent()\n      )\n    )\n    return@suspendTransaction \"OK\"\n  }\n}\n\nfun CreateProductCommand.mapToCreateRequest(): ProductCreateRequest = ProductCreateRequest(this.id, this.name, this.supplierId)\n\nfun ProductCreateRequest.mapToProductCreatedEvent(): ProductCreatedEvent = ProductCreatedEvent(\n  this.id,\n  this.name,\n  this.supplierId,\n  Instant.now()\n)\n\ndata class ProductCreatedEvent(\n  val id: Long,\n  val name: String,\n  val supplierId: Long,\n  val createdDate: Instant,\n  val type: String = ProductCreatedEvent::class.simpleName!!\n)\n\ndata class ProductCreateRequest(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/application/services/SupplierService.kt",
    "content": "package stove.spring.example.application.services\n\ndata class SupplierPermission(\n  val supplierId: Long,\n  val isAllowed: Boolean\n)\n\ninterface SupplierService {\n  suspend fun getSupplierPermission(id: Long): SupplierPermission\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/domain/ProductTable.kt",
    "content": "package stove.spring.example.domain\n\nimport org.jetbrains.exposed.v1.core.Table\nimport org.jetbrains.exposed.v1.javatime.timestamp\n\nobject Products : Table(\"products\") {\n  val id = long(\"id\")\n  val name = varchar(\"name\", 255)\n  val supplierId = long(\"supplier_id\")\n  val createdDate = timestamp(\"created_date\")\n\n  override val primaryKey = PrimaryKey(id)\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/Constants.kt",
    "content": "package stove.spring.example.infrastructure\n\nimport org.slf4j.MDC\nimport java.net.InetAddress\nimport java.net.UnknownHostException\n\nobject Defaults {\n  val HOST_NAME: String =\n    try {\n      InetAddress.getLocalHost().hostName\n    } catch (\n      @Suppress(\"SwallowedException\") e: UnknownHostException\n    ) {\n      \"stove-service-host\"\n    }\n\n  const val AGENT_NAME = \"stove-service\"\n  const val USER_EMAIL = \"stove@trendyol.com\"\n}\n\nobject Headers {\n  const val USER_EMAIL_KEY: String = \"X-UserEmail\"\n  const val CORRELATION_ID_KEY: String = \"X-CorrelationId\"\n  const val AGENT_NAME_KEY = \"X-AgentName\"\n  const val PUBLISHED_DATE_KEY = \"X-PublishedDate\"\n  const val MESSAGE_ID_KEY = \"X-MessageId\"\n  const val HOST_KEY = \"X-Host\"\n  const val EVENT_TYPE = \"X-EventType\"\n\n  fun getOrDefault(\n    key: String,\n    defaultValue: String = Defaults.USER_EMAIL\n  ): String = try {\n    MDC.get(key) ?: MDC.get(key.lowercase()) ?: MDC.get(key.uppercase()) ?: defaultValue\n  } catch (\n    @Suppress(\"SwallowedException\") exception: IllegalStateException\n  ) {\n    defaultValue\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/ObjectMapperConfig.kt",
    "content": "package stove.spring.example.infrastructure\n\nimport com.fasterxml.jackson.annotation.JsonInclude\nimport com.fasterxml.jackson.databind.*\nimport com.fasterxml.jackson.module.kotlin.KotlinModule\nimport org.springframework.boot.autoconfigure.AutoConfigureBefore\nimport org.springframework.boot.autoconfigure.jackson.*\nimport org.springframework.context.annotation.*\n\n@Configuration\n@AutoConfigureBefore(JacksonAutoConfiguration::class)\nclass ObjectMapperConfig {\n  companion object {\n    fun default(): ObjectMapper = ObjectMapper()\n      .registerModule(KotlinModule.Builder().build())\n      .findAndRegisterModules()\n      .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)\n      .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)\n  }\n\n  @Bean\n  @Primary\n  fun objectMapper(): ObjectMapper = default()\n\n  @Bean\n  fun jacksonCustomizer(): Jackson2ObjectMapperBuilderCustomizer {\n    val default = default()\n    return Jackson2ObjectMapperBuilderCustomizer { builder ->\n      builder.factory(default.factory)\n    }\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/api/ProductController.kt",
    "content": "package stove.spring.example.infrastructure.api\n\nimport kotlinx.coroutines.reactive.*\nimport kotlinx.coroutines.reactor.mono\nimport org.springframework.http.codec.multipart.FilePart\nimport org.springframework.web.bind.annotation.GetMapping\nimport org.springframework.web.bind.annotation.PostMapping\nimport org.springframework.web.bind.annotation.RequestBody\nimport org.springframework.web.bind.annotation.RequestMapping\nimport org.springframework.web.bind.annotation.RequestParam\nimport org.springframework.web.bind.annotation.RequestPart\nimport org.springframework.web.bind.annotation.RestController\nimport stove.spring.example.application.handlers.ProductCreateRequest\nimport stove.spring.example.application.handlers.ProductCreator\n\n@RestController\n@RequestMapping(\"/api\")\nclass ProductController(\n  private val productCreator: ProductCreator\n) {\n  @GetMapping(\"/index\")\n  suspend fun get(\n    @RequestParam(required = false) keyword: String?\n  ): String = \"Hi from Stove framework with $keyword\"\n\n  @PostMapping(\"/product/create\")\n  suspend fun createProduct(\n    @RequestBody productCreateRequest: ProductCreateRequest\n  ): String = productCreator.create(productCreateRequest)\n\n  @PostMapping(\"/product/import\")\n  suspend fun importFile(\n    @RequestPart(name = \"name\") name: String,\n    @RequestPart(name = \"file\") file: FilePart\n  ): String {\n    val content = file\n      .content()\n      .flatMap { mono { it.asInputStream().readAllBytes() } }\n      .awaitSingle()\n      .let { String(it) }\n    return \"File ${file.filename()} is imported with $name and content: $content\"\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/http/SupplierHttpService.kt",
    "content": "package stove.spring.example.infrastructure.http\n\nimport io.ktor.client.*\nimport io.ktor.client.call.*\nimport io.ktor.client.request.*\nimport io.ktor.http.*\nimport org.springframework.stereotype.Component\nimport stove.spring.example.application.services.*\n\n@Component\nclass SupplierHttpService(\n  private val supplierHttpClient: HttpClient\n) : SupplierService {\n  override suspend fun getSupplierPermission(id: Long): SupplierPermission =\n    supplierHttpClient\n      .get(\"/suppliers/$id/allowed\") {\n        contentType(ContentType.Application.Json)\n      }.body()\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/http/WebClientConfiguration.kt",
    "content": "package stove.spring.example.infrastructure.http\n\nimport io.ktor.client.*\nimport io.ktor.client.engine.okhttp.*\nimport io.ktor.client.plugins.*\nimport io.ktor.client.plugins.contentnegotiation.*\nimport io.ktor.serialization.jackson.*\nimport org.springframework.boot.context.properties.EnableConfigurationProperties\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport stove.spring.example.infrastructure.ObjectMapperConfig\n\n@Suppress(\"MagicNumber\")\n@Configuration\n@EnableConfigurationProperties(WebClientConfigurationProperties::class)\nclass WebClientConfiguration(\n  private val webClientConfigurationProperties: WebClientConfigurationProperties\n) {\n  @Bean\n  fun supplierHttpClient(): HttpClient = HttpClient(OkHttp) {\n    install(ContentNegotiation) {\n      jackson(contentType = io.ktor.http.ContentType.Application.Json) {\n        ObjectMapperConfig.default()\n      }\n    }\n\n    defaultRequest {\n      url(webClientConfigurationProperties.supplierHttp.url)\n    }\n\n    engine {\n      config {\n        followRedirects(true)\n        connectTimeout(java.time.Duration.ofSeconds(30))\n        readTimeout(java.time.Duration.ofSeconds(30))\n      }\n    }\n\n    expectSuccess = true\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/http/WebClientConfigurationProperties.kt",
    "content": "package stove.spring.example.infrastructure.http\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\nimport java.net.URI\n\n@ConfigurationProperties(prefix = \"http-clients\")\ndata class WebClientConfigurationProperties(\n  var supplierHttp: ClientConfigurationProperty = ClientConfigurationProperty()\n)\n\ndata class ClientConfigurationProperty(\n  var url: String = \"\",\n  val uri: URI = URI.create(url),\n  var connectTimeout: Int = 0,\n  var readTimeout: Long = 0\n)\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/KafkaProducer.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka\n\nimport kotlinx.coroutines.future.await\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.apache.kafka.common.header.internals.RecordHeader\nimport org.slf4j.*\nimport org.springframework.kafka.core.KafkaTemplate\nimport org.springframework.stereotype.Component\n\ndata class KafkaOutgoingMessage<K, V>(\n  val topic: String,\n  val key: K,\n  val payload: V,\n  val headers: Map<String, String>,\n  val partition: Int? = null\n)\n\n@Component\nclass KafkaProducer(\n  private val kafkaTemplate: KafkaTemplate<String, Any>\n) {\n  private val logger: Logger = LoggerFactory.getLogger(KafkaProducer::class.java)\n\n  suspend fun send(message: KafkaOutgoingMessage<String, Any>) {\n    val recordHeaders = message.headers.map { RecordHeader(it.key, it.value.toByteArray()) }\n    val record = ProducerRecord<String, Any>(\n      message.topic,\n      message.partition,\n      message.key,\n      message.payload,\n      recordHeaders\n    )\n    logger.info(\"Kafka message has published $message\")\n    kafkaTemplate.send(record).await()\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/configuration/ConsumerSettings.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.configuration\n\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.common.serialization.StringDeserializer\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.boot.context.properties.EnableConfigurationProperties\nimport org.springframework.kafka.support.serializer.ErrorHandlingDeserializer\nimport org.springframework.kafka.support.serializer.JsonDeserializer\nimport org.springframework.stereotype.Component\nimport java.time.Duration\n\ninterface ConsumerSettings : MapBasedSettings\n\n@Component\n@EnableConfigurationProperties(KafkaProperties::class)\nclass DefaultConsumerSettings(\n  val kafkaProperties: KafkaProperties\n) : ConsumerSettings {\n  companion object {\n    const val AUTO_COMMIT_INTERVAL = 5L\n    const val SESSION_TIMEOUT = 120L\n    const val MAX_POLL_INTERVAL = 5L\n  }\n\n  @Value(\"\\${kafka.config.thread-count.basic-listener}\")\n  private val basicListenerThreadCount: String = \"100\"\n\n  /**\n   * We gave some properties as parameterized from application yaml for the override of this param from the stove.\n   * These are like below;\n   * autoCreateTopics: we are sending as true this param for creating missing topics in initialize time.\n   * heartbeatInSeconds: we should reduce heartbeat seconds the e2e environment, so we parameterized this field.\n   * secureKafka: this is Kafka secure parameter we can set false in default yaml.\n   *      If we want to use it for stage and prod yaml environment for adding secure Kafka configs set isSecure:true\n   * offset: we should override this field as earliest for the stove e2e environment.\n   */\n  override fun settings(): Map<String, Any> {\n    val props: MutableMap<String, Any> = HashMap()\n    props[ConsumerConfig.CLIENT_ID_CONFIG] = kafkaProperties.createClientId()\n    props[ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG] = kafkaProperties.autoCreateTopics\n    props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = kafkaProperties.bootstrapServers\n    props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = ErrorHandlingDeserializer::class.java\n    props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = ErrorHandlingDeserializer::class.java\n    props[ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS] = JsonDeserializer::class.java\n    props[ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS] = StringDeserializer::class.java\n    props[JsonDeserializer.TRUSTED_PACKAGES] = \"*\"\n    props[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] = kafkaProperties.offset\n    props[ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG] = true\n    props[ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG] = ofSeconds(AUTO_COMMIT_INTERVAL)\n    props[ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG] = ofSeconds(SESSION_TIMEOUT)\n    props[ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG] = ofSeconds(kafkaProperties.heartbeatInSeconds.toLong())\n    props[ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG] = ofMinutes(MAX_POLL_INTERVAL)\n    props[ConsumerConfig.MAX_POLL_RECORDS_CONFIG] = basicListenerThreadCount\n    props[ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG] = kafkaProperties.defaultApiTimeout\n    props[ConsumerConfig.DEFAULT_API_TIMEOUT_MS_CONFIG] = kafkaProperties.requestTimeout\n    // if we want to add secure Kafka config we can add these config inside of if (kafkaProperties.isSecure)\n    return props\n  }\n\n  private fun ofSeconds(seconds: Long) = Duration.ofSeconds(seconds).toMillis().toInt()\n\n  private fun ofMinutes(minutes: Long) = Duration.ofMinutes(minutes).toMillis().toInt()\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/configuration/KafkaConsumerConfiguration.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.configuration\n\nimport org.springframework.context.annotation.*\nimport org.springframework.kafka.annotation.EnableKafka\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.*\nimport org.springframework.util.backoff.FixedBackOff\n\n@EnableKafka\n@Configuration\n@Suppress(\"UNCHECKED_CAST\")\nclass KafkaConsumerConfiguration(\n  private val interceptor: RecordInterceptor<*, *>\n) {\n  @Bean\n  fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, Any>\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.setConcurrency(1)\n    factory.consumerFactory = consumerFactory\n    factory.containerProperties.isDeliveryAttemptHeader = true\n    val errorHandler = DefaultErrorHandler(FixedBackOff(0, 0))\n    factory.setCommonErrorHandler(errorHandler)\n    factory.setRecordInterceptor(interceptor as RecordInterceptor<String, String>)\n    return factory\n  }\n\n  @Bean\n  fun kafkaRetryListenerContainerFactory(\n    consumerRetryFactory: ConsumerFactory<String, Any>\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.setConcurrency(1)\n    factory.containerProperties.isDeliveryAttemptHeader = true\n    factory.consumerFactory = consumerRetryFactory\n    val errorHandler = DefaultErrorHandler(FixedBackOff(INTERVAL, 1))\n    factory.setCommonErrorHandler(errorHandler)\n    factory.setRecordInterceptor(interceptor as RecordInterceptor<String, String>)\n    return factory\n  }\n\n  @Bean\n  fun consumerFactory(\n    consumerSettings: ConsumerSettings\n  ): ConsumerFactory<String, Any> = DefaultKafkaConsumerFactory(consumerSettings.settings())\n\n  @Bean\n  fun consumerRetryFactory(\n    consumerSettings: ConsumerSettings\n  ): ConsumerFactory<String, Any> = DefaultKafkaConsumerFactory(consumerSettings.settings())\n\n  companion object {\n    const val RETRY_LISTENER_BEAN_NAME = \"kafkaRetryListenerContainerFactory\"\n    const val LISTENER_BEAN_NAME = \"kafkaListenerContainerFactory\"\n    const val INTERVAL = 5000L\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/configuration/KafkaProducerConfiguration.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.configuration\n\nimport org.apache.kafka.clients.producer.Producer\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.apache.kafka.clients.producer.RecordMetadata\nimport org.slf4j.LoggerFactory\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.kafka.annotation.EnableKafka\nimport org.springframework.kafka.core.DefaultKafkaProducerFactory\nimport org.springframework.kafka.core.KafkaTemplate\nimport org.springframework.kafka.core.ProducerFactory\nimport org.springframework.kafka.support.ProducerListener\n\n@EnableKafka\n@Configuration\nclass KafkaProducerConfiguration {\n  private val logger = LoggerFactory.getLogger(javaClass)\n\n  @Bean\n  fun producer(producerFactory: ProducerFactory<String, Any>): Producer<String, Any> = producerFactory.createProducer()\n\n  @Bean\n  fun kafkaTemplate(producerFactory: ProducerFactory<String, Any>): KafkaTemplate<String, Any> {\n    val kafkaTemplate = KafkaTemplate(producerFactory)\n    kafkaTemplate.setProducerListener(\n      object : ProducerListener<String, Any> {\n        override fun onError(\n          producerRecord: ProducerRecord<String, Any>,\n          recordMetadata: RecordMetadata?,\n          exception: java.lang.Exception?\n        ) {\n          logger.error(\n            \"ProducerListener Topic: ${producerRecord.topic()}, Key: ${producerRecord.value()}\",\n            exception\n          )\n          super.onError(producerRecord, recordMetadata, exception)\n        }\n      }\n    )\n    return kafkaTemplate\n  }\n\n  @Bean\n  fun producerFactory(producerSettings: ProducerSettings): ProducerFactory<String, Any> =\n    DefaultKafkaProducerFactory(producerSettings.settings())\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/configuration/KafkaProperties.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.configuration\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\nimport java.util.*\n\n@ConfigurationProperties(prefix = \"kafka\")\ndata class KafkaProperties(\n  var bootstrapServers: String = \"\",\n  var acks: String = \"1\",\n  var topicPrefix: String = \"\",\n  var heartbeatInSeconds: Int = 30,\n  var requestTimeout: String,\n  var defaultApiTimeout: String,\n  var compression: String = \"zstd\",\n  var offset: String = \"latest\",\n  var autoCreateTopics: Boolean = false,\n  var secureKafka: Boolean = false\n) {\n  val maxProducerConsumerBytes = \"4194304\"\n\n  fun createClientId() = UUID.randomUUID().toString().substring(0, SUBSTRING_LENGTH)\n\n  companion object {\n    private const val SUBSTRING_LENGTH = 5\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/configuration/MapBasedSettings.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.configuration\n\ninterface MapBasedSettings {\n  fun settings(): Map<String, Any>\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/configuration/ProducerSettings.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.configuration\n\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.StringSerializer\nimport org.springframework.boot.context.properties.EnableConfigurationProperties\nimport org.springframework.kafka.support.serializer.JsonSerializer\nimport org.springframework.stereotype.Component\nimport stove.spring.example.infrastructure.messaging.kafka.interceptors.CustomProducerInterceptor\n\ninterface ProducerSettings : MapBasedSettings\n\n@Component\n@EnableConfigurationProperties(KafkaProperties::class)\nclass DefaultProducerSettings(\n  private val kafkaProperties: KafkaProperties\n) : ProducerSettings {\n  override fun settings(): Map<String, Any> {\n    val props: MutableMap<String, Any> = HashMap()\n    props[ProducerConfig.BOOTSTRAP_SERVERS_CONFIG] = kafkaProperties.bootstrapServers\n    props[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java\n    props[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = JsonSerializer::class.java\n    props[ProducerConfig.INTERCEPTOR_CLASSES_CONFIG] = CustomProducerInterceptor::class.java.name\n    props[ProducerConfig.ACKS_CONFIG] = kafkaProperties.acks\n    props[ProducerConfig.COMPRESSION_TYPE_CONFIG] = kafkaProperties.compression\n    props[ProducerConfig.CLIENT_ID_CONFIG] = kafkaProperties.createClientId()\n    props[ProducerConfig.MAX_REQUEST_SIZE_CONFIG] = kafkaProperties.maxProducerConsumerBytes\n    props[\"default.api.timeout.ms\"] = kafkaProperties.defaultApiTimeout\n    props[ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG] = kafkaProperties.requestTimeout\n    return props\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/consumers/FailingProductCreateConsumer.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.consumers\n\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.slf4j.MDCContext\nimport org.apache.kafka.clients.consumer.ConsumerRecord\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty\nimport org.springframework.kafka.annotation.KafkaListener\nimport org.springframework.stereotype.Component\nimport stove.spring.example.infrastructure.messaging.kafka.configuration.KafkaConsumerConfiguration\n\ndata class BusinessException(\n  override val message: String\n) : RuntimeException(message)\n\ndata class FailingEvent(\n  val id: Long\n)\n\n@Component\n@ConditionalOnProperty(prefix = \"kafka.consumers\", value = [\"enabled\"], havingValue = \"true\")\nclass FailingProductCreateConsumer {\n  private val logger = LoggerFactory.getLogger(javaClass)\n\n  @KafkaListener(\n    topics = [\"#{@productFailingEventTopicConfig.topic}\"],\n    groupId = \"#{@consumerConfig.groupId}\",\n    containerFactory = KafkaConsumerConfiguration.LISTENER_BEAN_NAME\n  )\n  fun listen(record: ConsumerRecord<*, *>): Unit = runBlocking(MDCContext()) {\n    logger.info(\"Received product failing event $record\")\n    throw BusinessException(\"Failing product create event\")\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/consumers/JobTopicConfig.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.consumers\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\n@ConfigurationProperties(prefix = \"kafka.consumers\")\nclass ConsumerConfig(\n  var enabled: Boolean = false,\n  var groupId: String = \"\",\n  var retryTopicSuffix: String = \"\",\n  var errorTopicSuffix: String = \"\"\n)\n\n@Configuration\n@ConfigurationProperties(prefix = \"kafka.consumers.product-create\")\nclass ProductCreateEventTopicConfig : TopicConfig()\n\n@Configuration\n@ConfigurationProperties(prefix = \"kafka.consumers.product-failing\")\nclass ProductFailingEventTopicConfig : TopicConfig()\n\nabstract class TopicConfig(\n  var topic: String = \"\",\n  var retryTopic: String = \"\",\n  var errorTopic: String = \"\"\n)\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/consumers/ProductCreateConsumers.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.consumers\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.kotlin.convertValue\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.slf4j.MDCContext\nimport org.apache.kafka.clients.consumer.ConsumerRecord\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty\nimport org.springframework.kafka.annotation.KafkaListener\nimport org.springframework.stereotype.Component\nimport stove.spring.example.application.handlers.*\nimport stove.spring.example.infrastructure.messaging.kafka.configuration.KafkaConsumerConfiguration\n\n@Component\n@ConditionalOnProperty(prefix = \"kafka.consumers\", value = [\"enabled\"], havingValue = \"true\")\nclass ProductTransferConsumers(\n  private val productCreator: ProductCreator,\n  private val objectMapper: ObjectMapper\n) {\n  private val logger = LoggerFactory.getLogger(ProductTransferConsumers::class.java)\n\n  @KafkaListener(\n    topics = [\"#{@productCreateEventTopicConfig.topic}\"],\n    groupId = \"#{@consumerConfig.groupId}\",\n    containerFactory = KafkaConsumerConfiguration.LISTENER_BEAN_NAME\n  )\n  @KafkaListener(\n    topics = [\"#{@productCreateEventTopicConfig.retryTopic}\"],\n    groupId = \"#{@consumerConfig.groupId}_retry\",\n    containerFactory = KafkaConsumerConfiguration.RETRY_LISTENER_BEAN_NAME\n  )\n  fun listen(record: ConsumerRecord<*, Any>) = runBlocking(MDCContext()) {\n    logger.info(\"Received product transfer command $record\")\n    val command = objectMapper.convertValue<CreateProductCommand>(record.value())\n    productCreator.create(command.mapToCreateRequest())\n  }\n}\n\ndata class CreateProductCommand(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/interceptors/CustomConsumerInterceptor.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.interceptors\n\nimport org.apache.kafka.clients.consumer.*\nimport org.apache.kafka.common.header.Header\nimport org.slf4j.MDC\nimport org.springframework.kafka.listener.RecordInterceptor\nimport org.springframework.stereotype.Component\nimport java.nio.charset.StandardCharsets\n\n/**\n * if we use RecordInterceptor<String, String> we should change\n * it as ConsumerAwareRecordInterceptor<String, String> for the stove e2e testing.\n */\n@Component\nclass CustomConsumerInterceptor : RecordInterceptor<String, String> {\n  override fun intercept(\n    record: ConsumerRecord<String, String>,\n    consumer: Consumer<String, String>\n  ): ConsumerRecord<String, String>? {\n    val contextMap = HashMap<String, String>()\n    record\n      .headers()\n      .filter { it.key().lowercase().startsWith(\"x-\") }\n      .forEach { h: Header ->\n        contextMap[h.key()] = String(h.value(), StandardCharsets.UTF_8)\n      }\n    MDC.setContextMap(contextMap)\n    return record\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/messaging/kafka/interceptors/CustomProducerInterceptor.kt",
    "content": "package stove.spring.example.infrastructure.messaging.kafka.interceptors\n\nimport org.apache.kafka.clients.producer.ProducerInterceptor\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.apache.kafka.clients.producer.RecordMetadata\nimport stove.spring.example.infrastructure.Defaults\nimport stove.spring.example.infrastructure.Headers\nimport java.time.Instant\nimport java.util.UUID\n\nclass CustomProducerInterceptor : ProducerInterceptor<String, Any> {\n  companion object {\n    private val DEFAULT_HOST_NAME_AS_BYTE: ByteArray = Defaults.HOST_NAME.toByteArray()\n  }\n\n  override fun onSend(record: ProducerRecord<String, Any>): ProducerRecord<String, Any> {\n    val messageId = UUID.randomUUID().toString()\n    record.headers().add(Headers.MESSAGE_ID_KEY, messageId.toByteArray())\n    record.headers().add(Headers.PUBLISHED_DATE_KEY, Instant.now().toString().toByteArray())\n    record.headers().add(Headers.HOST_KEY, DEFAULT_HOST_NAME_AS_BYTE)\n    record.headers().add(\n      Headers.CORRELATION_ID_KEY,\n      Headers.getOrDefault(Headers.CORRELATION_ID_KEY, messageId).toByteArray()\n    )\n    record.headers().add(Headers.AGENT_NAME_KEY, Defaults.AGENT_NAME.toByteArray())\n    record.headers().add(\n      Headers.USER_EMAIL_KEY,\n      Headers.getOrDefault(Headers.USER_EMAIL_KEY, Defaults.USER_EMAIL).toByteArray()\n    )\n    return record\n  }\n\n  override fun configure(configs: MutableMap<String, *>?) = Unit\n\n  override fun onAcknowledgement(\n    metadata: RecordMetadata?,\n    exception: Exception?\n  ) = Unit\n\n  override fun close() = Unit\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/kotlin/stove/spring/example/infrastructure/postgres/ExposedConfiguration.kt",
    "content": "package stove.spring.example.infrastructure.postgres\n\nimport com.zaxxer.hikari.HikariDataSource\nimport org.jetbrains.exposed.v1.jdbc.Database\nimport org.springframework.boot.context.properties.ConfigurationProperties\nimport org.springframework.context.annotation.*\nimport javax.sql.DataSource\n\n@ConfigurationProperties(\"spring.datasource\")\ndata class DataSourceConfig(\n  val url: String,\n  val username: String,\n  val password: String\n)\n\n@Configuration\nclass ExposedConfiguration {\n  @Bean\n  fun dataSource(\n    dataSourceConfig: DataSourceConfig\n  ): DataSource = HikariDataSource().apply {\n    this.jdbcUrl = dataSourceConfig.url\n    this.username = dataSourceConfig.username\n    this.password = dataSourceConfig.password\n  }\n\n  @Bean\n  fun database(dataSource: DataSource): Database = Database.connect(dataSource)\n}\n"
  },
  {
    "path": "examples/spring-example/src/main/resources/application.yml",
    "content": "spring:\n  application:\n    name: \"stove\"\n  servlet:\n    multipart:\n      max-request-size: 10MB\n  datasource:\n    url: jdbc:postgresql://localhost:5432/stove\n    username: postgres\n    password: postgres\n    hikari:\n      maximum-pool-size: 10\n\nserver:\n  port: 8001\n  http2:\n    enabled: false\n\nhttp-clients:\n  supplier-http:\n    url: http://localhost:7078\n    connectTimeout: 2000\n    readTimeout: 20000\n\nkafka:\n  bootstrapServers: localhost:9092\n  topicPrefix: trendyol.stove.service\n  acks: 1\n  secureKafka: false\n  autoCreateTopics: false\n  offset: \"latest\"\n  heartbeatInSeconds: 30\n  request-timeout: \"20000\"\n  default-api-timeout: \"20000\"\n  config:\n    thread-count:\n      basic-listener: 25\n  producer:\n    prefix: trendyol.stove.service\n    product-created:\n      topic-name: ${kafka.producer.prefix}.productCreated.1\n  consumers:\n    retryTopicSuffix: trendyol.stove.service.retry\n    errorTopicSuffix: trendyol.stove.service.error\n    enabled: true\n    groupId: trendyol.stove.service\n    product-create:\n      topic: trendyol.stove.service.product.create.0\n      retryTopic: ${kafka.consumers.product-create.topic}.${kafka.consumers.retryTopicSuffix}\n      errorTopic: ${kafka.consumers.product-create.topic}.${kafka.consumers.errorTopicSuffix}\n    product-failing:\n      topic: trendyol.stove.service.product.failing.0\n      retryTopic: ${kafka.consumers.product-failing.topic}.${kafka.consumers.retryTopicSuffix}\n      errorTopic: ${kafka.consumers.product-failing.topic}.${kafka.consumers.errorTopicSuffix}\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/CreateProductsTableMigration.kt",
    "content": "package com.stove.spring.example.e2e\n\nimport com.trendyol.stove.postgres.PostgresSqlMigrationContext\nimport com.trendyol.stove.postgres.PostgresqlMigration\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\nclass CreateProductsTableMigration : PostgresqlMigration {\n  private val logger: Logger = LoggerFactory.getLogger(CreateProductsTableMigration::class.java)\n\n  override val order: Int = 1\n\n  override suspend fun execute(connection: PostgresSqlMigrationContext) {\n    logger.info(\"Creating products table\")\n    connection.operations.execute(\n      \"\"\"\n      DROP TABLE IF EXISTS products;\n      CREATE TABLE IF NOT EXISTS products (\n        id BIGINT PRIMARY KEY,\n        name VARCHAR(255) NOT NULL,\n        supplier_id BIGINT NOT NULL,\n        created_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n      );\n      \"\"\".trimIndent()\n    )\n    logger.info(\"Products table created\")\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/ExampleTest.kt",
    "content": "package com.stove.spring.example.e2e\n\nimport arrow.core.some\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.*\nimport io.kotest.matchers.string.shouldContain\nimport org.jetbrains.exposed.v1.jdbc.Database\nimport org.springframework.http.MediaType\nimport stove.spring.example.application.handlers.*\nimport stove.spring.example.application.services.SupplierPermission\nimport stove.spring.example.infrastructure.messaging.kafka.consumers.*\nimport kotlin.time.Duration.Companion.seconds\n\nclass ExampleTest :\n  FunSpec({\n    test(\"bridge should work\") {\n      stove {\n        using<Database> {\n          this shouldNotBe null\n        }\n      }\n    }\n\n    test(\"index should be reachable\") {\n      stove {\n        http {\n          get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to testCase.name.name)) { actual ->\n            actual shouldContain \"Hi from Stove framework with ${testCase.name.name}\"\n            println(actual)\n          }\n          get<String>(\"/api/index\") { actual ->\n            actual shouldContain \"Hi from Stove framework with\"\n            println(actual)\n          }\n        }\n      }\n    }\n\n    test(\"should create new product when send product create request from api for the allowed supplier\") {\n      stove {\n        val productCreateRequest = ProductCreateRequest(1L, name = \"product name\", 99L)\n        val supplierPermission = SupplierPermission(productCreateRequest.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${productCreateRequest.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        http {\n          postAndExpectBodilessResponse(uri = \"/api/product/create\", body = productCreateRequest.some()) { actual ->\n            actual.status shouldBe 200\n          }\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent> {\n            actual.id == productCreateRequest.id &&\n              actual.name == productCreateRequest.name &&\n              actual.supplierId == productCreateRequest.supplierId\n          }\n        }\n\n        postgresql {\n          shouldQuery<ProductCreateRequest>(\n            \"SELECT * FROM products WHERE id = ${productCreateRequest.id}\",\n            mapper = { row ->\n              ProductCreateRequest(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.long(\"supplier_id\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first().id shouldBe productCreateRequest.id\n            products.first().name shouldBe productCreateRequest.name\n            products.first().supplierId shouldBe productCreateRequest.supplierId\n          }\n        }\n      }\n    }\n\n    test(\"should throw error when send product create request from api for for the not allowed supplier\") {\n      stove {\n        val productCreateRequest = ProductCreateRequest(2L, name = \"product name\", 98L)\n        val supplierPermission = SupplierPermission(productCreateRequest.supplierId, isAllowed = false)\n        wiremock {\n          mockGet(\n            \"/suppliers/${productCreateRequest.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n        http {\n          postAndExpectJson<String>(uri = \"/api/product/create\", body = productCreateRequest.some()) { actual ->\n            actual shouldBe \"Supplier with the given id(${productCreateRequest.supplierId}) is not allowed for product creation\"\n          }\n        }\n      }\n    }\n\n    test(\"should throw error when send product create event for the not allowed supplier\") {\n      stove {\n        val command = CreateProductCommand(3L, name = \"product name\", 97L)\n        val supplierPermission = SupplierPermission(command.supplierId, isAllowed = false)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${supplierPermission.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        kafka {\n          publish(\"trendyol.stove.service.product.create.0\", command)\n          shouldBeConsumed<CreateProductCommand>(10.seconds) {\n            actual.id == command.id\n          }\n        }\n      }\n    }\n\n    test(\"should create new product when send product create event for the allowed supplier\") {\n      stove {\n        val createProductCommand = CreateProductCommand(4L, name = \"product name\", 96L)\n        val supplierPermission = SupplierPermission(createProductCommand.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${createProductCommand.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        kafka {\n          publish(\"trendyol.stove.service.product.create.0\", createProductCommand)\n          shouldBeConsumed<CreateProductCommand> {\n            actual.id == createProductCommand.id &&\n              actual.name == createProductCommand.name &&\n              actual.supplierId == createProductCommand.supplierId &&\n              metadata.headers[\"X-UserEmail\"] == \"stove@trendyol.com\"\n          }\n        }\n\n        postgresql {\n          shouldQuery<ProductCreateRequest>(\n            \"SELECT * FROM products WHERE id = ${createProductCommand.id}\",\n            mapper = { row ->\n              ProductCreateRequest(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.long(\"supplier_id\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first().id shouldBe createProductCommand.id\n            products.first().name shouldBe createProductCommand.name\n            products.first().supplierId shouldBe createProductCommand.supplierId\n          }\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent> {\n            actual.id == createProductCommand.id &&\n              actual.name == createProductCommand.name &&\n              actual.supplierId == createProductCommand.supplierId\n          }\n        }\n      }\n    }\n\n    test(\"when failing event is published then it should be validated\") {\n      stove {\n        kafka {\n          publish(\"trendyol.stove.service.product.failing.0\", FailingEvent(5L))\n          shouldBeFailed<FailingEvent> {\n            actual.id == 5L && reason is BusinessException\n          }\n\n          shouldBeFailed<FailingEvent> {\n            actual == FailingEvent(5L) && reason is BusinessException\n          }\n        }\n      }\n    }\n\n    test(\"file import should work\") {\n      stove {\n        http {\n          postMultipartAndExpectResponse<String>(\n            \"/api/product/import\",\n            body = listOf(\n              StoveMultiPartContent.Text(\"name\", \"product name\"),\n              StoveMultiPartContent.File(\n                \"file\",\n                \"file.txt\",\n                \"file\".toByteArray(),\n                contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE\n              )\n            )\n          ) { actual ->\n            actual.body() shouldBe \"File file.txt is imported with product name and content: file\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/StoveConfig.kt",
    "content": "package com.stove.spring.example.e2e\n\nimport com.trendyol.stove.dashboard.*\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.postgres.*\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.tracing.tracing\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.slf4j.*\nimport stove.spring.example.run\n\nclass StoveConfig : AbstractProjectConfig() {\n  private val appPort = PortFinder.findAvailablePort()\n\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val logger: Logger = LoggerFactory.getLogger(\"WireMockMonitor\")\n\n  @Suppress(\"LongMethod\")\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        dashboard { DashboardSystemOptions(appName = \"spring-example\") }\n        tracing { enableSpanReceiver() }\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:$appPort\"\n          )\n        }\n        postgresql {\n          PostgresqlOptions(\n            databaseName = \"stove\",\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"spring.datasource.url=${cfg.jdbcUrl}\",\n                \"spring.datasource.username=${cfg.username}\",\n                \"spring.datasource.password=${cfg.password}\"\n              )\n            }\n          ).migrations {\n            register<CreateProductsTableMigration>()\n          }\n        }\n        kafka {\n          KafkaSystemOptions(\n            containerOptions = KafkaContainerOptions(tag = \"8.0.3\"),\n            configureExposedConfiguration = {\n              listOf(\n                \"kafka.bootstrapServers=${it.bootstrapServers}\",\n                \"kafka.isSecure=false\"\n              )\n            }\n          )\n        }\n        bridge()\n        wiremock {\n          WireMockSystemOptions(\n            port = 0,\n            removeStubAfterRequestMatched = true,\n            afterRequest = { e, _ ->\n              logger.info(e.request.toString())\n            },\n            configureExposedConfiguration = { cfg ->\n              listOf(\"http-clients.supplier-http.url=${cfg.baseUrl}\")\n            }\n          )\n        }\n\n        springBoot(\n          runner = { parameters ->\n            run(parameters) {\n              this.addTestSystemDependencies()\n            }\n          },\n          withParameters = listOf(\n            \"server.port=$appPort\",\n            \"logging.level.root=info\",\n            \"logging.level.org.springframework.web=info\",\n            \"spring.profiles.active=default\",\n            \"kafka.heartbeatInSeconds=2\",\n            \"kafka.autoCreateTopics=true\",\n            \"kafka.offset=earliest\",\n            \"kafka.secureKafka=false\"\n          )\n        )\n      }.run()\n\n  override suspend fun afterProject() {\n    // Stove.stop() is intentionally not called here to allow JUnit tests\n    // (which run via a separate test engine in the same JVM) to use the\n    // same Stove instance. Cleanup happens via JVM shutdown hooks.\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/TestSystemInitializer.kt",
    "content": "package com.stove.spring.example.e2e\n\nimport com.trendyol.stove.kafka.TestSystemKafkaInterceptor\nimport com.trendyol.stove.serialization.*\nimport com.trendyol.stove.spring.stoveSpringRegistrar\nimport org.springframework.boot.SpringApplication\nimport stove.spring.example.infrastructure.ObjectMapperConfig\n\nfun SpringApplication.addTestSystemDependencies() {\n  this.addInitializers(\n    stoveSpringRegistrar {\n      bean<TestSystemKafkaInterceptor<*, *>>(isPrimary = true)\n      bean { StoveSerde.jackson.anyByteArraySerde(ObjectMapperConfig.default()) }\n    }\n  )\n}\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/TracingValidationTest.kt",
    "content": "package com.stove.spring.example.e2e\n\nimport arrow.core.some\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.tracing.*\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport stove.spring.example.application.handlers.ProductCreateRequest\nimport stove.spring.example.application.services.SupplierPermission\n\nclass TracingValidationTest :\n  FunSpec({\n\n    test(\"tracing should capture HTTP request context implicitly\") {\n      stove {\n        // Trace is auto-started, just make HTTP call\n        http {\n          get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"tracing-test\")) { actual ->\n            actual shouldContain \"Hi from Stove framework\"\n          }\n        }\n\n        // Access trace context for validation - all props accessible directly\n        tracing {\n          traceId.length shouldBe 32\n          rootSpanId.length shouldBe 16\n\n          val traceparent = toTraceparent()\n          traceparent shouldContain traceId\n\n          println(\"✓ Trace context created implicitly:\")\n          println(\"  - traceId: $traceId\")\n          println(\"  - testId: $testId\")\n          println(\"  - traceparent: $traceparent\")\n        }\n      }\n    }\n\n    test(\"tracing should work with full request flow\") {\n      stove {\n        val productCreateRequest = ProductCreateRequest(100L, name = \"traced product\", 999L)\n        val supplierPermission = SupplierPermission(productCreateRequest.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${productCreateRequest.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        http {\n          postAndExpectBodilessResponse(uri = \"/api/product/create\", body = productCreateRequest.some()) { actual ->\n            actual.status shouldBe 200\n          }\n        }\n\n        // Validate trace was captured\n        tracing {\n          println(\"✓ Full request flow traced:\")\n          println(\"  - traceId: $traceId\")\n          println(\"  - testId: $testId\")\n        }\n      }\n    }\n\n    test(\"trace collector should record spans\") {\n      stove {\n        tracing {\n          // Record test spans\n          collector.record(\n            SpanInfo(\n              traceId = traceId,\n              spanId = TraceContext.generateSpanId(),\n              parentSpanId = rootSpanId,\n              operationName = \"TestController.handleRequest\",\n              serviceName = \"collector-test\",\n              startTimeNanos = System.nanoTime(),\n              endTimeNanos = System.nanoTime() + 1_000_000,\n              status = SpanStatus.OK\n            )\n          )\n\n          collector.record(\n            SpanInfo(\n              traceId = traceId,\n              spanId = TraceContext.generateSpanId(),\n              parentSpanId = rootSpanId,\n              operationName = \"TestService.processData\",\n              serviceName = \"collector-test\",\n              startTimeNanos = System.nanoTime(),\n              endTimeNanos = System.nanoTime() + 2_000_000,\n              status = SpanStatus.OK\n            )\n          )\n\n          // Validate spans - methods available directly\n          shouldContainSpan(\"TestController.handleRequest\")\n          shouldContainSpan(\"TestService.processData\")\n          shouldNotHaveFailedSpans()\n          spanCountShouldBeAtLeast(2)\n\n          println(\"✓ Trace collector recorded spans:\")\n          println(renderTree())\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/hierarchy/BehaviorSpecHierarchyTest.kt",
    "content": "package com.stove.spring.example.e2e.hierarchy\n\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.BehaviorSpec\nimport io.kotest.matchers.string.shouldContain\n\nclass BehaviorSpecHierarchyTest :\n  BehaviorSpec({\n\n    given(\"the index endpoint\") {\n      `when`(\"requesting with a keyword\") {\n        then(\"should return greeting with keyword\") {\n          stove {\n            http {\n              get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"bdd-test\")) { actual ->\n                actual shouldContain \"Hi from Stove framework with bdd-test\"\n              }\n            }\n          }\n        }\n\n        then(\"should contain framework name\") {\n          stove {\n            http {\n              get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"bdd\")) { actual ->\n                actual shouldContain \"Stove\"\n              }\n            }\n          }\n        }\n      }\n\n      `when`(\"requesting without a keyword\") {\n        then(\"should return default greeting\") {\n          stove {\n            http {\n              get<String>(\"/api/index\") { actual ->\n                actual shouldContain \"Hi from Stove framework\"\n              }\n            }\n          }\n        }\n      }\n    }\n\n    given(\"health check scenarios\") {\n      `when`(\"the application is running\") {\n        then(\"index should be reachable\") {\n          stove {\n            http {\n              get<String>(\"/api/index\") { actual ->\n                actual shouldContain \"Stove\"\n              }\n            }\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/hierarchy/DescribeSpecHierarchyTest.kt",
    "content": "package com.stove.spring.example.e2e.hierarchy\n\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.DescribeSpec\nimport io.kotest.matchers.string.shouldContain\n\nclass DescribeSpecHierarchyTest :\n  DescribeSpec({\n\n    describe(\"Index API\") {\n      it(\"should return greeting\") {\n        stove {\n          http {\n            get<String>(\"/api/index\") { actual ->\n              actual shouldContain \"Hi from Stove framework\"\n            }\n          }\n        }\n      }\n\n      describe(\"with query parameters\") {\n        it(\"should include keyword in response\") {\n          stove {\n            http {\n              get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"describe-test\")) { actual ->\n                actual shouldContain \"describe-test\"\n              }\n            }\n          }\n        }\n\n        it(\"should handle different keywords\") {\n          stove {\n            http {\n              get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"another\")) { actual ->\n                actual shouldContain \"another\"\n              }\n            }\n          }\n        }\n      }\n    }\n\n    describe(\"Application health\") {\n      it(\"should respond to index\") {\n        stove {\n          http {\n            get<String>(\"/api/index\") { actual ->\n              actual shouldContain \"Stove\"\n            }\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/hierarchy/FunSpecContextHierarchyTest.kt",
    "content": "package com.stove.spring.example.e2e.hierarchy\n\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.string.shouldContain\n\nclass FunSpecContextHierarchyTest :\n  FunSpec({\n\n    context(\"index endpoint\") {\n      test(\"should return greeting\") {\n        stove {\n          http {\n            get<String>(\"/api/index\") { actual ->\n              actual shouldContain \"Hi from Stove framework\"\n            }\n          }\n        }\n      }\n\n      test(\"should accept keyword parameter\") {\n        stove {\n          http {\n            get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"ctx-test\")) { actual ->\n              actual shouldContain \"ctx-test\"\n            }\n          }\n        }\n      }\n    }\n\n    context(\"nested context levels\") {\n      context(\"level two\") {\n        test(\"should still work at deeper nesting\") {\n          stove {\n            http {\n              get<String>(\"/api/index\") { actual ->\n                actual shouldContain \"Stove\"\n              }\n            }\n          }\n        }\n      }\n    }\n\n    test(\"flat top-level test\") {\n      stove {\n        http {\n          get<String>(\"/api/index\") { actual ->\n            actual shouldContain \"Stove\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/hierarchy/NestedJunitHierarchyTest.kt",
    "content": "package com.stove.spring.example.e2e.hierarchy\n\nimport com.trendyol.stove.extensions.junit.StoveJUnitExtension\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.system.stove\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.Nested\nimport org.junit.jupiter.api.Test\nimport org.junit.jupiter.api.extension.ExtendWith\n\n@ExtendWith(StoveJUnitExtension::class)\nclass NestedJunitHierarchyTest {\n\n  @Nested\n  inner class IndexEndpoint {\n\n    @Test\n    fun `should return greeting`() = runBlocking {\n      stove {\n        http {\n          get<String>(\"/api/index\") { actual ->\n            actual shouldContain \"Hi from Stove framework\"\n          }\n        }\n      }\n    }\n\n    @Test\n    fun `should include keyword in response`() = runBlocking {\n      stove {\n        http {\n          get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"junit-nested\")) { actual ->\n            actual shouldContain \"junit-nested\"\n          }\n        }\n      }\n    }\n\n    @Nested\n    inner class WithQueryParams {\n\n      @Test\n      fun `should handle keyword parameter`() = runBlocking {\n        stove {\n          http {\n            get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"deep-nested\")) { actual ->\n              actual shouldContain \"deep-nested\"\n            }\n          }\n        }\n      }\n    }\n  }\n\n  @Nested\n  inner class HealthCheck {\n\n    @Test\n    fun `should be reachable`() = runBlocking {\n      stove {\n        http {\n          get<String>(\"/api/index\") { actual ->\n            actual shouldContain \"Stove\"\n          }\n        }\n      }\n    }\n  }\n\n  @Test\n  fun `flat junit test at root level`() = runBlocking {\n    stove {\n      http {\n        get<String>(\"/api/index\") { actual ->\n          actual shouldContain \"Stove\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "examples/spring-example/src/test/kotlin/com/stove/spring/example/e2e/hierarchy/StringSpecHierarchyTest.kt",
    "content": "package com.stove.spring.example.e2e.hierarchy\n\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.StringSpec\nimport io.kotest.matchers.string.shouldContain\n\nclass StringSpecHierarchyTest :\n  StringSpec({\n\n    \"should return greeting\" {\n      stove {\n        http {\n          get<String>(\"/api/index\") { actual ->\n            actual shouldContain \"Hi from Stove framework\"\n          }\n        }\n      }\n    }\n\n    \"should include keyword in response\" {\n      stove {\n        http {\n          get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to \"string-spec\")) { actual ->\n            actual shouldContain \"string-spec\"\n          }\n        }\n      }\n    }\n\n    \"should contain framework name\" {\n      stove {\n        http {\n          get<String>(\"/api/index\") { actual ->\n            actual shouldContain \"Stove\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/spring-example/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.stove.spring.example.e2e.StoveConfig\n"
  },
  {
    "path": "examples/spring-example/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "examples/spring-standalone-example/build.gradle.kts",
    "content": "import com.trendyol.stove.gradle.stoveTracing\n\nplugins {\n  alias(libs.plugins.spring.plugin)\n  alias(libs.plugins.spring.boot.three)\n  idea\n  application\n}\n\ndependencies {\n  implementation(libs.spring.boot.three)\n  implementation(libs.spring.boot.three.autoconfigure)\n  implementation(libs.spring.boot.three.webflux)\n  implementation(libs.ktor.client.core)\n  implementation(libs.ktor.client.okhttp)\n  implementation(libs.ktor.client.content.negotiation)\n  implementation(libs.ktor.serialization.jackson.json)\n  implementation(libs.spring.boot.three.actuator)\n  annotationProcessor(libs.spring.boot.three.annotationProcessor)\n  implementation(libs.spring.boot.three.kafka)\n  implementation(libs.exposed.core)\n  implementation(libs.exposed.jdbc)\n  implementation(libs.exposed.java.time)\n  implementation(libs.kotlinx.reactor)\n  implementation(libs.kotlinx.core)\n  implementation(libs.kotlinx.reactive)\n  implementation(libs.postgresql)\n  implementation(libs.jackson.kotlin)\n  implementation(libs.kotlinx.slf4j)\n  implementation(libs.hikari)\n}\n\ndependencies {\n  testImplementation(projects.stove.testExtensions.stoveExtensionsKotest)\n  testImplementation(projects.stove.lib.stoveHttp)\n  testImplementation(projects.stove.lib.stoveTracing)\n  testImplementation(projects.stove.lib.stoveDashboard)\n  testImplementation(projects.stove.lib.stoveWiremock)\n  testImplementation(projects.stove.lib.stovePostgres)\n  testImplementation(projects.stove.lib.stoveElasticsearch)\n  testImplementation(projects.stove.lib.stoveKafka)\n  testImplementation(projects.stove.starters.spring.stoveSpring)\n}\n\nstoveTracing {\n  serviceName = \"spring-standalone-example\"\n  otelAgentVersion = libs.versions.opentelemetry.instrumentation.get()\n}\n\napplication { mainClass.set(\"stove.spring.standalone.example.ExampleAppKt\") }\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/ExampleApp.kt",
    "content": "package stove.spring.standalone.example\n\nimport org.springframework.boot.*\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.properties.ConfigurationPropertiesScan\nimport org.springframework.context.ConfigurableApplicationContext\n\n@SpringBootApplication\n@ConfigurationPropertiesScan\nclass ExampleApp\n\nfun main(args: Array<String>) {\n  run(args)\n}\n\n/**\n * This is the point where spring application gets run.\n * run(args, init) method is the important point for the testing configuration.\n * init allows us to override any dependency from the testing side that is being time related or configuration related.\n * Spring itself opens this configuration higher order function to the outside.\n */\nfun run(\n  args: Array<String>,\n  init: SpringApplication.() -> Unit = {}\n): ConfigurableApplicationContext = runApplication<ExampleApp>(*args, init = init)\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/application/handlers/ProductCreator.kt",
    "content": "package stove.spring.standalone.example.application.handlers\n\nimport org.jetbrains.exposed.v1.jdbc.insert\nimport org.jetbrains.exposed.v1.jdbc.transactions.suspendTransaction\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Component\nimport stove.spring.standalone.example.domain.Products\nimport stove.spring.standalone.example.infrastructure.Headers\nimport stove.spring.standalone.example.infrastructure.http.SupplierHttpService\nimport stove.spring.standalone.example.infrastructure.messaging.kafka.*\nimport stove.spring.standalone.example.infrastructure.messaging.kafka.consumers.CreateProductCommand\nimport java.time.Instant\n\n@Component\nclass ProductCreator(\n  private val supplierHttpService: SupplierHttpService,\n  private val kafkaProducer: KafkaProducer\n) {\n  @Value(\"\\${kafka.producer.product-created.topic-name}\")\n  lateinit var productCreatedTopic: String\n\n  suspend fun create(req: ProductCreateRequest): String = suspendTransaction {\n    val supplierPermission = supplierHttpService.getSupplierPermission(req.supplierId)\n    if (!supplierPermission.isAllowed) {\n      return@suspendTransaction \"Supplier with the given id(${req.supplierId}) is not allowed for product creation\"\n    }\n\n    Products.insert {\n      it[id] = req.id\n      it[name] = req.name\n      it[supplierId] = req.supplierId\n      it[Products.createdDate] = Instant.now()\n    }\n\n    kafkaProducer.send(\n      KafkaOutgoingMessage(\n        topic = productCreatedTopic,\n        key = req.id.toString(),\n        headers = mapOf(Headers.EVENT_TYPE to ProductCreatedEvent::class.simpleName!!),\n        partition = 0,\n        payload = req.mapToProductCreatedEvent()\n      )\n    )\n    return@suspendTransaction \"OK\"\n  }\n}\n\nfun CreateProductCommand.mapToCreateRequest(): ProductCreateRequest = ProductCreateRequest(this.id, this.name, this.supplierId)\n\nfun ProductCreateRequest.mapToProductCreatedEvent(): ProductCreatedEvent = ProductCreatedEvent(\n  this.id,\n  this.name,\n  this.supplierId,\n  Instant.now()\n)\n\ndata class ProductCreatedEvent(\n  val id: Long,\n  val name: String,\n  val supplierId: Long,\n  val createdDate: Instant,\n  val type: String = ProductCreatedEvent::class.simpleName!!\n)\n\ndata class ProductCreateRequest(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/application/services/SupplierService.kt",
    "content": "package stove.spring.standalone.example.application.services\n\ndata class SupplierPermission(\n  val id: Long,\n  val isAllowed: Boolean\n)\n\ninterface SupplierService {\n  suspend fun getSupplierPermission(id: Long): SupplierPermission?\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/domain/ProductTable.kt",
    "content": "package stove.spring.standalone.example.domain\n\nimport org.jetbrains.exposed.v1.core.Table\nimport org.jetbrains.exposed.v1.javatime.timestamp\n\nobject Products : Table(\"products\") {\n  val id = long(\"id\")\n  val name = varchar(\"name\", 255)\n  val supplierId = long(\"supplier_id\")\n  val createdDate = timestamp(\"created_date\")\n\n  override val primaryKey = PrimaryKey(id)\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/Constants.kt",
    "content": "package stove.spring.standalone.example.infrastructure\n\nimport org.slf4j.MDC\nimport java.net.*\n\nobject Defaults {\n  val HOST_NAME: String =\n    try {\n      InetAddress.getLocalHost().hostName\n    } catch (\n      @Suppress(\"SwallowedException\") e: UnknownHostException\n    ) {\n      \"stove-service-host\"\n    }\n\n  const val AGENT_NAME = \"stove-service\"\n  const val USER_EMAIL = \"stove@trendyol.com\"\n}\n\nobject Headers {\n  const val USER_EMAIL_KEY: String = \"X-UserEmail\"\n  const val CORRELATION_ID_KEY: String = \"X-CorrelationId\"\n  const val AGENT_NAME_KEY = \"X-AgentName\"\n  const val PUBLISHED_DATE_KEY = \"X-PublishedDate\"\n  const val MESSAGE_ID_KEY = \"X-MessageId\"\n  const val HOST_KEY = \"X-Host\"\n  const val EVENT_TYPE = \"X-EventType\"\n\n  fun getOrDefault(\n    key: String,\n    defaultValue: String = Defaults.USER_EMAIL\n  ): String = try {\n    MDC.get(key) ?: MDC.get(key.lowercase()) ?: MDC.get(key.uppercase()) ?: defaultValue\n  } catch (\n    @Suppress(\"SwallowedException\") exception: IllegalStateException\n  ) {\n    defaultValue\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/ObjectMapperConfig.kt",
    "content": "package stove.spring.standalone.example.infrastructure\n\nimport com.fasterxml.jackson.annotation.JsonInclude\nimport com.fasterxml.jackson.databind.*\nimport com.fasterxml.jackson.module.kotlin.KotlinModule\nimport org.springframework.boot.autoconfigure.AutoConfigureBefore\nimport org.springframework.boot.autoconfigure.jackson.*\nimport org.springframework.context.annotation.*\n\n@Configuration\n@AutoConfigureBefore(JacksonAutoConfiguration::class)\nclass ObjectMapperConfig {\n  companion object {\n    val default: ObjectMapper = ObjectMapper()\n      .registerModule(KotlinModule.Builder().build())\n      .findAndRegisterModules()\n      .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)\n      .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)\n  }\n\n  @Bean\n  @Primary\n  fun objectMapper(): ObjectMapper = default\n\n  @Bean\n  fun jacksonCustomizer(): Jackson2ObjectMapperBuilderCustomizer = Jackson2ObjectMapperBuilderCustomizer { builder ->\n    builder.factory(default.factory)\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/api/ProductController.kt",
    "content": "package stove.spring.standalone.example.infrastructure.api\n\nimport kotlinx.coroutines.reactive.awaitSingle\nimport kotlinx.coroutines.reactor.mono\nimport org.springframework.http.codec.multipart.FilePart\nimport org.springframework.web.bind.annotation.*\nimport stove.spring.standalone.example.application.handlers.*\n\n@RestController\n@RequestMapping(\"/api\")\nclass ProductController(\n  private val productCreator: ProductCreator\n) {\n  @GetMapping(\"/index\")\n  suspend fun get(\n    @RequestParam(required = false) keyword: String\n  ): String = \"Hi from Stove framework with $keyword\"\n\n  @PostMapping(\"/product/create\")\n  suspend fun createProduct(\n    @RequestBody productCreateRequest: ProductCreateRequest\n  ): String = productCreator.create(productCreateRequest)\n\n  @PostMapping(\"/product/import\")\n  suspend fun importFile(\n    @RequestPart(name = \"name\") name: String,\n    @RequestPart(name = \"file\") file: FilePart\n  ): String {\n    val content = file\n      .content()\n      .flatMap { mono { it.asInputStream().readAllBytes() } }\n      .awaitSingle()\n      .let { String(it) }\n    return \"File ${file.filename()} is imported with $name and content: $content\"\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/http/SupplierHttpService.kt",
    "content": "package stove.spring.standalone.example.infrastructure.http\n\nimport io.ktor.client.*\nimport io.ktor.client.call.*\nimport io.ktor.client.request.*\nimport io.ktor.http.*\nimport org.springframework.stereotype.Component\nimport stove.spring.standalone.example.application.services.*\n\n@Component\nclass SupplierHttpService(\n  private val supplierHttpClient: HttpClient\n) : SupplierService {\n  override suspend fun getSupplierPermission(id: Long): SupplierPermission =\n    supplierHttpClient\n      .get(\"/suppliers/$id/allowed\") {\n        contentType(ContentType.Application.Json)\n      }.body()\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/http/WebClientConfiguration.kt",
    "content": "package stove.spring.standalone.example.infrastructure.http\n\nimport io.ktor.client.*\nimport io.ktor.client.engine.okhttp.*\nimport io.ktor.client.plugins.*\nimport io.ktor.client.plugins.contentnegotiation.*\nimport io.ktor.serialization.jackson.*\nimport org.springframework.boot.context.properties.EnableConfigurationProperties\nimport org.springframework.context.annotation.*\n\n@Suppress(\"MagicNumber\")\n@Configuration\n@EnableConfigurationProperties(WebClientConfigurationProperties::class)\nclass WebClientConfiguration(\n  private val webClientConfigurationProperties: WebClientConfigurationProperties\n) {\n  @Bean\n  fun supplierHttpClient(): HttpClient = HttpClient(OkHttp) {\n    install(ContentNegotiation) {\n      jackson(contentType = io.ktor.http.ContentType.Application.Json)\n    }\n\n    defaultRequest {\n      url(webClientConfigurationProperties.supplierHttp.url)\n    }\n\n    engine {\n      config {\n        followRedirects(true)\n        connectTimeout(java.time.Duration.ofSeconds(30))\n        readTimeout(java.time.Duration.ofSeconds(30))\n      }\n    }\n\n    expectSuccess = true\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/http/WebClientConfigurationProperties.kt",
    "content": "package stove.spring.standalone.example.infrastructure.http\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\nimport java.net.URI\n\n@ConfigurationProperties(prefix = \"http-clients\")\ndata class WebClientConfigurationProperties(\n  var supplierHttp: ClientConfigurationProperty = ClientConfigurationProperty()\n)\n\ndata class ClientConfigurationProperty(\n  var url: String = \"\",\n  val uri: URI = URI.create(url),\n  var connectTimeout: Int = 0,\n  var readTimeout: Long = 0\n)\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/KafkaProducer.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka\n\nimport kotlinx.coroutines.future.await\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.apache.kafka.common.header.internals.RecordHeader\nimport org.slf4j.*\nimport org.springframework.kafka.core.KafkaTemplate\nimport org.springframework.stereotype.Component\n\ndata class KafkaOutgoingMessage<K, V>(\n  val topic: String,\n  val key: K,\n  val payload: V,\n  val headers: Map<String, String>,\n  val partition: Int? = null\n)\n\n@Component\nclass KafkaProducer(\n  private val kafkaTemplate: KafkaTemplate<String, Any>\n) {\n  private val logger: Logger = LoggerFactory.getLogger(KafkaProducer::class.java)\n\n  suspend fun send(message: KafkaOutgoingMessage<String, Any>) {\n    val recordHeaders = message.headers.map { RecordHeader(it.key, it.value.toByteArray()) }\n    val record = ProducerRecord<String, Any>(\n      message.topic,\n      message.partition,\n      message.key,\n      message.payload,\n      recordHeaders\n    )\n    logger.info(\"Kafka message has published $message\")\n    kafkaTemplate.send(record).await()\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/configuration/ConsumerSettings.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.configuration\n\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.common.serialization.StringDeserializer\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.boot.context.properties.EnableConfigurationProperties\nimport org.springframework.kafka.support.serializer.*\nimport org.springframework.stereotype.Component\nimport java.time.Duration\n\ninterface ConsumerSettings : MapBasedSettings\n\n@Component\n@EnableConfigurationProperties(KafkaProperties::class)\nclass DefaultConsumerSettings(\n  val kafkaProperties: KafkaProperties\n) : ConsumerSettings {\n  companion object {\n    const val AUTO_COMMIT_INTERVAL = 5L\n    const val SESSION_TIMEOUT = 120L\n    const val MAX_POLL_INTERVAL = 5L\n  }\n\n  @Value(\"\\${kafka.config.thread-count.basic-listener}\")\n  private val basicListenerThreadCount: String = \"100\"\n\n  /**\n   * We gave some properties as parameterized from application yaml for the override of this param from the stove.\n   * These are like below;\n   * autoCreateTopics: we are sending as true this param for creating missing topics in initialize time.\n   * heartbeatInSeconds: we should reduce heartbeat seconds the e2e environment, so we parameterized this field.\n   * secureKafka: this is Kafka secure parameter we can set false in default yaml.\n   *      If we want to use it for stage and prod yaml environment for adding secure Kafka configs set isSecure:true\n   * offset: we should override this field as earliest for the stove e2e environment.\n   */\n  override fun settings(): Map<String, Any> {\n    val props: MutableMap<String, Any> = HashMap()\n    props[ConsumerConfig.CLIENT_ID_CONFIG] = kafkaProperties.createClientId()\n    props[ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG] = kafkaProperties.autoCreateTopics\n    props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = kafkaProperties.bootstrapServers\n    props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = ErrorHandlingDeserializer::class.java\n    props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = ErrorHandlingDeserializer::class.java\n    props[ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS] = JsonDeserializer::class.java\n    props[ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS] = StringDeserializer::class.java\n    props[JsonDeserializer.TRUSTED_PACKAGES] = \"*\"\n    props[JsonDeserializer.REMOVE_TYPE_INFO_HEADERS] = false\n    props[JsonDeserializer.VALUE_DEFAULT_TYPE] = Any::class.java\n    props[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] = kafkaProperties.offset\n    props[ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG] = true\n    props[ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG] = ofSeconds(AUTO_COMMIT_INTERVAL)\n    props[ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG] = ofSeconds(SESSION_TIMEOUT)\n    props[ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG] = ofSeconds(kafkaProperties.heartbeatInSeconds.toLong())\n    props[ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG] = ofMinutes(MAX_POLL_INTERVAL)\n    props[ConsumerConfig.MAX_POLL_RECORDS_CONFIG] = basicListenerThreadCount\n    props[ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG] = kafkaProperties.defaultApiTimeout\n    props[ConsumerConfig.DEFAULT_API_TIMEOUT_MS_CONFIG] = kafkaProperties.requestTimeout\n    props[ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG] = kafkaProperties.interceptorClasses\n    // if we want to add secure Kafka config we can add these config inside of if (kafkaProperties.isSecure)\n    return props\n  }\n\n  private fun ofSeconds(seconds: Long) = Duration.ofSeconds(seconds).toMillis().toInt()\n\n  private fun ofMinutes(minutes: Long) = Duration.ofMinutes(minutes).toMillis().toInt()\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/configuration/KafkaConsumerConfiguration.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.configuration\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport org.springframework.context.annotation.*\nimport org.springframework.kafka.annotation.EnableKafka\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.*\nimport org.springframework.kafka.support.converter.StringJsonMessageConverter\nimport org.springframework.util.backoff.FixedBackOff\n\n@EnableKafka\n@Configuration\nclass KafkaConsumerConfiguration(\n  private val objectMapper: ObjectMapper,\n  private val interceptor: RecordInterceptor<String, String>\n) {\n  @Bean\n  fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, Any>,\n    kafkaTemplate: KafkaTemplate<String, Any>\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.setConcurrency(1)\n    factory.consumerFactory = consumerFactory\n    factory.containerProperties.isDeliveryAttemptHeader = true\n    val errorHandler = DefaultErrorHandler(\n      DeadLetterPublishingRecoverer(kafkaTemplate),\n      FixedBackOff(0, 0)\n    )\n    factory.setCommonErrorHandler(errorHandler)\n    factory.setRecordInterceptor(interceptor)\n    return factory\n  }\n\n  @Bean\n  fun kafkaRetryListenerContainerFactory(\n    consumerRetryFactory: ConsumerFactory<String, Any>,\n    kafkaTemplate: KafkaTemplate<String, Any>\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.setConcurrency(1)\n    factory.containerProperties.isDeliveryAttemptHeader = true\n    factory.consumerFactory = consumerRetryFactory\n    val errorHandler = DefaultErrorHandler(\n      DeadLetterPublishingRecoverer(kafkaTemplate),\n      FixedBackOff(INTERVAL, 1)\n    )\n    factory.setCommonErrorHandler(errorHandler)\n    factory.setRecordInterceptor(interceptor)\n    return factory\n  }\n\n  @Bean\n  fun consumerFactory(consumerSettings: ConsumerSettings): ConsumerFactory<String, Any> =\n    DefaultKafkaConsumerFactory(consumerSettings.settings())\n\n  @Bean\n  fun consumerRetryFactory(consumerSettings: ConsumerSettings): ConsumerFactory<String, Any> =\n    DefaultKafkaConsumerFactory(consumerSettings.settings())\n\n  @Bean\n  fun stringJsonMessageConverter(): StringJsonMessageConverter = StringJsonMessageConverter(objectMapper)\n\n  companion object {\n    const val RETRY_LISTENER_BEAN_NAME = \"kafkaRetryListenerContainerFactory\"\n    const val LISTENER_BEAN_NAME = \"kafkaListenerContainerFactory\"\n    const val INTERVAL = 5000L\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/configuration/KafkaProducerConfiguration.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.configuration\n\nimport org.apache.kafka.clients.producer.*\nimport org.slf4j.LoggerFactory\nimport org.springframework.context.annotation.*\nimport org.springframework.kafka.annotation.EnableKafka\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.support.ProducerListener\n\n@EnableKafka\n@Configuration\nclass KafkaProducerConfiguration {\n  private val logger = LoggerFactory.getLogger(javaClass)\n\n  @Bean\n  fun producer(producerFactory: ProducerFactory<String, Any>): Producer<String, Any> = producerFactory.createProducer()\n\n  @Bean\n  fun kafkaTemplate(producerFactory: ProducerFactory<String, Any>): KafkaTemplate<String, Any> {\n    val kafkaTemplate = KafkaTemplate(producerFactory)\n    kafkaTemplate.setProducerListener(\n      object : ProducerListener<String, Any> {\n        override fun onError(\n          producerRecord: ProducerRecord<String, Any>,\n          recordMetadata: RecordMetadata?,\n          exception: java.lang.Exception?\n        ) {\n          logger.error(\n            \"ProducerListener Topic: ${producerRecord.topic()}, Key: ${producerRecord.value()}\",\n            exception\n          )\n          super.onError(producerRecord, recordMetadata, exception)\n        }\n      }\n    )\n    return kafkaTemplate\n  }\n\n  @Bean\n  fun producerFactory(\n    producerSettings: ProducerSettings\n  ): ProducerFactory<String, Any> = DefaultKafkaProducerFactory(producerSettings.settings())\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/configuration/KafkaProperties.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.configuration\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\nimport java.util.*\n\n@ConfigurationProperties(prefix = \"kafka\")\ndata class KafkaProperties(\n  var bootstrapServers: String = \"\",\n  var acks: String = \"1\",\n  var topicPrefix: String = \"\",\n  var heartbeatInSeconds: Int = 30,\n  var requestTimeout: String,\n  var defaultApiTimeout: String,\n  var compression: String = \"zstd\",\n  var offset: String = \"latest\",\n  var autoCreateTopics: Boolean = false,\n  var secureKafka: Boolean = false,\n  var interceptorClasses: List<String> = emptyList()\n) {\n  val maxProducerConsumerBytes = \"4194304\"\n\n  fun createClientId() = UUID.randomUUID().toString().substring(0, SUBSTRING_LENGTH)\n\n  companion object {\n    private const val SUBSTRING_LENGTH = 5\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/configuration/MapBasedSettings.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.configuration\n\ninterface MapBasedSettings {\n  fun settings(): Map<String, Any>\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/configuration/ProducerSettings.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.configuration\n\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.StringSerializer\nimport org.springframework.boot.context.properties.EnableConfigurationProperties\nimport org.springframework.kafka.support.serializer.JsonSerializer\nimport org.springframework.stereotype.Component\nimport stove.spring.standalone.example.infrastructure.messaging.kafka.interceptors.CustomProducerInterceptor\n\ninterface ProducerSettings : MapBasedSettings\n\n@Component\n@EnableConfigurationProperties(KafkaProperties::class)\nclass DefaultProducerSettings(\n  private val kafkaProperties: KafkaProperties\n) : ProducerSettings {\n  override fun settings(): Map<String, Any> {\n    val props: MutableMap<String, Any> = HashMap()\n    props[ProducerConfig.BOOTSTRAP_SERVERS_CONFIG] = kafkaProperties.bootstrapServers\n    props[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java\n    props[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = JsonSerializer::class.java\n    props[ProducerConfig.INTERCEPTOR_CLASSES_CONFIG] =\n      listOf(CustomProducerInterceptor::class.java.name) + kafkaProperties.interceptorClasses\n    props[ProducerConfig.ACKS_CONFIG] = kafkaProperties.acks\n    props[ProducerConfig.COMPRESSION_TYPE_CONFIG] = kafkaProperties.compression\n    props[ProducerConfig.CLIENT_ID_CONFIG] = kafkaProperties.createClientId()\n    props[ProducerConfig.MAX_REQUEST_SIZE_CONFIG] = kafkaProperties.maxProducerConsumerBytes\n    props[\"default.api.timeout.ms\"] = kafkaProperties.defaultApiTimeout\n    props[ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG] = kafkaProperties.requestTimeout\n    return props\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/consumers/FailingProductCreateConsumer.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.consumers\n\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.slf4j.MDCContext\nimport org.apache.kafka.clients.consumer.ConsumerRecord\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty\nimport org.springframework.kafka.annotation.KafkaListener\nimport org.springframework.stereotype.Component\nimport stove.spring.standalone.example.infrastructure.messaging.kafka.configuration.KafkaConsumerConfiguration\n\ndata class BusinessException(\n  override val message: String\n) : RuntimeException(message)\n\n@Component\n@ConditionalOnProperty(prefix = \"kafka.consumers\", value = [\"enabled\"], havingValue = \"true\")\nclass FailingProductCreateConsumer {\n  private val logger = LoggerFactory.getLogger(javaClass)\n\n  @KafkaListener(\n    topics = [\"#{@productFailingEventTopicConfig.topic}\"],\n    groupId = \"#{@consumerConfig.groupId}\",\n    containerFactory = KafkaConsumerConfiguration.LISTENER_BEAN_NAME\n  )\n  fun listen(record: ConsumerRecord<*, *>): Unit = runBlocking(MDCContext()) {\n    logger.info(\"Received product failing event ${record.value()}\")\n    throw BusinessException(\"Failing product create event\")\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/consumers/JobTopicConfig.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.consumers\n\nimport org.springframework.boot.context.properties.ConfigurationProperties\nimport org.springframework.context.annotation.Configuration\n\n@Configuration\n@ConfigurationProperties(prefix = \"kafka.consumers\")\nclass ConsumerConfig(\n  var enabled: Boolean = false,\n  var groupId: String = \"\",\n  var retryTopicSuffix: String = \"\",\n  var errorTopicSuffix: String = \"\"\n)\n\n@Configuration\n@ConfigurationProperties(prefix = \"kafka.consumers.product-create\")\nclass ProductCreateEventTopicConfig : TopicConfig()\n\n@Configuration\n@ConfigurationProperties(prefix = \"kafka.consumers.product-failing\")\nclass ProductFailingEventTopicConfig : TopicConfig()\n\nabstract class TopicConfig(\n  var topic: String = \"\",\n  var retryTopic: String = \"\",\n  var errorTopic: String = \"\"\n)\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/consumers/ProductCreateConsumers.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.consumers\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.slf4j.MDCContext\nimport org.apache.kafka.clients.consumer.ConsumerRecord\nimport org.slf4j.LoggerFactory\nimport org.springframework.boot.autoconfigure.condition.ConditionalOnProperty\nimport org.springframework.kafka.annotation.KafkaListener\nimport org.springframework.stereotype.Component\nimport stove.spring.standalone.example.application.handlers.*\nimport stove.spring.standalone.example.infrastructure.messaging.kafka.configuration.KafkaConsumerConfiguration\n\n@Component\n@ConditionalOnProperty(prefix = \"kafka.consumers\", value = [\"enabled\"], havingValue = \"true\")\nclass ProductTransferConsumers(\n  private val objectMapper: ObjectMapper,\n  private val productCreator: ProductCreator\n) {\n  private val logger = LoggerFactory.getLogger(ProductTransferConsumers::class.java)\n\n  @KafkaListener(\n    topics = [\"#{@productCreateEventTopicConfig.topic}\"],\n    groupId = \"#{@consumerConfig.groupId}\",\n    containerFactory = KafkaConsumerConfiguration.LISTENER_BEAN_NAME\n  )\n  @KafkaListener(\n    topics = [\"#{@productCreateEventTopicConfig.retryTopic}\"],\n    groupId = \"#{@consumerConfig.groupId}_retry\",\n    containerFactory = KafkaConsumerConfiguration.RETRY_LISTENER_BEAN_NAME\n  )\n  fun listen(record: ConsumerRecord<*, Any>) = runBlocking(MDCContext()) {\n    logger.info(\"Received product transfer command ${record.value()}\")\n    val command = objectMapper.convertValue(record.value(), CreateProductCommand::class.java)\n    productCreator.create(command.mapToCreateRequest())\n  }\n}\n\ndata class CreateProductCommand(\n  val id: Long,\n  val name: String,\n  val supplierId: Long\n)\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/interceptors/CustomConsumerInterceptor.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.interceptors\n\nimport org.apache.kafka.clients.consumer.*\nimport org.apache.kafka.common.header.Header\nimport org.slf4j.MDC\nimport org.springframework.kafka.listener.RecordInterceptor\nimport org.springframework.stereotype.Component\nimport java.nio.charset.StandardCharsets\n\n/**\n * if we use RecordInterceptor<String, String> we should change\n * it as ConsumerAwareRecordInterceptor<String, String> for the stove e2e testing.\n */\n@Component\nclass CustomConsumerInterceptor : RecordInterceptor<String, String> {\n  override fun intercept(\n    record: ConsumerRecord<String, String>,\n    consumer: Consumer<String, String>\n  ): ConsumerRecord<String, String>? {\n    val contextMap = HashMap<String, String>()\n    record\n      .headers()\n      .filter { it.key().lowercase().startsWith(\"x-\") }\n      .forEach { h: Header ->\n        contextMap[h.key()] = String(h.value(), StandardCharsets.UTF_8)\n      }\n    MDC.setContextMap(contextMap)\n    return record\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/messaging/kafka/interceptors/CustomProducerInterceptor.kt",
    "content": "package stove.spring.standalone.example.infrastructure.messaging.kafka.interceptors\n\nimport org.apache.kafka.clients.producer.*\nimport stove.spring.standalone.example.infrastructure.*\nimport java.time.Instant\nimport java.util.*\n\nclass CustomProducerInterceptor : ProducerInterceptor<String, Any> {\n  companion object {\n    private val DEFAULT_HOST_NAME_AS_BYTE: ByteArray = Defaults.HOST_NAME.toByteArray()\n  }\n\n  override fun onSend(record: ProducerRecord<String, Any>): ProducerRecord<String, Any> {\n    val messageId = UUID.randomUUID().toString()\n    record.headers().add(Headers.MESSAGE_ID_KEY, messageId.toByteArray())\n    record.headers().add(Headers.PUBLISHED_DATE_KEY, Instant.now().toString().toByteArray())\n    record.headers().add(Headers.HOST_KEY, DEFAULT_HOST_NAME_AS_BYTE)\n    record.headers().add(\n      Headers.CORRELATION_ID_KEY,\n      Headers.getOrDefault(Headers.CORRELATION_ID_KEY, messageId).toByteArray()\n    )\n    record.headers().add(Headers.AGENT_NAME_KEY, Defaults.AGENT_NAME.toByteArray())\n    record.headers().add(\n      Headers.USER_EMAIL_KEY,\n      Headers.getOrDefault(Headers.USER_EMAIL_KEY, Defaults.USER_EMAIL).toByteArray()\n    )\n    return record\n  }\n\n  override fun configure(configs: MutableMap<String, *>?) = Unit\n\n  override fun onAcknowledgement(\n    metadata: RecordMetadata?,\n    exception: Exception?\n  ) = Unit\n\n  override fun close() = Unit\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/kotlin/stove/spring/standalone/example/infrastructure/postgres/ExposedConfiguration.kt",
    "content": "package stove.spring.standalone.example.infrastructure.postgres\n\nimport com.zaxxer.hikari.HikariDataSource\nimport org.jetbrains.exposed.v1.jdbc.Database\nimport org.springframework.boot.context.properties.ConfigurationProperties\nimport org.springframework.context.annotation.*\nimport javax.sql.DataSource\n\n@ConfigurationProperties(\"spring.datasource\")\ndata class DataSourceConfig(\n  val url: String,\n  val username: String,\n  val password: String\n)\n\n@Configuration\nclass ExposedConfiguration {\n  @Bean\n  fun dataSource(\n    dataSourceConfig: DataSourceConfig\n  ): DataSource = HikariDataSource().apply {\n    this.jdbcUrl = dataSourceConfig.url\n    this.username = dataSourceConfig.username\n    this.password = dataSourceConfig.password\n  }\n\n  @Bean\n  fun database(dataSource: DataSource): Database = Database.connect(dataSource)\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/main/resources/application.yml",
    "content": "spring:\n  application:\n    name: \"stove\"\n  servlet:\n    multipart:\n      max-request-size: 10MB\n  datasource:\n    url: jdbc:postgresql://localhost:5432/stove\n    username: postgres\n    password: postgres\n    hikari:\n      maximum-pool-size: 10\n      minimum-idle: 2\n\nserver:\n  port: 8001\n  http2:\n    enabled: false\n\n\nhttp-clients:\n  supplier-http:\n    url: http://localhost:9099\n    connectTimeout: 2000\n    readTimeout: 20000\n\nkafka:\n  bootstrapServers: localhost:9092\n  topicPrefix: stove-standalone-example\n  acks: 1\n  secureKafka: false\n  autoCreateTopics: false\n  offset: \"latest\"\n  heartbeatInSeconds: 30\n  request-timeout: \"20000\"\n  default-api-timeout: \"20000\"\n  interceptorClasses: []\n  config:\n    thread-count:\n      basic-listener: 25\n  producer:\n    prefix: stove-standalone-example\n    product-created:\n      topic-name: ${kafka.producer.prefix}.productCreated.1\n  consumers:\n    retryTopicSuffix: retry\n    errorTopicSuffix: error\n    enabled: true\n    groupId: stove-standalone-example\n    product-create:\n      topic: trendyol.stove.service.product.create.0\n      retryTopic: ${kafka.consumers.product-create.topic}.${kafka.consumers.retryTopicSuffix}\n      errorTopic: ${kafka.consumers.product-create.topic}.${kafka.consumers.errorTopicSuffix}\n    product-failing:\n      topic: trendyol.stove.service.product.failing.0\n      retryTopic: ${kafka.consumers.product-failing.topic}.${kafka.consumers.retryTopicSuffix}\n      errorTopic: ${kafka.consumers.product-failing.topic}.${kafka.consumers.errorTopicSuffix}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/test/kotlin/com/stove/spring/standalone/example/e2e/CreateProductsTableMigration.kt",
    "content": "package com.stove.spring.standalone.example.e2e\n\nimport com.trendyol.stove.postgres.PostgresSqlMigrationContext\nimport com.trendyol.stove.postgres.PostgresqlMigration\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\nclass CreateProductsTableMigration : PostgresqlMigration {\n  private val logger: Logger = LoggerFactory.getLogger(CreateProductsTableMigration::class.java)\n\n  override val order: Int = 1\n\n  override suspend fun execute(connection: PostgresSqlMigrationContext) {\n    logger.info(\"Creating products table\")\n    connection.operations.execute(\n      \"\"\"\n      DROP TABLE IF EXISTS products;\n      CREATE TABLE IF NOT EXISTS products (\n        id BIGINT PRIMARY KEY,\n        name VARCHAR(255) NOT NULL,\n        supplier_id BIGINT NOT NULL,\n        created_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP\n      );\n      \"\"\".trimIndent()\n    )\n    logger.info(\"Products table created\")\n  }\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/test/kotlin/com/stove/spring/standalone/example/e2e/ExampleTest.kt",
    "content": "package com.stove.spring.standalone.example.e2e\n\nimport arrow.core.some\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.*\nimport io.kotest.matchers.string.shouldContain\nimport org.jetbrains.exposed.v1.jdbc.Database\nimport org.springframework.http.MediaType\nimport stove.spring.standalone.example.application.handlers.*\nimport stove.spring.standalone.example.application.services.SupplierPermission\nimport stove.spring.standalone.example.infrastructure.messaging.kafka.consumers.CreateProductCommand\nimport kotlin.time.Duration.Companion.seconds\n\nclass ExampleTest :\n  FunSpec({\n    test(\"bridge should work\") {\n      stove {\n        using<Database> {\n          this shouldNotBe null\n        }\n      }\n    }\n\n    test(\"index should be reachable\") {\n      stove {\n        http {\n          get<String>(\"/api/index\", queryParams = mapOf(\"keyword\" to testCase.name.name)) { actual ->\n            actual shouldContain \"Hi from Stove framework with ${testCase.name.name}\"\n            println(actual)\n          }\n          get<String>(\"/api/index\") { actual ->\n            actual shouldContain \"Hi from Stove framework with\"\n            println(actual)\n          }\n        }\n      }\n    }\n\n    test(\"should create new product when send product create request from api for the allowed supplier\") {\n      stove {\n        val request = ProductCreateRequest(1L, name = \"product name\", 99L)\n        val permission = SupplierPermission(request.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${request.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = permission.some()\n          )\n        }\n\n        http {\n          postAndExpectBodilessResponse(uri = \"/api/product/create\", body = request.some()) { actual ->\n            actual.status shouldBe 200\n          }\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent> {\n            actual.id == request.id &&\n              actual.name == request.name &&\n              actual.supplierId == request.supplierId\n          }\n        }\n\n        postgresql {\n          shouldQuery<ProductCreateRequest>(\n            \"SELECT * FROM products WHERE id = ${request.id}\",\n            mapper = { row ->\n              ProductCreateRequest(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.long(\"supplier_id\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first().id shouldBe request.id\n            products.first().name shouldBe request.name\n            products.first().supplierId shouldBe request.supplierId\n          }\n        }\n      }\n    }\n\n    test(\"should throw error when send product create request from api for for the not allowed supplier\") {\n      stove {\n        val request = ProductCreateRequest(2L, name = \"product name\", 98L)\n        val permission = SupplierPermission(request.supplierId, isAllowed = false)\n        wiremock {\n          mockGet(\n            \"/suppliers/${request.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = permission.some()\n          )\n        }\n        http {\n          postAndExpectJson<String>(uri = \"/api/product/create\", body = request.some()) { actual ->\n            actual shouldBe \"Supplier with the given id(${request.supplierId}) is not allowed for product creation\"\n          }\n        }\n      }\n    }\n\n    test(\"should throw error when send product create event for the not allowed supplier\") {\n      stove {\n        val command = CreateProductCommand(3L, name = \"product name\", 97L)\n        val supplierPermission = SupplierPermission(command.supplierId, isAllowed = false)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${command.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        kafka {\n          publish(\"trendyol.stove.service.product.create.0\", command)\n          shouldBeConsumed<CreateProductCommand>(10.seconds) {\n            actual.id == command.id\n          }\n        }\n      }\n    }\n\n    test(\"should create new product when send product create event for the allowed supplier\") {\n      stove {\n        val command = CreateProductCommand(4L, name = \"product name\", 96L)\n        val supplierPermission = SupplierPermission(command.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\n            \"/suppliers/${command.supplierId}/allowed\",\n            statusCode = 200,\n            responseBody = supplierPermission.some()\n          )\n        }\n\n        kafka {\n          publish(\"trendyol.stove.service.product.create.0\", command)\n          shouldBeConsumed<CreateProductCommand> {\n            actual.id == command.id &&\n              actual.name == command.name &&\n              actual.supplierId == command.supplierId\n          }\n\n          shouldBePublished<ProductCreatedEvent> {\n            actual.id == command.id &&\n              actual.name == command.name &&\n              actual.supplierId == command.supplierId &&\n              metadata.headers[\"X-UserEmail\"] == \"stove@trendyol.com\"\n          }\n        }\n\n        postgresql {\n          shouldQuery<ProductCreateRequest>(\n            \"SELECT * FROM products WHERE id = ${command.id}\",\n            mapper = { row ->\n              ProductCreateRequest(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.long(\"supplier_id\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first().id shouldBe command.id\n            products.first().name shouldBe command.name\n            products.first().supplierId shouldBe command.supplierId\n          }\n        }\n      }\n    }\n\n    test(\"when failing event is published then it should be validated\") {\n      data class FailingEvent(\n        val id: Long\n      )\n      stove {\n        kafka {\n          publish(\"trendyol.stove.service.product.failing.0\", FailingEvent(5L))\n          shouldBeFailed<FailingEvent> {\n            actual.id == 5L\n          }\n\n          shouldBeFailed<FailingEvent> {\n            actual == FailingEvent(5L)\n          }\n        }\n      }\n    }\n\n    test(\"file import should work\") {\n      stove {\n        http {\n          postMultipartAndExpectResponse<String>(\n            \"/api/product/import\",\n            body = listOf(\n              StoveMultiPartContent.Text(\"name\", \"product name\"),\n              StoveMultiPartContent.File(\n                \"file\",\n                \"file.txt\",\n                \"file\".toByteArray(),\n                contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE\n              )\n            )\n          ) { actual ->\n            actual.body() shouldBe \"File file.txt is imported with product name and content: file\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "examples/spring-standalone-example/src/test/kotlin/com/stove/spring/standalone/example/e2e/ReportingIntegrationTest.kt",
    "content": "package com.stove.spring.standalone.example.e2e\n\nimport arrow.core.some\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.*\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.comparables.shouldBeGreaterThan\nimport io.kotest.matchers.string.shouldContain\nimport stove.spring.standalone.example.application.handlers.*\nimport stove.spring.standalone.example.application.services.SupplierPermission\nimport java.util.*\nimport kotlin.time.Duration.Companion.seconds\n\nclass ReportingIntegrationTest :\n  FunSpec({\n    test(\"report should capture HTTP and Kafka operations\") {\n      stove {\n        val request = ProductCreateRequest(100L, \"test product\", 1L)\n        val permission = SupplierPermission(request.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\"/suppliers/${permission.id}/allowed\", 200, permission.some())\n        }\n\n        http {\n          postAndExpectBodilessResponse(\"/api/product/create\", request.some()) {\n            it.status shouldBe 200\n          }\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent> {\n            actual.id == request.id\n          }\n        }\n      }\n\n      // Validate the report contents\n      val report = Stove.reporter().currentTest()\n\n      // Should have WireMock stub action\n      report\n        .entries()\n        .any { it.system == \"WireMock\" && it.action.contains(\"GET /suppliers\") } shouldBe true\n\n      // Should have HTTP action\n      report\n        .entries()\n        .any { it.system == \"HTTP\" && it.action.contains(\"POST /api/product\") } shouldBe true\n\n      // Should have Kafka assertion\n      report\n        .entries()\n        .any { it.system == \"Kafka\" && it.action.contains(\"shouldBePublished\") } shouldBe true\n    }\n\n    test(\"report should include Kafka MessageStore snapshot on failure\") {\n      try {\n        stove {\n          kafka {\n            publish(\"orders.test\", mapOf(\"id\" to 1))\n\n            // This will fail - no consumer for this topic\n            shouldBePublished<Map<String, Any>>(atLeastIn = 1.seconds) {\n              actual[\"nonexistent\"] == true\n            }\n          }\n        }\n      } catch (_: Throwable) {\n        // Expected - can be TimeoutCancellationException, AssertionError, or wrapped exception\n      }\n\n      // Get the snapshot\n      val snapshot = Stove.getSystem<KafkaSystem>(KafkaSystem::class).snapshot()\n      snapshot shouldNotBe null\n      snapshot.system shouldBe \"Kafka\"\n      (snapshot.state[\"published\"] as? List<*>)?.size?.shouldBeGreaterThan(0)\n    }\n\n    test(\"report should capture PostgreSQL operations\") {\n      stove {\n        val request = ProductCreateRequest(200L, \"postgres test\", 1L)\n        val permission = SupplierPermission(request.supplierId, isAllowed = true)\n\n        wiremock {\n          mockGet(\"/suppliers/${permission.id}/allowed\", 200, permission.some())\n        }\n\n        http {\n          postAndExpectBodilessResponse(\"/api/product/create\", request.some()) {\n            it.status shouldBe 200\n          }\n        }\n\n        postgresql {\n          shouldQuery<ProductCreateRequest>(\n            \"SELECT * FROM products WHERE id = ${request.id}\",\n            mapper = { row ->\n              ProductCreateRequest(\n                id = row.long(\"id\"),\n                name = row.string(\"name\"),\n                supplierId = row.long(\"supplier_id\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first().id shouldBe request.id\n          }\n        }\n      }\n\n      val report = Stove.reporter().currentTest()\n\n      // Should have PostgreSQL action with result\n      report\n        .entries()\n        .any { it.system == \"PostgreSQL\" && it.action.contains(\"Query\") } shouldBe true\n    }\n\n    test(\"report should capture multiple system interactions\") {\n      stove {\n        val request = ProductCreateRequest(300L, \"multi-system test\", 1L)\n        val permission = SupplierPermission(request.supplierId, isAllowed = true)\n\n        // WireMock\n        wiremock {\n          mockGet(\"/suppliers/${permission.id}/allowed\", 200, permission.some())\n        }\n\n        // HTTP\n        http {\n          postAndExpectBodilessResponse(\"/api/product/create\", request.some()) {\n            it.status shouldBe 200\n          }\n        }\n\n        // Kafka\n        kafka {\n          shouldBePublished<ProductCreatedEvent> {\n            actual.id == request.id\n          }\n        }\n\n        // PostgreSQL\n        postgresql {\n          shouldQuery<ProductCreateRequest>(\n            \"SELECT * FROM products WHERE id = ${request.id}\",\n            mapper = { row ->\n              ProductCreateRequest(\n                id = row.long(\"id\"),\n                name = row.string(\"name\"),\n                supplierId = row.long(\"supplier_id\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first().id shouldBe request.id\n          }\n        }\n      }\n\n      val report = Stove.reporter().currentTest()\n      // Use entriesForThisTest() to filter only entries for this specific test\n      val entries = report.entriesForThisTest()\n\n      // Should have entries from all systems\n      entries.filter { it.system == \"WireMock\" } shouldHaveSize 1\n      entries.filter { it.system == \"HTTP\" } shouldHaveSize 1\n      entries.filter { it.system == \"Kafka\" } shouldHaveSize 1\n      entries.filter { it.system == \"PostgreSQL\" } shouldHaveSize 1\n    }\n\n    test(\"report should be renderable as JSON\") {\n      stove {\n        http {\n          get<String>(\"/api/index\") {\n            it shouldContain \"Hi from Stove framework\"\n          }\n        }\n      }\n\n      val report = Stove.reporter().currentTest()\n      val json = JsonReportRenderer.render(report, emptyList())\n\n      json shouldContain \"testId\"\n      json shouldContain \"testName\"\n      json shouldContain \"entries\"\n      json shouldContain \"summary\"\n      json shouldContain \"HTTP\"\n    }\n\n    test(\"report should be renderable as pretty console\") {\n      stove {\n        http {\n          get<String>(\"/api/index\") {\n            it shouldContain \"Hi from Stove framework\"\n          }\n        }\n      }\n\n      val report = Stove.reporter().currentTest()\n      val pretty = PrettyConsoleRenderer.render(report, emptyList())\n\n      pretty shouldContain \"STOVE TEST EXECUTION REPORT\"\n      pretty shouldContain \"TIMELINE\"\n      pretty shouldContain \"HTTP\"\n      pretty shouldContain \"GET /api/index\"\n      pretty shouldContain \"╭\"\n      pretty shouldContain \"╰\"\n    }\n  })\n"
  },
  {
    "path": "examples/spring-standalone-example/src/test/kotlin/com/stove/spring/standalone/example/e2e/StoveConfig.kt",
    "content": "package com.stove.spring.standalone.example.e2e\n\nimport com.trendyol.stove.dashboard.DashboardSystemOptions\nimport com.trendyol.stove.dashboard.dashboard\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.postgres.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.tracing.tracing\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.slf4j.*\nimport stove.spring.standalone.example.infrastructure.ObjectMapperConfig\nimport stove.spring.standalone.example.run\n\nclass StoveConfig : AbstractProjectConfig() {\n  private val logger: Logger = LoggerFactory.getLogger(\"WireMockMonitor\")\n  private val appPort = PortFinder.findAvailablePort()\n\n  init {\n    stoveKafkaBridgePortDefault = PortFinder.findAvailablePortAsString()\n  }\n\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  @Suppress(\"LongMethod\")\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:$appPort\"\n          )\n        }\n        postgresql {\n          PostgresqlOptions(\n            databaseName = \"stove\",\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"spring.datasource.url=${cfg.jdbcUrl}\",\n                \"spring.datasource.username=${cfg.username}\",\n                \"spring.datasource.password=${cfg.password}\"\n              )\n            }\n          ).migrations {\n            register<CreateProductsTableMigration>()\n          }\n        }\n        kafka {\n          KafkaSystemOptions(\n            useEmbeddedKafka = true,\n            topicSuffixes = TopicSuffixes().copy(error = listOf(\".error\", \".DLT\", \"dlt\")),\n            serde = StoveSerde.jackson.anyByteArraySerde(ObjectMapperConfig.default),\n            containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n          ) {\n            listOf(\n              \"kafka.bootstrapServers=${it.bootstrapServers}\",\n              \"kafka.isSecure=false\",\n              \"kafka.interceptorClasses=${it.interceptorClass}\"\n            )\n          }\n        }\n        bridge()\n        tracing { enableSpanReceiver() }\n        dashboard { DashboardSystemOptions(appName = \"spring-standalone-example\") }\n        wiremock {\n          WireMockSystemOptions(\n            port = 0,\n            removeStubAfterRequestMatched = true,\n            afterRequest = { e, _ ->\n              logger.info(e.request.toString())\n            },\n            configureExposedConfiguration = { cfg ->\n              listOf(\"http-clients.supplier-http.url=${cfg.baseUrl}\")\n            }\n          )\n        }\n        springBoot(\n          runner = { parameters ->\n            run(parameters)\n          },\n          withParameters = listOf(\n            \"server.port=$appPort\",\n            \"logging.level.root=info\",\n            \"logging.level.org.springframework.web=info\",\n            \"spring.profiles.active=default\",\n            \"kafka.heartbeatInSeconds=2\",\n            \"kafka.autoCreateTopics=true\",\n            \"kafka.offset=earliest\",\n            \"kafka.secureKafka=false\"\n          )\n        )\n      }.run()\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n"
  },
  {
    "path": "examples/spring-standalone-example/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.stove.spring.standalone.example.e2e.StoveConfig\n"
  },
  {
    "path": "examples/spring-standalone-example/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "examples/spring-streams-example/build.gradle.kts",
    "content": "import com.google.protobuf.gradle.id\nimport com.trendyol.stove.gradle.stoveTracing\n\nplugins {\n  alias(libs.plugins.spring.plugin)\n  alias(libs.plugins.spring.boot.three)\n  alias(libs.plugins.spring.dependencyManagement)\n  alias(libs.plugins.protobuf)\n  idea\n  application\n}\n\ndependencies {\n  implementation(libs.spring.boot.three)\n  implementation(libs.spring.boot.three.autoconfigure)\n  annotationProcessor(libs.spring.boot.three.annotationProcessor)\n  implementation(libs.spring.boot.three.kafka)\n  implementation(libs.jackson.kotlin)\n  implementation(libs.kafka)\n  implementation(libs.kafka.streams)\n  implementation(libs.kotlin.reflect)\n  implementation(libs.google.protobuf.kotlin)\n  implementation(libs.kafka.streams.protobuf.serde)\n}\n\ndependencies {\n  testImplementation(projects.stove.testExtensions.stoveExtensionsKotest)\n  testImplementation(projects.stove.lib.stoveKafka)\n  testImplementation(projects.stove.lib.stoveTracing)\n  testImplementation(projects.stove.lib.stoveDashboard)\n  testImplementation(projects.stove.starters.spring.stoveSpring)\n  testImplementation(libs.kotlinx.core)\n  testImplementation(libs.testcontainers.kafka)\n}\n\napplication { mainClass.set(\"stove.spring.streams.example.ExampleAppkt\") }\n\njava.sourceSets[\"main\"].java {\n  srcDir(\"build/generated/source/proto/main/java\")\n  srcDir(\"build/generated/source/proto/main/kotlin\")\n}\n\ntasks.withType<Test> {\n  useJUnitPlatform()\n}\n\nprotobuf {\n  protoc {\n    artifact = libs.protoc.get().toString()\n  }\n\n  generateProtoTasks {\n    all().forEach {\n      // If true, the descriptor set will contain line number information\n      // and comments. Default is false.\n      it.descriptorSetOptions.includeSourceInfo = true\n\n      // If true, the descriptor set will contain all transitive imports and\n      // is therefore self-contained. Default is false.\n      it.descriptorSetOptions.includeImports = true\n      it.builtins {\n        id(\"kotlin\")\n      }\n    }\n  }\n}\n\nconfigurations.matching { it.name == \"detekt\" }.all {\n  resolutionStrategy.eachDependency {\n    if (requested.group == \"org.jetbrains.kotlin\") {\n      @Suppress(\"UnstableApiUsage\")\n      useVersion(io.gitlab.arturbosch.detekt.getSupportedKotlinVersion())\n    }\n  }\n}\n\nstoveTracing {\n  serviceName = \"spring-streams-example\"\n  otelAgentVersion = libs.versions.opentelemetry.instrumentation.get()\n}\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/kotlin/stove/spring/streams/example/ExampleApp.kt",
    "content": "package stove.spring.streams.example\n\nimport org.springframework.boot.*\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.context.ConfigurableApplicationContext\n\n@SpringBootApplication\nclass ExampleApp\n\nfun main(args: Array<String>) {\n  run(args)\n}\n\n/**\n * This is the point where spring application gets run.\n * run(args, init) method is the important point for the testing configuration.\n * init allows us to override any dependency from the testing side that is being time related or configuration related.\n * Spring itself opens this configuration higher order function to the outside.\n */\nfun run(\n  args: Array<String>,\n  init: SpringApplication.() -> Unit = {}\n): ConfigurableApplicationContext = runApplication<ExampleApp>(*args, init = init)\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/kotlin/stove/spring/streams/example/kafka/CustomDeserializationExceptionHandler.kt",
    "content": "package stove.spring.streams.example.kafka\n\nimport org.apache.kafka.clients.consumer.ConsumerRecord\nimport org.apache.kafka.streams.errors.*\nimport org.slf4j.LoggerFactory\n\nclass CustomDeserializationExceptionHandler : DeserializationExceptionHandler {\n  companion object {\n    private val logger = LoggerFactory.getLogger(CustomDeserializationExceptionHandler::class.java)\n  }\n\n  override fun handleError(\n    context: ErrorHandlerContext,\n    record: ConsumerRecord<ByteArray?, ByteArray>,\n    exception: Exception?\n  ): DeserializationExceptionHandler.Response {\n    logger.error(\n      \"Deserialization exception in [${record.topic()}]: [${exception?.message}] Caused by: ${exception?.cause?.message}\"\n    )\n    return DeserializationExceptionHandler.Response.resume()\n  }\n\n  override fun configure(configs: MutableMap<String, *>?) = Unit\n}\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/kotlin/stove/spring/streams/example/kafka/CustomProductionExceptionHandler.kt",
    "content": "package stove.spring.streams.example.kafka\n\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.apache.kafka.streams.errors.*\nimport org.slf4j.LoggerFactory\n\nclass CustomProductionExceptionHandler : ProductionExceptionHandler {\n  companion object {\n    private val logger = LoggerFactory.getLogger(CustomProductionExceptionHandler::class.java)\n  }\n\n  override fun handleError(\n    context: ErrorHandlerContext?,\n    record: ProducerRecord<ByteArray?, ByteArray?>?,\n    exception: Exception?\n  ): ProductionExceptionHandler.Response? {\n    logger.error(\n      \"Production exception in [${record?.topic()}]: [${exception?.message}] Caused by: ${exception?.cause?.message}\"\n    )\n    return ProductionExceptionHandler.Response.resume()\n  }\n\n  override fun configure(configs: MutableMap<String, *>) = Unit\n}\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/kotlin/stove/spring/streams/example/kafka/CustomSerDe.kt",
    "content": "package stove.spring.streams.example.kafka\n\nimport com.google.protobuf.Message\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaProvider\nimport io.confluent.kafka.schemaregistry.testutil.MockSchemaRegistry\nimport io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG\nimport io.confluent.kafka.streams.serdes.protobuf.KafkaProtobufSerde\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Component\n\n@Component\nclass CustomSerDe {\n  @Value(\"\\${kafka.schema-registry-url}\")\n  val schemaRegistryUrl = \"\"\n\n  fun createSerdeForValues(): KafkaProtobufSerde<Message> = KafkaRegistry.createSerde(schemaRegistryUrl)\n}\n\nsealed class KafkaRegistry(\n  open val url: String\n) {\n  object Mock : KafkaRegistry(\"mock://mock-registry\")\n\n  data class Defined(\n    override val url: String\n  ) : KafkaRegistry(url)\n\n  companion object {\n    fun createSerde(fromUrl: String): KafkaProtobufSerde<Message> = createSerde(\n      if (fromUrl.contains(Mock.url)) Mock else Defined(fromUrl)\n    )\n\n    fun createSerde(registry: KafkaRegistry = Mock): KafkaProtobufSerde<Message> {\n      val schemaRegistryClient = when (registry) {\n        is Mock -> MockSchemaRegistry.getClientForScope(\"mock-registry\", listOf(ProtobufSchemaProvider()))\n        is Defined -> MockSchemaRegistry.getClientForScope(registry.url, listOf(ProtobufSchemaProvider()))\n      }\n      val serde: KafkaProtobufSerde<Message> = KafkaProtobufSerde<Message>(schemaRegistryClient)\n      val serdeConfig: MutableMap<String, Any?> = HashMap()\n      serdeConfig[SCHEMA_REGISTRY_URL_CONFIG] = registry.url\n      serde.configure(serdeConfig, false)\n      return serde\n    }\n  }\n}\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/kotlin/stove/spring/streams/example/kafka/StreamsConfig.kt",
    "content": "package stove.spring.streams.example.kafka\n\nimport org.apache.kafka.clients.consumer.ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG\nimport org.apache.kafka.common.serialization.Serdes\nimport org.apache.kafka.streams.StreamsConfig.*\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.context.annotation.*\nimport org.springframework.kafka.annotation.*\nimport org.springframework.kafka.config.KafkaStreamsConfiguration\n\n@Configuration\n@EnableKafka\n@EnableKafkaStreams\nclass StreamsConfig {\n  @Value(\"\\${spring.kafka.streams.bootstrap-servers}\")\n  val bootstrapServers: String = \"\"\n\n  @Value(\"\\${kafka.interceptorClasses}\")\n  val interceptorClass = emptyList<String>()\n\n  @Bean(name = [KafkaStreamsDefaultConfiguration.DEFAULT_STREAMS_CONFIG_BEAN_NAME])\n  fun kStreamsConfig(): KafkaStreamsConfiguration {\n    val props: MutableMap<String, Any?> = HashMap()\n    props[APPLICATION_ID_CONFIG] = \"stove.example\"\n    props[BOOTSTRAP_SERVERS_CONFIG] = bootstrapServers\n    props[DEFAULT_KEY_SERDE_CLASS_CONFIG] = Serdes.String().javaClass.name\n    props[DEFAULT_VALUE_SERDE_CLASS_CONFIG] = Serdes.String().javaClass.name\n    props[COMMIT_INTERVAL_MS_CONFIG] = 0\n    props[DESERIALIZATION_EXCEPTION_HANDLER_CLASS_CONFIG] = CustomDeserializationExceptionHandler::class.java\n    props[PRODUCTION_EXCEPTION_HANDLER_CLASS_CONFIG] = CustomProductionExceptionHandler::class.java\n    props[INTERCEPTOR_CLASSES_CONFIG] = interceptorClass\n\n    return KafkaStreamsConfiguration(props)\n  }\n}\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/kotlin/stove/spring/streams/example/kafka/application/processor/ExampleJoin.kt",
    "content": "package stove.spring.streams.example.kafka.application.processor\n\nimport com.google.protobuf.Message\nimport io.confluent.kafka.streams.serdes.protobuf.KafkaProtobufSerde\nimport org.apache.kafka.common.serialization.*\nimport org.apache.kafka.streams.StreamsBuilder\nimport org.apache.kafka.streams.kstream.*\nimport org.springframework.beans.factory.annotation.Autowired\nimport org.springframework.kafka.annotation.*\nimport org.springframework.stereotype.Component\nimport stove.example.protobuf.Input1Value.Input1\nimport stove.example.protobuf.Input2Value.Input2\nimport stove.example.protobuf.output\nimport stove.spring.streams.example.kafka.CustomSerDe\n\n@Component\n@EnableKafka\n@EnableKafkaStreams\nclass ExampleJoin(\n  customSerDe: CustomSerDe\n) {\n  private val protobufSerde: KafkaProtobufSerde<Message> = customSerDe.createSerdeForValues()\n  private val byteArraySerde: Serde<ByteArray> = Serdes.ByteArray()\n  private val stringSerde: Serde<String> = Serdes.String()\n\n  @Autowired\n  fun buildPipeline(streamsBuilder: StreamsBuilder) {\n    val input1: KTable<String, Message> = streamsBuilder\n      .stream(\"input1\", Consumed.with(stringSerde, protobufSerde))\n      .toTable(Materialized.with(stringSerde, protobufSerde))\n\n    val input2: KTable<String, Message> = streamsBuilder\n      .stream(\"input2\", Consumed.with(stringSerde, protobufSerde))\n      .toTable(Materialized.with(stringSerde, protobufSerde))\n\n    val joinedTable = input1.join(\n      input2,\n      { input1Message: Message, input2Message: Message ->\n        protobufSerde.serializer().serialize(\n          \"output\",\n          output {\n            this.firstName = Input1.parseFrom(input1Message.toByteArray()).firstName\n            this.lastName = Input1.parseFrom(input1Message.toByteArray()).lastName\n            this.bsn = Input2.parseFrom(input2Message.toByteArray()).bsn\n            this.age = Input2.parseFrom(input2Message.toByteArray()).age\n          }\n        )\n      }\n    )\n    joinedTable.toStream().to(\"output\", Produced.with(stringSerde, byteArraySerde))\n  }\n}\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/proto/Input1-value.proto",
    "content": "syntax = \"proto3\";\npackage stove.example.protobuf;\n\nmessage Input1 {\n  string firstName = 1;\n  string lastName = 2;\n}\n\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/proto/Input2-value.proto",
    "content": "syntax = \"proto3\";\npackage stove.example.protobuf;\n\nmessage Input2 {\n  string bsn = 1;\n  int32 age = 2;\n}\n\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/proto/Output-value.proto",
    "content": "syntax = \"proto3\";\npackage stove.example.protobuf;\n\nmessage Output {\n  string firstName = 1;\n  string lastName = 2;\n  string bsn = 3;\n  int32 age = 4;\n}\n\n"
  },
  {
    "path": "examples/spring-streams-example/src/main/resources/application.properties",
    "content": "spring.application.name= stove.example\nspring.kafka.consumer.bootstrap-servers = localhost:9092\nspring.kafka.producer.bootstrap-servers = localhost:9092\nspring.kafka.streams.bootstrap-servers = localhost:9092\nspring.kafka.consumer.group-id= group_id\nspring.kafka.consumer.auto-offset-reset = earliest\nspring.kafka.consumer.key-deserializer= org.apache.kafka.common.serialization.StringDeserializer\nspring.kafka.consumer.value-deserializer = org.apache.kafka.common.serialization.StringDeserializer\nspring.kafka.producer.value-serializer= org.apache.kafka.common.serialization.ByteArraySerializer\nkafka.interceptorClasses=\nkafka.schema-registry-url= http://localhost:8089\nspring.kafka.streams.cleanup.on-startup=true\n\n"
  },
  {
    "path": "examples/spring-streams-example/src/test/kotlin/com/stove/spring/streams/example/e2e/ExampleTest.kt",
    "content": "package com.stove.spring.streams.example.e2e\n\nimport arrow.core.Option\nimport com.google.protobuf.Message\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport org.apache.kafka.common.serialization.StringDeserializer\nimport stove.example.protobuf.*\nimport stove.example.protobuf.Input1Value.Input1\nimport stove.example.protobuf.Input2Value.Input2\nimport stove.example.protobuf.OutputValue.Output\nimport java.util.*\nimport kotlin.time.Duration.Companion.seconds\n\nclass ExampleTest :\n  FunSpec({\n    test(\"expect join\") {\n    /*-------------------------\n      |  Create test data\n      --------------------------*/\n      val firstName = UUID.randomUUID().toString()\n      val lastName = UUID.randomUUID().toString()\n      val bsn = UUID.randomUUID().toString()\n      val age = 18\n\n      // create input\n      val input1Message = input1 {\n        this.firstName = firstName\n        this.lastName = lastName\n      }\n      val input2Message = input2 {\n        this.bsn = bsn\n        this.age = age\n      }\n      val outputMessage = output {\n        this.firstName = firstName\n        this.lastName = lastName\n        this.bsn = bsn\n        this.age = age\n      }\n\n      stove {\n        kafka {\n        /*-------------------------\n         |  publish kafka messages\n         --------------------------*/\n\n          // inputs\n          publish(INPUT_TOPIC, input1Message, Option(\"test\"))\n          publish(INPUT_TOPIC2, input2Message, Option(\"test\"))\n\n        /*---------------------------\n         |  verify messages consumed\n         ----------------------------*/\n\n          //  Assert input1 message is consumed\n          shouldBeConsumed<Input1> {\n            actual == input1Message\n          }\n\n          //  Assert input2 message is consumed\n          shouldBeConsumed<Input2> {\n            actual == input2Message\n          }\n\n        /*---------------------------\n         |  verify messages published\n         ----------------------------*/\n\n          // Assert joined message is correctly published\n          shouldBePublished<Output>(atLeastIn = 20.seconds) {\n            actual.bsn == bsn\n          }\n\n          // Assert joined message is correctly published\n          // Similar to test above, but is able to run even if no messages are published\n          consumer<String, Message>(\n            \"output\",\n            valueDeserializer = StoveKafkaValueDeserializer(),\n            keyDeserializer = StringDeserializer()\n          ) { record ->\n            if (Output.parseFrom(record.value().toByteArray()) != outputMessage) throw AssertionError()\n          }\n        }\n      }\n    }\n  }) {\n  companion object {\n    const val INPUT_TOPIC = \"input1\"\n    const val INPUT_TOPIC2 = \"input2\"\n    const val OUTPUT_TOPIC = \"output\"\n  }\n}\n"
  },
  {
    "path": "examples/spring-streams-example/src/test/kotlin/com/stove/spring/streams/example/e2e/StoveConfig.kt",
    "content": "package com.stove.spring.streams.example.e2e\n\nimport com.stove.spring.streams.example.e2e.ExampleTest.Companion.INPUT_TOPIC\nimport com.stove.spring.streams.example.e2e.ExampleTest.Companion.INPUT_TOPIC2\nimport com.stove.spring.streams.example.e2e.ExampleTest.Companion.OUTPUT_TOPIC\nimport com.trendyol.stove.dashboard.DashboardSystemOptions\nimport com.trendyol.stove.dashboard.dashboard\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.tracing.tracing\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.apache.kafka.clients.admin.NewTopic\nimport stove.spring.streams.example.run\n\nclass StoveConfig : AbstractProjectConfig() {\n  private val appPort = PortFinder.findAvailablePort()\n\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  @Suppress(\"LongMethod\")\n  override suspend fun beforeProject(): Unit = Stove()\n    .also {\n      stoveKafkaBridgePortDefault = PortFinder.findAvailablePortAsString()\n    }.with {\n      kafka {\n        KafkaSystemOptions(\n          listenPublishedMessagesFromStove = false,\n          serde = StoveProtobufSerde(),\n          valueSerializer = StoveKafkaValueSerializer(),\n          containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n        ) {\n          listOf(\n            \"kafka.bootstrapServers=${it.bootstrapServers}\",\n            \"kafka.isSecure=false\",\n            \"kafka.interceptorClasses=${it.interceptorClass}\",\n            \"spring.kafka.streams.bootstrap-servers=${it.bootstrapServers}\",\n            \"spring.kafka.producer.bootstrap-servers=${it.bootstrapServers}\",\n            \"spring.kafka.consumer.bootstrap-servers=${it.bootstrapServers}\"\n          )\n        }\n      }\n      bridge()\n      tracing { enableSpanReceiver() }\n      dashboard { DashboardSystemOptions(appName = \"spring-streams-example\") }\n      springBoot(\n        runner = { parameters ->\n          run(parameters)\n        },\n        withParameters = listOf(\n          \"server.port=$appPort\",\n          \"logging.level.root=info\",\n          \"logging.level.org.springframework.web=info\",\n          \"spring.profiles.active=default\",\n          \"kafka.heartbeatInSeconds=2\",\n          \"kafka.autoCreateTopics=true\",\n          \"kafka.offset=earliest\",\n          \"kafka.secureKafka=false\",\n          \"kafka.topic.create-topics=true\",\n          \"kafka.schema-registry-url=mock://mock-registry\"\n        )\n      )\n    }.run()\n    .also {\n      stove {\n        kafka {\n          adminOperations {\n            createTopics(\n              listOf(\n                NewTopic(INPUT_TOPIC, 1, 1),\n                NewTopic(INPUT_TOPIC2, 1, 1),\n                NewTopic(OUTPUT_TOPIC, 1, 1)\n              )\n            )\n          }\n        }\n      }\n    }\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n"
  },
  {
    "path": "examples/spring-streams-example/src/test/kotlin/com/stove/spring/streams/example/e2e/TestHelper.kt",
    "content": "package com.stove.spring.streams.example.e2e\n\nimport arrow.core.*\nimport com.google.protobuf.Message\nimport com.trendyol.stove.serialization.StoveSerde\nimport io.confluent.kafka.streams.serdes.protobuf.KafkaProtobufSerde\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport okio.ByteString.Companion.toByteString\nimport org.apache.kafka.common.serialization.*\nimport stove.spring.streams.example.kafka.KafkaRegistry\nimport java.util.*\n\nclass StoveKafkaValueSerializer<T : Any> : Serializer<T> {\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde(KafkaRegistry.Mock)\n\n  override fun serialize(\n    topic: String,\n    data: T\n  ): ByteArray = when (data) {\n    is ByteArray -> data\n    else -> protobufSerde.serializer().serialize(topic, data as Message)\n  }\n}\n\nclass StoveKafkaValueDeserializer : Deserializer<Message> {\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde(KafkaRegistry.Mock)\n\n  override fun deserialize(\n    topic: String,\n    data: ByteArray\n  ): Message = protobufSerde.deserializer().deserialize(topic, data)\n}\n\n@Suppress(\"UNCHECKED_CAST\")\n@OptIn(ExperimentalSerializationApi::class)\nclass StoveProtobufSerde : StoveSerde<Any, ByteArray> {\n  private val parseFromMethod = \"parseFrom\"\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde(KafkaRegistry.Mock)\n\n  override fun serialize(value: Any): ByteArray = protobufSerde.serializer().serialize(\"any\", value as Message)\n\n  override fun <T : Any> deserialize(value: ByteArray, clazz: Class<T>): T {\n    val incoming: Message = protobufSerde.deserializer().deserialize(\"any\", value)\n    incoming.isAssignableFrom(clazz).also { isAssignableFrom ->\n      require(isAssignableFrom) {\n        \"Expected '${clazz.simpleName}' but got '${incoming.descriptorForType.name}'. \" +\n          \"This could be transient ser/de problem since the message stream is constantly checked if the expected message is arrived, \" +\n          \"so you can ignore this error if you are sure that the message is the expected one.\"\n      }\n    }\n\n    val parseFromMethod = clazz.getDeclaredMethod(parseFromMethod, ByteArray::class.java)\n    val parsed = parseFromMethod(incoming, incoming.toByteArray()) as T\n    return parsed\n  }\n}\n\nprivate fun Message.isAssignableFrom(clazz: Class<*>): Boolean = this.descriptorForType.name == clazz.simpleName\n\nfun KafkaProtobufSerde<Message>.messageAsBase64(\n  message: Any\n): Option<Message> = Either\n  .catch {\n    deserializer()\n      .deserialize(\n        \"any\",\n        Base64\n          .getDecoder()\n          .decode(message.toString())\n          .toByteString()\n          .toByteArray()\n      )\n  }.getOrNull()\n  .toOption()\n\nfun Message.onMatchingAssert(\n  descriptor: String,\n  assert: (message: Message) -> Boolean\n): Boolean = descriptor == descriptorForType.name && assert(this)\n"
  },
  {
    "path": "examples/spring-streams-example/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.stove.spring.streams.example.e2e.StoveConfig\n"
  },
  {
    "path": "examples/spring-streams-example/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "go/stove-kafka/bridge.go",
    "content": "// Package stovekafka provides a Kafka message bridge for Stove e2e testing.\n//\n// It forwards produced/consumed Kafka messages via gRPC to Stove's\n// StoveKafkaObserverGrpcServer, enabling shouldBeConsumed and\n// shouldBePublished assertions in Kotlin tests.\n//\n// The core bridge is library-agnostic. Use the appropriate subpackage\n// for your Kafka client:\n//\n//   - sarama  — IBM/sarama interceptors\n//   - franz   — twmb/franz-go hooks\n//   - segmentio — segmentio/kafka-go helpers\n//\n// Example with IBM/sarama:\n//\n//\tbridge, _ := stovekafka.NewBridgeFromEnv()\n//\tconfig.Producer.Interceptors = []sarama.ProducerInterceptor{\n//\t    &stovesarama.ProducerInterceptor{Bridge: bridge},\n//\t}\n//\n// Example with franz-go:\n//\n//\tbridge, _ := stovekafka.NewBridgeFromEnv()\n//\tclient, _ := kgo.NewClient(kgo.WithHooks(&franz.Hook{Bridge: bridge}))\n//\n// Example with kafka-go:\n//\n//\tbridge, _ := stovekafka.NewBridgeFromEnv()\n//\t_ = writer.WriteMessages(ctx, msgs...)\n//\tsegmentio.ReportWritten(ctx, bridge, msgs...)\npackage stovekafka\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/trendyol/stove/go/stove-kafka/stoveobserver\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\n// PublishedMessage is a library-agnostic representation of a produced Kafka message.\ntype PublishedMessage struct {\n\tTopic   string\n\tKey     string\n\tValue   []byte\n\tHeaders map[string]string\n}\n\n// ConsumedMessage is a library-agnostic representation of a consumed Kafka message.\ntype ConsumedMessage struct {\n\tTopic     string\n\tKey       string\n\tValue     []byte\n\tPartition int32\n\tOffset    int64\n\tHeaders   map[string]string\n}\n\nconst envBridgePort = \"STOVE_KAFKA_BRIDGE_PORT\"\nconst envBridgeHost = \"STOVE_KAFKA_BRIDGE_HOST\"\n\n// Bridge wraps the gRPC client to the Stove Kafka observer server.\n// A nil Bridge is safe to use — all methods are no-ops.\ntype Bridge struct {\n\tclient stoveobserver.StoveKafkaObserverServiceClient\n\tconn   *grpc.ClientConn\n}\n\n// NewBridge connects to the Stove observer on the given port.\n// Returns (nil, nil) if port is empty (production mode — zero overhead).\nfunc NewBridge(port string) (*Bridge, error) {\n\tif port == \"\" {\n\t\treturn nil, nil\n\t}\n\n\thost := os.Getenv(envBridgeHost)\n\tif host == \"\" {\n\t\thost = \"localhost\"\n\t}\n\n\ttarget := fmt.Sprintf(\"%s:%s\", host, port)\n\tconn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"stove bridge: failed to connect to %s: %w\", target, err)\n\t}\n\n\tlog.Printf(\"Stove Kafka bridge connected to %s\", target)\n\treturn &Bridge{\n\t\tclient: stoveobserver.NewStoveKafkaObserverServiceClient(conn),\n\t\tconn:   conn,\n\t}, nil\n}\n\n// NewBridgeFromEnv reads the STOVE_KAFKA_BRIDGE_PORT environment variable.\n// Returns (nil, nil) if not set (production mode).\nfunc NewBridgeFromEnv() (*Bridge, error) {\n\treturn NewBridge(os.Getenv(envBridgePort))\n}\n\n// Close shuts down the gRPC connection. Safe to call on nil Bridge.\nfunc (b *Bridge) Close() error {\n\tif b == nil {\n\t\treturn nil\n\t}\n\treturn b.conn.Close()\n}\n\n// ReportPublished sends a published message to the Stove observer.\n// Safe to call on nil Bridge (no-op).\nfunc (b *Bridge) ReportPublished(ctx context.Context, msg *PublishedMessage) error {\n\tif b == nil {\n\t\treturn nil\n\t}\n\n\t_, err := b.client.OnPublishedMessage(ctx, &stoveobserver.PublishedMessage{\n\t\tId:      uuid.New().String(),\n\t\tMessage: msg.Value,\n\t\tTopic:   msg.Topic,\n\t\tKey:     msg.Key,\n\t\tHeaders: msg.Headers,\n\t})\n\tif err != nil {\n\t\tlog.Printf(\"stove bridge: failed to report published message: %v\", err)\n\t}\n\treturn err\n}\n\n// ReportConsumed sends a consumed message to the Stove observer.\n// Safe to call on nil Bridge (no-op).\nfunc (b *Bridge) ReportConsumed(ctx context.Context, msg *ConsumedMessage) error {\n\tif b == nil {\n\t\treturn nil\n\t}\n\n\t_, err := b.client.OnConsumedMessage(ctx, &stoveobserver.ConsumedMessage{\n\t\tId:        uuid.New().String(),\n\t\tMessage:   msg.Value,\n\t\tTopic:     msg.Topic,\n\t\tPartition: msg.Partition,\n\t\tOffset:    msg.Offset,\n\t\tKey:       msg.Key,\n\t\tHeaders:   msg.Headers,\n\t})\n\tif err != nil {\n\t\tlog.Printf(\"stove bridge: failed to report consumed message: %v\", err)\n\t}\n\treturn err\n}\n\n// ReportCommitted sends a committed offset to the Stove observer.\n// Safe to call on nil Bridge (no-op).\nfunc (b *Bridge) ReportCommitted(ctx context.Context, topic string, partition int32, offset int64) error {\n\tif b == nil {\n\t\treturn nil\n\t}\n\n\t_, err := b.client.OnCommittedMessage(ctx, &stoveobserver.CommittedMessage{\n\t\tId:        uuid.New().String(),\n\t\tTopic:     topic,\n\t\tPartition: partition,\n\t\tOffset:    offset,\n\t})\n\tif err != nil {\n\t\tlog.Printf(\"stove bridge: failed to report committed message: %v\", err)\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "go/stove-kafka/bridge_test.go",
    "content": "package stovekafka\n\nimport (\n\t\"context\"\n\t\"testing\"\n)\n\nfunc TestNilBridge_ReportPublished(t *testing.T) {\n\tvar b *Bridge\n\terr := b.ReportPublished(context.Background(), &PublishedMessage{\n\t\tTopic: \"test-topic\",\n\t\tKey:   \"key\",\n\t\tValue: []byte(\"value\"),\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error from nil bridge, got %v\", err)\n\t}\n}\n\nfunc TestNilBridge_ReportConsumed(t *testing.T) {\n\tvar b *Bridge\n\terr := b.ReportConsumed(context.Background(), &ConsumedMessage{\n\t\tTopic: \"test-topic\",\n\t\tValue: []byte(\"value\"),\n\t})\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error from nil bridge, got %v\", err)\n\t}\n}\n\nfunc TestNilBridge_ReportCommitted(t *testing.T) {\n\tvar b *Bridge\n\terr := b.ReportCommitted(context.Background(), \"test-topic\", 0, 1)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error from nil bridge, got %v\", err)\n\t}\n}\n\nfunc TestNilBridge_Close(t *testing.T) {\n\tvar b *Bridge\n\terr := b.Close()\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error from nil bridge close, got %v\", err)\n\t}\n}\n\nfunc TestNewBridge_EmptyPort(t *testing.T) {\n\tb, err := NewBridge(\"\")\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif b != nil {\n\t\tt.Fatalf(\"expected nil bridge for empty port, got %+v\", b)\n\t}\n}\n\nfunc TestNewBridgeFromEnv_Unset(t *testing.T) {\n\tt.Setenv(\"STOVE_KAFKA_BRIDGE_PORT\", \"\")\n\tb, err := NewBridgeFromEnv()\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif b != nil {\n\t\tt.Fatalf(\"expected nil bridge when env unset, got %+v\", b)\n\t}\n}\n"
  },
  {
    "path": "go/stove-kafka/franz/hooks.go",
    "content": "// Package franz provides Stove Kafka bridge hooks for twmb/franz-go.\n//\n// Register the hook when creating a franz-go client:\n//\n//\tbridge, _ := stovekafka.NewBridgeFromEnv()\n//\n//\tclient, _ := kgo.NewClient(\n//\t    kgo.SeedBrokers(\"localhost:9092\"),\n//\t    kgo.WithHooks(&franz.Hook{Bridge: bridge}),\n//\t)\npackage franz\n\nimport (\n\t\"context\"\n\n\t\"github.com/twmb/franz-go/pkg/kgo\"\n\n\tstovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n)\n\n// Hook implements franz-go's HookProduceRecordBuffered and HookFetchRecordBuffered.\n// When Bridge is nil (production mode), all methods return immediately with zero overhead.\ntype Hook struct {\n\tBridge *stovekafka.Bridge\n}\n\n// OnProduceRecordBuffered is called when a record is buffered for producing.\n// It reports the message to the Stove observer for shouldBePublished assertions.\nfunc (h *Hook) OnProduceRecordBuffered(r *kgo.Record) {\n\tif h.Bridge == nil {\n\t\treturn\n\t}\n\t_ = h.Bridge.ReportPublished(context.Background(), &stovekafka.PublishedMessage{\n\t\tTopic:   r.Topic,\n\t\tKey:     string(r.Key),\n\t\tValue:   r.Value,\n\t\tHeaders: recordHeaders(r.Headers),\n\t})\n}\n\n// OnFetchRecordBuffered is called when a consumed record is buffered.\n// It reports the consumed message and pre-reports the commit (offset+1)\n// to the Stove observer for shouldBeConsumed assertions.\nfunc (h *Hook) OnFetchRecordBuffered(r *kgo.Record) {\n\tif h.Bridge == nil {\n\t\treturn\n\t}\n\t_ = h.Bridge.ReportConsumed(context.Background(), &stovekafka.ConsumedMessage{\n\t\tTopic:     r.Topic,\n\t\tKey:       string(r.Key),\n\t\tValue:     r.Value,\n\t\tPartition: r.Partition,\n\t\tOffset:    r.Offset,\n\t\tHeaders:   recordHeaders(r.Headers),\n\t})\n\t_ = h.Bridge.ReportCommitted(context.Background(), r.Topic, r.Partition, r.Offset+1)\n}\n\nfunc recordHeaders(headers []kgo.RecordHeader) map[string]string {\n\tm := make(map[string]string, len(headers))\n\tfor _, h := range headers {\n\t\tm[h.Key] = string(h.Value)\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "go/stove-kafka/franz/hooks_test.go",
    "content": "package franz\n\nimport (\n\t\"testing\"\n\n\t\"github.com/twmb/franz-go/pkg/kgo\"\n)\n\nfunc TestHook_NilBridge_OnProduceRecordBuffered(t *testing.T) {\n\th := &Hook{Bridge: nil}\n\th.OnProduceRecordBuffered(&kgo.Record{\n\t\tTopic: \"test-topic\",\n\t\tKey:   []byte(\"key\"),\n\t\tValue: []byte(\"value\"),\n\t})\n}\n\nfunc TestHook_NilBridge_OnFetchRecordBuffered(t *testing.T) {\n\th := &Hook{Bridge: nil}\n\th.OnFetchRecordBuffered(&kgo.Record{\n\t\tTopic:     \"test-topic\",\n\t\tPartition: 0,\n\t\tOffset:    42,\n\t\tKey:       []byte(\"key\"),\n\t\tValue:     []byte(\"value\"),\n\t})\n}\n\nfunc TestRecordHeaders(t *testing.T) {\n\theaders := []kgo.RecordHeader{\n\t\t{Key: \"h1\", Value: []byte(\"v1\")},\n\t\t{Key: \"h2\", Value: []byte(\"v2\")},\n\t}\n\tm := recordHeaders(headers)\n\tif len(m) != 2 {\n\t\tt.Fatalf(\"expected 2 headers, got %d\", len(m))\n\t}\n\tif m[\"h1\"] != \"v1\" || m[\"h2\"] != \"v2\" {\n\t\tt.Fatalf(\"unexpected headers: %v\", m)\n\t}\n}\n\nfunc TestRecordHeaders_Empty(t *testing.T) {\n\tm := recordHeaders(nil)\n\tif len(m) != 0 {\n\t\tt.Fatalf(\"expected 0 headers, got %d\", len(m))\n\t}\n}\n"
  },
  {
    "path": "go/stove-kafka/go.mod",
    "content": "module github.com/trendyol/stove/go/stove-kafka\n\ngo 1.26.2\n\nrequire (\n\tgithub.com/IBM/sarama v1.48.1\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/segmentio/kafka-go v0.4.51\n\tgithub.com/twmb/franz-go v1.21.1\n\tgoogle.golang.org/grpc v1.81.0\n\tgoogle.golang.org/protobuf v1.36.11\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.6 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.26 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/twmb/franz-go/pkg/kmsg v1.13.1 // indirect\n\tgolang.org/x/crypto v0.51.0 // indirect\n\tgolang.org/x/net v0.54.0 // indirect\n\tgolang.org/x/sys v0.44.0 // indirect\n\tgolang.org/x/text v0.37.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect\n)\n"
  },
  {
    "path": "go/stove-kafka/go.sum",
    "content": "github.com/IBM/sarama v1.47.0 h1:GcQFEd12+KzfPYeLgN69Fh7vLCtYRhVIx0rO4TZO318=\ngithub.com/IBM/sarama v1.47.0/go.mod h1:7gLLIU97nznOmA6TX++Qds+DRxH89P2XICY2KAQUzAY=\ngithub.com/IBM/sarama v1.48.0 h1:9LJS0VNeg/boXxT/GLAMDKX6uSQ1mr/5F/j4v9gSeBQ=\ngithub.com/IBM/sarama v1.48.0/go.mod h1:UhvwPF8zilmLOSd6O+ENzdycCJYwMww1U9DJOZpoCro=\ngithub.com/IBM/sarama v1.48.1 h1:x1dSWebprjjE7Wr7n8RVAxwa4mt4O9JejRxnZrGIXk0=\ngithub.com/IBM/sarama v1.48.1/go.mod h1:m/Q1aFezH82/AglfTpJbw/fO0ZybYXhPgTmvajiZX50=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=\ngithub.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=\ngithub.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/segmentio/kafka-go v0.4.50 h1:mcyC3tT5WeyWzrFbd6O374t+hmcu1NKt2Pu1L3QaXmc=\ngithub.com/segmentio/kafka-go v0.4.50/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=\ngithub.com/segmentio/kafka-go v0.4.51 h1:JgDPPG75tC1rWIS2Me6MwcvXJ6f49UQ4HjAOef71Hno=\ngithub.com/segmentio/kafka-go v0.4.51/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/twmb/franz-go v1.20.7 h1:P4MGSXJjjAPP3NRGPCks/Lrq+j+twWMVl1qYCVgNmWY=\ngithub.com/twmb/franz-go v1.20.7/go.mod h1:0bRX9HZVaoueqFWhPZNi2ODnJL7DNa6mK0HeCrC2bNU=\ngithub.com/twmb/franz-go v1.21.0 h1:J3uB/poWgHD6VIilER2uCPFAZHDRXVFT+11pBgRKod4=\ngithub.com/twmb/franz-go v1.21.0/go.mod h1:1o+jj5oRbItsIMoE+DGpfJIcPcPtDdtkcNFPj4bWNwU=\ngithub.com/twmb/franz-go v1.21.1 h1:sp17bMRLz6OB/w+7vHtBadHGIQVymzQHwvRbEKe5c4I=\ngithub.com/twmb/franz-go v1.21.1/go.mod h1:1o+jj5oRbItsIMoE+DGpfJIcPcPtDdtkcNFPj4bWNwU=\ngithub.com/twmb/franz-go/pkg/kmsg v1.13.1 h1:fG5kItwysTk5UXqVwb64EpQEy3TydF3vYYK21nUQ+bI=\ngithub.com/twmb/franz-go/pkg/kmsg v1.13.1/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY=\ngithub.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=\ngithub.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=\ngithub.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=\ngithub.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=\ngo.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=\ngo.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=\ngo.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=\ngo.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=\ngo.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=\ngo.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=\ngo.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=\ngo.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=\ngolang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=\ngolang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=\ngolang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngolang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=\ngolang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=\ngonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=\ngoogle.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=\ngoogle.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=\ngoogle.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "go/stove-kafka/sarama/interceptors.go",
    "content": "// Package sarama provides Stove Kafka bridge interceptors for IBM/sarama.\n//\n// Wire the interceptors into your sarama.Config:\n//\n//\tbridge, _ := stovekafka.NewBridgeFromEnv()\n//\n//\tconfig := sarama.NewConfig()\n//\tconfig.Producer.Interceptors = []sarama.ProducerInterceptor{\n//\t    &stovesarama.ProducerInterceptor{Bridge: bridge},\n//\t}\n//\tconfig.Consumer.Interceptors = []sarama.ConsumerInterceptor{\n//\t    &stovesarama.ConsumerInterceptor{Bridge: bridge},\n//\t}\npackage sarama\n\nimport (\n\t\"context\"\n\n\t\"github.com/IBM/sarama\"\n\tstovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n)\n\n// ProducerInterceptor implements sarama.ProducerInterceptor.\n// It forwards every sent message to the Stove observer via gRPC.\n// When Bridge is nil (production mode), OnSend returns immediately with zero overhead.\ntype ProducerInterceptor struct {\n\tBridge *stovekafka.Bridge\n}\n\n// OnSend is called when a message is about to be sent to Kafka.\n// It reports the message to the Stove observer for shouldBePublished assertions.\n// When Bridge is nil (production), returns immediately without any encoding or allocation.\nfunc (i *ProducerInterceptor) OnSend(msg *sarama.ProducerMessage) {\n\tif i.Bridge == nil {\n\t\treturn\n\t}\n\n\tvalue, err := msg.Value.Encode()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tkey, _ := encodeSaramaKey(msg.Key)\n\n\t_ = i.Bridge.ReportPublished(context.Background(), &stovekafka.PublishedMessage{\n\t\tTopic:   msg.Topic,\n\t\tKey:     key,\n\t\tValue:   value,\n\t\tHeaders: producerHeaders(msg.Headers),\n\t})\n}\n\n// ConsumerInterceptor implements sarama.ConsumerInterceptor.\n// It forwards every consumed message to the Stove observer via gRPC,\n// and pre-reports the commit (offset+1) since Sarama has no onCommit interceptor.\n// When Bridge is nil (production mode), OnConsume returns immediately with zero overhead.\ntype ConsumerInterceptor struct {\n\tBridge *stovekafka.Bridge\n}\n\n// OnConsume is called when a message is consumed from Kafka.\n// It reports the consumed message and a pre-committed offset (offset+1) to the observer.\n// This satisfies Stove's shouldBeConsumed which checks isCommitted(offset+1).\n// When Bridge is nil (production), returns immediately without any encoding or allocation.\nfunc (i *ConsumerInterceptor) OnConsume(msg *sarama.ConsumerMessage) {\n\tif i.Bridge == nil {\n\t\treturn\n\t}\n\n\t_ = i.Bridge.ReportConsumed(context.Background(), &stovekafka.ConsumedMessage{\n\t\tTopic:     msg.Topic,\n\t\tKey:       string(msg.Key),\n\t\tValue:     msg.Value,\n\t\tPartition: msg.Partition,\n\t\tOffset:    msg.Offset,\n\t\tHeaders:   consumerHeaders(msg.Headers),\n\t})\n\t_ = i.Bridge.ReportCommitted(context.Background(), msg.Topic, msg.Partition, msg.Offset+1)\n}\n\nfunc encodeSaramaKey(key sarama.Encoder) (string, error) {\n\tif key == nil {\n\t\treturn \"\", nil\n\t}\n\tkeyBytes, err := key.Encode()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(keyBytes), nil\n}\n\nfunc producerHeaders(headers []sarama.RecordHeader) map[string]string {\n\tm := make(map[string]string, len(headers))\n\tfor _, h := range headers {\n\t\tm[string(h.Key)] = string(h.Value)\n\t}\n\treturn m\n}\n\nfunc consumerHeaders(headers []*sarama.RecordHeader) map[string]string {\n\tm := make(map[string]string, len(headers))\n\tfor _, h := range headers {\n\t\tm[string(h.Key)] = string(h.Value)\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "go/stove-kafka/sarama/interceptors_test.go",
    "content": "package sarama\n\nimport (\n\t\"testing\"\n\n\t\"github.com/IBM/sarama\"\n)\n\nfunc TestProducerInterceptor_NilBridge(t *testing.T) {\n\ti := &ProducerInterceptor{Bridge: nil}\n\t// Should not panic\n\ti.OnSend(&sarama.ProducerMessage{\n\t\tTopic: \"test-topic\",\n\t\tKey:   sarama.StringEncoder(\"key\"),\n\t\tValue: sarama.StringEncoder(\"value\"),\n\t})\n}\n\nfunc TestConsumerInterceptor_NilBridge(t *testing.T) {\n\ti := &ConsumerInterceptor{Bridge: nil}\n\t// Should not panic\n\ti.OnConsume(&sarama.ConsumerMessage{\n\t\tTopic:     \"test-topic\",\n\t\tPartition: 0,\n\t\tOffset:    42,\n\t\tValue:     []byte(\"value\"),\n\t})\n}\n\nfunc TestEncodeSaramaKey_Nil(t *testing.T) {\n\tkey, err := encodeSaramaKey(nil)\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif key != \"\" {\n\t\tt.Fatalf(\"expected empty string, got %q\", key)\n\t}\n}\n\nfunc TestEncodeSaramaKey_String(t *testing.T) {\n\tkey, err := encodeSaramaKey(sarama.StringEncoder(\"my-key\"))\n\tif err != nil {\n\t\tt.Fatalf(\"expected nil error, got %v\", err)\n\t}\n\tif key != \"my-key\" {\n\t\tt.Fatalf(\"expected %q, got %q\", \"my-key\", key)\n\t}\n}\n\nfunc TestProducerHeaders(t *testing.T) {\n\theaders := []sarama.RecordHeader{\n\t\t{Key: []byte(\"h1\"), Value: []byte(\"v1\")},\n\t\t{Key: []byte(\"h2\"), Value: []byte(\"v2\")},\n\t}\n\tm := producerHeaders(headers)\n\tif len(m) != 2 {\n\t\tt.Fatalf(\"expected 2 headers, got %d\", len(m))\n\t}\n\tif m[\"h1\"] != \"v1\" || m[\"h2\"] != \"v2\" {\n\t\tt.Fatalf(\"unexpected headers: %v\", m)\n\t}\n}\n\nfunc TestProducerHeaders_Empty(t *testing.T) {\n\tm := producerHeaders(nil)\n\tif len(m) != 0 {\n\t\tt.Fatalf(\"expected 0 headers, got %d\", len(m))\n\t}\n}\n\nfunc TestConsumerHeaders(t *testing.T) {\n\theaders := []*sarama.RecordHeader{\n\t\t{Key: []byte(\"h1\"), Value: []byte(\"v1\")},\n\t\t{Key: []byte(\"h2\"), Value: []byte(\"v2\")},\n\t}\n\tm := consumerHeaders(headers)\n\tif len(m) != 2 {\n\t\tt.Fatalf(\"expected 2 headers, got %d\", len(m))\n\t}\n\tif m[\"h1\"] != \"v1\" || m[\"h2\"] != \"v2\" {\n\t\tt.Fatalf(\"unexpected headers: %v\", m)\n\t}\n}\n\nfunc TestConsumerHeaders_Empty(t *testing.T) {\n\tm := consumerHeaders(nil)\n\tif len(m) != 0 {\n\t\tt.Fatalf(\"expected 0 headers, got %d\", len(m))\n\t}\n}\n"
  },
  {
    "path": "go/stove-kafka/segmentio/bridge.go",
    "content": "// Package segmentio provides Stove Kafka bridge helpers for segmentio/kafka-go.\n//\n// kafka-go does not have interceptor interfaces. Call ReportWritten after\n// Writer.WriteMessages and ReportRead after Reader.ReadMessage/FetchMessage:\n//\n//\tbridge, _ := stovekafka.NewBridgeFromEnv()\n//\n//\t// After producing\n//\t_ = writer.WriteMessages(ctx, msgs...)\n//\tsegmentio.ReportWritten(ctx, bridge, msgs...)\n//\n//\t// After consuming\n//\tmsg, _ := reader.ReadMessage(ctx)\n//\tsegmentio.ReportRead(ctx, bridge, msg)\npackage segmentio\n\nimport (\n\t\"context\"\n\n\tkafka \"github.com/segmentio/kafka-go\"\n\n\tstovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n)\n\n// ReportWritten reports produced messages to the Stove bridge.\n// Safe to call with nil bridge (no-op, zero overhead).\nfunc ReportWritten(ctx context.Context, bridge *stovekafka.Bridge, msgs ...kafka.Message) {\n\tif bridge == nil {\n\t\treturn\n\t}\n\tfor _, msg := range msgs {\n\t\t_ = bridge.ReportPublished(ctx, toPublished(msg))\n\t}\n}\n\n// ReportRead reports a consumed message and pre-reports the commit (offset+1)\n// to the Stove bridge.\n// Safe to call with nil bridge (no-op, zero overhead).\nfunc ReportRead(ctx context.Context, bridge *stovekafka.Bridge, msg kafka.Message) {\n\tif bridge == nil {\n\t\treturn\n\t}\n\t_ = bridge.ReportConsumed(ctx, toConsumed(msg))\n\t_ = bridge.ReportCommitted(ctx, msg.Topic, int32(msg.Partition), msg.Offset+1)\n}\n\nfunc toPublished(msg kafka.Message) *stovekafka.PublishedMessage {\n\treturn &stovekafka.PublishedMessage{\n\t\tTopic:   msg.Topic,\n\t\tKey:     string(msg.Key),\n\t\tValue:   msg.Value,\n\t\tHeaders: messageHeaders(msg.Headers),\n\t}\n}\n\nfunc toConsumed(msg kafka.Message) *stovekafka.ConsumedMessage {\n\treturn &stovekafka.ConsumedMessage{\n\t\tTopic:     msg.Topic,\n\t\tKey:       string(msg.Key),\n\t\tValue:     msg.Value,\n\t\tPartition: int32(msg.Partition),\n\t\tOffset:    msg.Offset,\n\t\tHeaders:   messageHeaders(msg.Headers),\n\t}\n}\n\nfunc messageHeaders(headers []kafka.Header) map[string]string {\n\tm := make(map[string]string, len(headers))\n\tfor _, h := range headers {\n\t\tm[h.Key] = string(h.Value)\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "go/stove-kafka/segmentio/bridge_test.go",
    "content": "package segmentio\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\tkafka \"github.com/segmentio/kafka-go\"\n)\n\nfunc TestReportWritten_NilBridge(t *testing.T) {\n\tReportWritten(context.Background(), nil, kafka.Message{\n\t\tTopic: \"test-topic\",\n\t\tKey:   []byte(\"key\"),\n\t\tValue: []byte(\"value\"),\n\t})\n}\n\nfunc TestReportRead_NilBridge(t *testing.T) {\n\tReportRead(context.Background(), nil, kafka.Message{\n\t\tTopic:     \"test-topic\",\n\t\tPartition: 0,\n\t\tOffset:    42,\n\t\tKey:       []byte(\"key\"),\n\t\tValue:     []byte(\"value\"),\n\t})\n}\n\nfunc TestMessageHeaders(t *testing.T) {\n\theaders := []kafka.Header{\n\t\t{Key: \"h1\", Value: []byte(\"v1\")},\n\t\t{Key: \"h2\", Value: []byte(\"v2\")},\n\t}\n\tm := messageHeaders(headers)\n\tif len(m) != 2 {\n\t\tt.Fatalf(\"expected 2 headers, got %d\", len(m))\n\t}\n\tif m[\"h1\"] != \"v1\" || m[\"h2\"] != \"v2\" {\n\t\tt.Fatalf(\"unexpected headers: %v\", m)\n\t}\n}\n\nfunc TestMessageHeaders_Empty(t *testing.T) {\n\tm := messageHeaders(nil)\n\tif len(m) != 0 {\n\t\tt.Fatalf(\"expected 0 headers, got %d\", len(m))\n\t}\n}\n\nfunc TestToPublished(t *testing.T) {\n\tmsg := kafka.Message{\n\t\tTopic:   \"test-topic\",\n\t\tKey:     []byte(\"key\"),\n\t\tValue:   []byte(\"value\"),\n\t\tHeaders: []kafka.Header{{Key: \"h1\", Value: []byte(\"v1\")}},\n\t}\n\tpub := toPublished(msg)\n\tif pub.Topic != \"test-topic\" || pub.Key != \"key\" || string(pub.Value) != \"value\" {\n\t\tt.Fatalf(\"unexpected published message: %+v\", pub)\n\t}\n\tif pub.Headers[\"h1\"] != \"v1\" {\n\t\tt.Fatalf(\"unexpected headers: %v\", pub.Headers)\n\t}\n}\n\nfunc TestToConsumed(t *testing.T) {\n\tmsg := kafka.Message{\n\t\tTopic:     \"test-topic\",\n\t\tPartition: 3,\n\t\tOffset:    99,\n\t\tKey:       []byte(\"key\"),\n\t\tValue:     []byte(\"value\"),\n\t\tHeaders:   []kafka.Header{{Key: \"h1\", Value: []byte(\"v1\")}},\n\t}\n\tcon := toConsumed(msg)\n\tif con.Topic != \"test-topic\" || con.Partition != 3 || con.Offset != 99 {\n\t\tt.Fatalf(\"unexpected consumed message: %+v\", con)\n\t}\n\tif con.Key != \"key\" || string(con.Value) != \"value\" {\n\t\tt.Fatalf(\"unexpected key/value: %+v\", con)\n\t}\n\tif con.Headers[\"h1\"] != \"v1\" {\n\t\tt.Fatalf(\"unexpected headers: %v\", con.Headers)\n\t}\n}\n"
  },
  {
    "path": "go/stove-kafka/stoveobserver/messages.pb.go",
    "content": "// Code generated by protoc-gen-go. DO NOT EDIT.\n// versions:\n// \tprotoc-gen-go v1.36.11\n// \tprotoc        v7.34.1\n// source: messages.proto\n\n// buf:lint:ignore FILE_SAME_PACKAGE\n\npackage stoveobserver\n\nimport (\n\tprotoreflect \"google.golang.org/protobuf/reflect/protoreflect\"\n\tprotoimpl \"google.golang.org/protobuf/runtime/protoimpl\"\n\treflect \"reflect\"\n\tsync \"sync\"\n\tunsafe \"unsafe\"\n)\n\nconst (\n\t// Verify that this generated code is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)\n\t// Verify that runtime/protoimpl is sufficiently up-to-date.\n\t_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)\n)\n\ntype HealthCheckResponse_ServingStatus int32\n\nconst (\n\tHealthCheckResponse_UNKNOWN         HealthCheckResponse_ServingStatus = 0\n\tHealthCheckResponse_SERVING         HealthCheckResponse_ServingStatus = 1\n\tHealthCheckResponse_NOT_SERVING     HealthCheckResponse_ServingStatus = 2\n\tHealthCheckResponse_SERVICE_UNKNOWN HealthCheckResponse_ServingStatus = 3 // Used only by the Watch method.\n)\n\n// Enum value maps for HealthCheckResponse_ServingStatus.\nvar (\n\tHealthCheckResponse_ServingStatus_name = map[int32]string{\n\t\t0: \"UNKNOWN\",\n\t\t1: \"SERVING\",\n\t\t2: \"NOT_SERVING\",\n\t\t3: \"SERVICE_UNKNOWN\",\n\t}\n\tHealthCheckResponse_ServingStatus_value = map[string]int32{\n\t\t\"UNKNOWN\":         0,\n\t\t\"SERVING\":         1,\n\t\t\"NOT_SERVING\":     2,\n\t\t\"SERVICE_UNKNOWN\": 3,\n\t}\n)\n\nfunc (x HealthCheckResponse_ServingStatus) Enum() *HealthCheckResponse_ServingStatus {\n\tp := new(HealthCheckResponse_ServingStatus)\n\t*p = x\n\treturn p\n}\n\nfunc (x HealthCheckResponse_ServingStatus) String() string {\n\treturn protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))\n}\n\nfunc (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor {\n\treturn file_messages_proto_enumTypes[0].Descriptor()\n}\n\nfunc (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType {\n\treturn &file_messages_proto_enumTypes[0]\n}\n\nfunc (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber {\n\treturn protoreflect.EnumNumber(x)\n}\n\n// Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead.\nfunc (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{6, 0}\n}\n\ntype ConsumedMessage struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tMessage       []byte                 `protobuf:\"bytes,2,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tTopic         string                 `protobuf:\"bytes,3,opt,name=topic,proto3\" json:\"topic,omitempty\"`\n\tPartition     int32                  `protobuf:\"varint,4,opt,name=partition,proto3\" json:\"partition,omitempty\"`\n\tOffset        int64                  `protobuf:\"varint,5,opt,name=offset,proto3\" json:\"offset,omitempty\"`\n\tKey           string                 `protobuf:\"bytes,6,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tHeaders       map[string]string      `protobuf:\"bytes,8,rep,name=headers,proto3\" json:\"headers,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *ConsumedMessage) Reset() {\n\t*x = ConsumedMessage{}\n\tmi := &file_messages_proto_msgTypes[0]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *ConsumedMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*ConsumedMessage) ProtoMessage() {}\n\nfunc (x *ConsumedMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[0]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use ConsumedMessage.ProtoReflect.Descriptor instead.\nfunc (*ConsumedMessage) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{0}\n}\n\nfunc (x *ConsumedMessage) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *ConsumedMessage) GetMessage() []byte {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn nil\n}\n\nfunc (x *ConsumedMessage) GetTopic() string {\n\tif x != nil {\n\t\treturn x.Topic\n\t}\n\treturn \"\"\n}\n\nfunc (x *ConsumedMessage) GetPartition() int32 {\n\tif x != nil {\n\t\treturn x.Partition\n\t}\n\treturn 0\n}\n\nfunc (x *ConsumedMessage) GetOffset() int64 {\n\tif x != nil {\n\t\treturn x.Offset\n\t}\n\treturn 0\n}\n\nfunc (x *ConsumedMessage) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *ConsumedMessage) GetHeaders() map[string]string {\n\tif x != nil {\n\t\treturn x.Headers\n\t}\n\treturn nil\n}\n\ntype PublishedMessage struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tMessage       []byte                 `protobuf:\"bytes,2,opt,name=message,proto3\" json:\"message,omitempty\"`\n\tTopic         string                 `protobuf:\"bytes,3,opt,name=topic,proto3\" json:\"topic,omitempty\"`\n\tKey           string                 `protobuf:\"bytes,4,opt,name=key,proto3\" json:\"key,omitempty\"`\n\tHeaders       map[string]string      `protobuf:\"bytes,5,rep,name=headers,proto3\" json:\"headers,omitempty\" protobuf_key:\"bytes,1,opt,name=key\" protobuf_val:\"bytes,2,opt,name=value\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *PublishedMessage) Reset() {\n\t*x = PublishedMessage{}\n\tmi := &file_messages_proto_msgTypes[1]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *PublishedMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*PublishedMessage) ProtoMessage() {}\n\nfunc (x *PublishedMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[1]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use PublishedMessage.ProtoReflect.Descriptor instead.\nfunc (*PublishedMessage) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{1}\n}\n\nfunc (x *PublishedMessage) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *PublishedMessage) GetMessage() []byte {\n\tif x != nil {\n\t\treturn x.Message\n\t}\n\treturn nil\n}\n\nfunc (x *PublishedMessage) GetTopic() string {\n\tif x != nil {\n\t\treturn x.Topic\n\t}\n\treturn \"\"\n}\n\nfunc (x *PublishedMessage) GetKey() string {\n\tif x != nil {\n\t\treturn x.Key\n\t}\n\treturn \"\"\n}\n\nfunc (x *PublishedMessage) GetHeaders() map[string]string {\n\tif x != nil {\n\t\treturn x.Headers\n\t}\n\treturn nil\n}\n\ntype CommittedMessage struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tTopic         string                 `protobuf:\"bytes,2,opt,name=topic,proto3\" json:\"topic,omitempty\"`\n\tPartition     int32                  `protobuf:\"varint,3,opt,name=partition,proto3\" json:\"partition,omitempty\"`\n\tOffset        int64                  `protobuf:\"varint,4,opt,name=offset,proto3\" json:\"offset,omitempty\"`\n\tMetadata      string                 `protobuf:\"bytes,5,opt,name=metadata,proto3\" json:\"metadata,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *CommittedMessage) Reset() {\n\t*x = CommittedMessage{}\n\tmi := &file_messages_proto_msgTypes[2]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *CommittedMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*CommittedMessage) ProtoMessage() {}\n\nfunc (x *CommittedMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[2]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use CommittedMessage.ProtoReflect.Descriptor instead.\nfunc (*CommittedMessage) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{2}\n}\n\nfunc (x *CommittedMessage) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *CommittedMessage) GetTopic() string {\n\tif x != nil {\n\t\treturn x.Topic\n\t}\n\treturn \"\"\n}\n\nfunc (x *CommittedMessage) GetPartition() int32 {\n\tif x != nil {\n\t\treturn x.Partition\n\t}\n\treturn 0\n}\n\nfunc (x *CommittedMessage) GetOffset() int64 {\n\tif x != nil {\n\t\treturn x.Offset\n\t}\n\treturn 0\n}\n\nfunc (x *CommittedMessage) GetMetadata() string {\n\tif x != nil {\n\t\treturn x.Metadata\n\t}\n\treturn \"\"\n}\n\ntype AcknowledgedMessage struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tId            string                 `protobuf:\"bytes,1,opt,name=id,proto3\" json:\"id,omitempty\"`\n\tTopic         string                 `protobuf:\"bytes,2,opt,name=topic,proto3\" json:\"topic,omitempty\"`\n\tPartition     int32                  `protobuf:\"varint,3,opt,name=partition,proto3\" json:\"partition,omitempty\"`\n\tOffset        int64                  `protobuf:\"varint,4,opt,name=offset,proto3\" json:\"offset,omitempty\"`\n\tException     string                 `protobuf:\"bytes,5,opt,name=exception,proto3\" json:\"exception,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *AcknowledgedMessage) Reset() {\n\t*x = AcknowledgedMessage{}\n\tmi := &file_messages_proto_msgTypes[3]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *AcknowledgedMessage) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*AcknowledgedMessage) ProtoMessage() {}\n\nfunc (x *AcknowledgedMessage) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[3]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use AcknowledgedMessage.ProtoReflect.Descriptor instead.\nfunc (*AcknowledgedMessage) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{3}\n}\n\nfunc (x *AcknowledgedMessage) GetId() string {\n\tif x != nil {\n\t\treturn x.Id\n\t}\n\treturn \"\"\n}\n\nfunc (x *AcknowledgedMessage) GetTopic() string {\n\tif x != nil {\n\t\treturn x.Topic\n\t}\n\treturn \"\"\n}\n\nfunc (x *AcknowledgedMessage) GetPartition() int32 {\n\tif x != nil {\n\t\treturn x.Partition\n\t}\n\treturn 0\n}\n\nfunc (x *AcknowledgedMessage) GetOffset() int64 {\n\tif x != nil {\n\t\treturn x.Offset\n\t}\n\treturn 0\n}\n\nfunc (x *AcknowledgedMessage) GetException() string {\n\tif x != nil {\n\t\treturn x.Exception\n\t}\n\treturn \"\"\n}\n\ntype Reply struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tStatus        int32                  `protobuf:\"varint,3,opt,name=status,proto3\" json:\"status,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *Reply) Reset() {\n\t*x = Reply{}\n\tmi := &file_messages_proto_msgTypes[4]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *Reply) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*Reply) ProtoMessage() {}\n\nfunc (x *Reply) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[4]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use Reply.ProtoReflect.Descriptor instead.\nfunc (*Reply) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{4}\n}\n\nfunc (x *Reply) GetStatus() int32 {\n\tif x != nil {\n\t\treturn x.Status\n\t}\n\treturn 0\n}\n\ntype HealthCheckRequest struct {\n\tstate         protoimpl.MessageState `protogen:\"open.v1\"`\n\tService       string                 `protobuf:\"bytes,1,opt,name=service,proto3\" json:\"service,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HealthCheckRequest) Reset() {\n\t*x = HealthCheckRequest{}\n\tmi := &file_messages_proto_msgTypes[5]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HealthCheckRequest) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HealthCheckRequest) ProtoMessage() {}\n\nfunc (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[5]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead.\nfunc (*HealthCheckRequest) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{5}\n}\n\nfunc (x *HealthCheckRequest) GetService() string {\n\tif x != nil {\n\t\treturn x.Service\n\t}\n\treturn \"\"\n}\n\ntype HealthCheckResponse struct {\n\tstate         protoimpl.MessageState            `protogen:\"open.v1\"`\n\tStatus        HealthCheckResponse_ServingStatus `protobuf:\"varint,1,opt,name=status,proto3,enum=com.trendyol.stove.kafka.HealthCheckResponse_ServingStatus\" json:\"status,omitempty\"`\n\tunknownFields protoimpl.UnknownFields\n\tsizeCache     protoimpl.SizeCache\n}\n\nfunc (x *HealthCheckResponse) Reset() {\n\t*x = HealthCheckResponse{}\n\tmi := &file_messages_proto_msgTypes[6]\n\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\tms.StoreMessageInfo(mi)\n}\n\nfunc (x *HealthCheckResponse) String() string {\n\treturn protoimpl.X.MessageStringOf(x)\n}\n\nfunc (*HealthCheckResponse) ProtoMessage() {}\n\nfunc (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {\n\tmi := &file_messages_proto_msgTypes[6]\n\tif x != nil {\n\t\tms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))\n\t\tif ms.LoadMessageInfo() == nil {\n\t\t\tms.StoreMessageInfo(mi)\n\t\t}\n\t\treturn ms\n\t}\n\treturn mi.MessageOf(x)\n}\n\n// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead.\nfunc (*HealthCheckResponse) Descriptor() ([]byte, []int) {\n\treturn file_messages_proto_rawDescGZIP(), []int{6}\n}\n\nfunc (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus {\n\tif x != nil {\n\t\treturn x.Status\n\t}\n\treturn HealthCheckResponse_UNKNOWN\n}\n\nvar File_messages_proto protoreflect.FileDescriptor\n\nconst file_messages_proto_rawDesc = \"\" +\n\t\"\\n\" +\n\t\"\\x0emessages.proto\\x12\\x18com.trendyol.stove.kafka\\\"\\xa7\\x02\\n\" +\n\t\"\\x0fConsumedMessage\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\x02 \\x01(\\fR\\amessage\\x12\\x14\\n\" +\n\t\"\\x05topic\\x18\\x03 \\x01(\\tR\\x05topic\\x12\\x1c\\n\" +\n\t\"\\tpartition\\x18\\x04 \\x01(\\x05R\\tpartition\\x12\\x16\\n\" +\n\t\"\\x06offset\\x18\\x05 \\x01(\\x03R\\x06offset\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x06 \\x01(\\tR\\x03key\\x12P\\n\" +\n\t\"\\aheaders\\x18\\b \\x03(\\v26.com.trendyol.stove.kafka.ConsumedMessage.HeadersEntryR\\aheaders\\x1a:\\n\" +\n\t\"\\fHeadersEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x028\\x01\\\"\\xf3\\x01\\n\" +\n\t\"\\x10PublishedMessage\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x18\\n\" +\n\t\"\\amessage\\x18\\x02 \\x01(\\fR\\amessage\\x12\\x14\\n\" +\n\t\"\\x05topic\\x18\\x03 \\x01(\\tR\\x05topic\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x04 \\x01(\\tR\\x03key\\x12Q\\n\" +\n\t\"\\aheaders\\x18\\x05 \\x03(\\v27.com.trendyol.stove.kafka.PublishedMessage.HeadersEntryR\\aheaders\\x1a:\\n\" +\n\t\"\\fHeadersEntry\\x12\\x10\\n\" +\n\t\"\\x03key\\x18\\x01 \\x01(\\tR\\x03key\\x12\\x14\\n\" +\n\t\"\\x05value\\x18\\x02 \\x01(\\tR\\x05value:\\x028\\x01\\\"\\x8a\\x01\\n\" +\n\t\"\\x10CommittedMessage\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x14\\n\" +\n\t\"\\x05topic\\x18\\x02 \\x01(\\tR\\x05topic\\x12\\x1c\\n\" +\n\t\"\\tpartition\\x18\\x03 \\x01(\\x05R\\tpartition\\x12\\x16\\n\" +\n\t\"\\x06offset\\x18\\x04 \\x01(\\x03R\\x06offset\\x12\\x1a\\n\" +\n\t\"\\bmetadata\\x18\\x05 \\x01(\\tR\\bmetadata\\\"\\x8f\\x01\\n\" +\n\t\"\\x13AcknowledgedMessage\\x12\\x0e\\n\" +\n\t\"\\x02id\\x18\\x01 \\x01(\\tR\\x02id\\x12\\x14\\n\" +\n\t\"\\x05topic\\x18\\x02 \\x01(\\tR\\x05topic\\x12\\x1c\\n\" +\n\t\"\\tpartition\\x18\\x03 \\x01(\\x05R\\tpartition\\x12\\x16\\n\" +\n\t\"\\x06offset\\x18\\x04 \\x01(\\x03R\\x06offset\\x12\\x1c\\n\" +\n\t\"\\texception\\x18\\x05 \\x01(\\tR\\texception\\\"\\x1f\\n\" +\n\t\"\\x05Reply\\x12\\x16\\n\" +\n\t\"\\x06status\\x18\\x03 \\x01(\\x05R\\x06status\\\".\\n\" +\n\t\"\\x12HealthCheckRequest\\x12\\x18\\n\" +\n\t\"\\aservice\\x18\\x01 \\x01(\\tR\\aservice\\\"\\xbb\\x01\\n\" +\n\t\"\\x13HealthCheckResponse\\x12S\\n\" +\n\t\"\\x06status\\x18\\x01 \\x01(\\x0e2;.com.trendyol.stove.kafka.HealthCheckResponse.ServingStatusR\\x06status\\\"O\\n\" +\n\t\"\\rServingStatus\\x12\\v\\n\" +\n\t\"\\aUNKNOWN\\x10\\x00\\x12\\v\\n\" +\n\t\"\\aSERVING\\x10\\x01\\x12\\x0f\\n\" +\n\t\"\\vNOT_SERVING\\x10\\x02\\x12\\x13\\n\" +\n\t\"\\x0fSERVICE_UNKNOWN\\x10\\x032\\xa1\\x04\\n\" +\n\t\"\\x19StoveKafkaObserverService\\x12l\\n\" +\n\t\"\\vhealthCheck\\x12,.com.trendyol.stove.kafka.HealthCheckRequest\\x1a-.com.trendyol.stove.kafka.HealthCheckResponse\\\"\\x00\\x12a\\n\" +\n\t\"\\x11onConsumedMessage\\x12).com.trendyol.stove.kafka.ConsumedMessage\\x1a\\x1f.com.trendyol.stove.kafka.Reply\\\"\\x00\\x12c\\n\" +\n\t\"\\x12onPublishedMessage\\x12*.com.trendyol.stove.kafka.PublishedMessage\\x1a\\x1f.com.trendyol.stove.kafka.Reply\\\"\\x00\\x12c\\n\" +\n\t\"\\x12onCommittedMessage\\x12*.com.trendyol.stove.kafka.CommittedMessage\\x1a\\x1f.com.trendyol.stove.kafka.Reply\\\"\\x00\\x12i\\n\" +\n\t\"\\x15onAcknowledgedMessage\\x12-.com.trendyol.stove.kafka.AcknowledgedMessage\\x1a\\x1f.com.trendyol.stove.kafka.Reply\\\"\\x00b\\x06proto3\"\n\nvar (\n\tfile_messages_proto_rawDescOnce sync.Once\n\tfile_messages_proto_rawDescData []byte\n)\n\nfunc file_messages_proto_rawDescGZIP() []byte {\n\tfile_messages_proto_rawDescOnce.Do(func() {\n\t\tfile_messages_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_messages_proto_rawDesc), len(file_messages_proto_rawDesc)))\n\t})\n\treturn file_messages_proto_rawDescData\n}\n\nvar file_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 1)\nvar file_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 9)\nvar file_messages_proto_goTypes = []any{\n\t(HealthCheckResponse_ServingStatus)(0), // 0: com.trendyol.stove.kafka.HealthCheckResponse.ServingStatus\n\t(*ConsumedMessage)(nil),                // 1: com.trendyol.stove.kafka.ConsumedMessage\n\t(*PublishedMessage)(nil),               // 2: com.trendyol.stove.kafka.PublishedMessage\n\t(*CommittedMessage)(nil),               // 3: com.trendyol.stove.kafka.CommittedMessage\n\t(*AcknowledgedMessage)(nil),            // 4: com.trendyol.stove.kafka.AcknowledgedMessage\n\t(*Reply)(nil),                          // 5: com.trendyol.stove.kafka.Reply\n\t(*HealthCheckRequest)(nil),             // 6: com.trendyol.stove.kafka.HealthCheckRequest\n\t(*HealthCheckResponse)(nil),            // 7: com.trendyol.stove.kafka.HealthCheckResponse\n\tnil,                                    // 8: com.trendyol.stove.kafka.ConsumedMessage.HeadersEntry\n\tnil,                                    // 9: com.trendyol.stove.kafka.PublishedMessage.HeadersEntry\n}\nvar file_messages_proto_depIdxs = []int32{\n\t8, // 0: com.trendyol.stove.kafka.ConsumedMessage.headers:type_name -> com.trendyol.stove.kafka.ConsumedMessage.HeadersEntry\n\t9, // 1: com.trendyol.stove.kafka.PublishedMessage.headers:type_name -> com.trendyol.stove.kafka.PublishedMessage.HeadersEntry\n\t0, // 2: com.trendyol.stove.kafka.HealthCheckResponse.status:type_name -> com.trendyol.stove.kafka.HealthCheckResponse.ServingStatus\n\t6, // 3: com.trendyol.stove.kafka.StoveKafkaObserverService.healthCheck:input_type -> com.trendyol.stove.kafka.HealthCheckRequest\n\t1, // 4: com.trendyol.stove.kafka.StoveKafkaObserverService.onConsumedMessage:input_type -> com.trendyol.stove.kafka.ConsumedMessage\n\t2, // 5: com.trendyol.stove.kafka.StoveKafkaObserverService.onPublishedMessage:input_type -> com.trendyol.stove.kafka.PublishedMessage\n\t3, // 6: com.trendyol.stove.kafka.StoveKafkaObserverService.onCommittedMessage:input_type -> com.trendyol.stove.kafka.CommittedMessage\n\t4, // 7: com.trendyol.stove.kafka.StoveKafkaObserverService.onAcknowledgedMessage:input_type -> com.trendyol.stove.kafka.AcknowledgedMessage\n\t7, // 8: com.trendyol.stove.kafka.StoveKafkaObserverService.healthCheck:output_type -> com.trendyol.stove.kafka.HealthCheckResponse\n\t5, // 9: com.trendyol.stove.kafka.StoveKafkaObserverService.onConsumedMessage:output_type -> com.trendyol.stove.kafka.Reply\n\t5, // 10: com.trendyol.stove.kafka.StoveKafkaObserverService.onPublishedMessage:output_type -> com.trendyol.stove.kafka.Reply\n\t5, // 11: com.trendyol.stove.kafka.StoveKafkaObserverService.onCommittedMessage:output_type -> com.trendyol.stove.kafka.Reply\n\t5, // 12: com.trendyol.stove.kafka.StoveKafkaObserverService.onAcknowledgedMessage:output_type -> com.trendyol.stove.kafka.Reply\n\t8, // [8:13] is the sub-list for method output_type\n\t3, // [3:8] is the sub-list for method input_type\n\t3, // [3:3] is the sub-list for extension type_name\n\t3, // [3:3] is the sub-list for extension extendee\n\t0, // [0:3] is the sub-list for field type_name\n}\n\nfunc init() { file_messages_proto_init() }\nfunc file_messages_proto_init() {\n\tif File_messages_proto != nil {\n\t\treturn\n\t}\n\ttype x struct{}\n\tout := protoimpl.TypeBuilder{\n\t\tFile: protoimpl.DescBuilder{\n\t\t\tGoPackagePath: reflect.TypeOf(x{}).PkgPath(),\n\t\t\tRawDescriptor: unsafe.Slice(unsafe.StringData(file_messages_proto_rawDesc), len(file_messages_proto_rawDesc)),\n\t\t\tNumEnums:      1,\n\t\t\tNumMessages:   9,\n\t\t\tNumExtensions: 0,\n\t\t\tNumServices:   1,\n\t\t},\n\t\tGoTypes:           file_messages_proto_goTypes,\n\t\tDependencyIndexes: file_messages_proto_depIdxs,\n\t\tEnumInfos:         file_messages_proto_enumTypes,\n\t\tMessageInfos:      file_messages_proto_msgTypes,\n\t}.Build()\n\tFile_messages_proto = out.File\n\tfile_messages_proto_goTypes = nil\n\tfile_messages_proto_depIdxs = nil\n}\n"
  },
  {
    "path": "go/stove-kafka/stoveobserver/messages_grpc.pb.go",
    "content": "// Code generated by protoc-gen-go-grpc. DO NOT EDIT.\n// versions:\n// - protoc-gen-go-grpc v1.6.1\n// - protoc             v7.34.1\n// source: messages.proto\n\n// buf:lint:ignore FILE_SAME_PACKAGE\n\npackage stoveobserver\n\nimport (\n\tcontext \"context\"\n\tgrpc \"google.golang.org/grpc\"\n\tcodes \"google.golang.org/grpc/codes\"\n\tstatus \"google.golang.org/grpc/status\"\n)\n\n// This is a compile-time assertion to ensure that this generated file\n// is compatible with the grpc package it is being compiled against.\n// Requires gRPC-Go v1.64.0 or later.\nconst _ = grpc.SupportPackageIsVersion9\n\nconst (\n\tStoveKafkaObserverService_HealthCheck_FullMethodName           = \"/com.trendyol.stove.kafka.StoveKafkaObserverService/healthCheck\"\n\tStoveKafkaObserverService_OnConsumedMessage_FullMethodName     = \"/com.trendyol.stove.kafka.StoveKafkaObserverService/onConsumedMessage\"\n\tStoveKafkaObserverService_OnPublishedMessage_FullMethodName    = \"/com.trendyol.stove.kafka.StoveKafkaObserverService/onPublishedMessage\"\n\tStoveKafkaObserverService_OnCommittedMessage_FullMethodName    = \"/com.trendyol.stove.kafka.StoveKafkaObserverService/onCommittedMessage\"\n\tStoveKafkaObserverService_OnAcknowledgedMessage_FullMethodName = \"/com.trendyol.stove.kafka.StoveKafkaObserverService/onAcknowledgedMessage\"\n)\n\n// StoveKafkaObserverServiceClient is the client API for StoveKafkaObserverService service.\n//\n// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.\ntype StoveKafkaObserverServiceClient interface {\n\tHealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)\n\t// buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n\tOnConsumedMessage(ctx context.Context, in *ConsumedMessage, opts ...grpc.CallOption) (*Reply, error)\n\t// buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n\tOnPublishedMessage(ctx context.Context, in *PublishedMessage, opts ...grpc.CallOption) (*Reply, error)\n\t// buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n\tOnCommittedMessage(ctx context.Context, in *CommittedMessage, opts ...grpc.CallOption) (*Reply, error)\n\t// buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n\tOnAcknowledgedMessage(ctx context.Context, in *AcknowledgedMessage, opts ...grpc.CallOption) (*Reply, error)\n}\n\ntype stoveKafkaObserverServiceClient struct {\n\tcc grpc.ClientConnInterface\n}\n\nfunc NewStoveKafkaObserverServiceClient(cc grpc.ClientConnInterface) StoveKafkaObserverServiceClient {\n\treturn &stoveKafkaObserverServiceClient{cc}\n}\n\nfunc (c *stoveKafkaObserverServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(HealthCheckResponse)\n\terr := c.cc.Invoke(ctx, StoveKafkaObserverService_HealthCheck_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *stoveKafkaObserverServiceClient) OnConsumedMessage(ctx context.Context, in *ConsumedMessage, opts ...grpc.CallOption) (*Reply, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Reply)\n\terr := c.cc.Invoke(ctx, StoveKafkaObserverService_OnConsumedMessage_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *stoveKafkaObserverServiceClient) OnPublishedMessage(ctx context.Context, in *PublishedMessage, opts ...grpc.CallOption) (*Reply, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Reply)\n\terr := c.cc.Invoke(ctx, StoveKafkaObserverService_OnPublishedMessage_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *stoveKafkaObserverServiceClient) OnCommittedMessage(ctx context.Context, in *CommittedMessage, opts ...grpc.CallOption) (*Reply, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Reply)\n\terr := c.cc.Invoke(ctx, StoveKafkaObserverService_OnCommittedMessage_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\nfunc (c *stoveKafkaObserverServiceClient) OnAcknowledgedMessage(ctx context.Context, in *AcknowledgedMessage, opts ...grpc.CallOption) (*Reply, error) {\n\tcOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)\n\tout := new(Reply)\n\terr := c.cc.Invoke(ctx, StoveKafkaObserverService_OnAcknowledgedMessage_FullMethodName, in, out, cOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn out, nil\n}\n\n// StoveKafkaObserverServiceServer is the server API for StoveKafkaObserverService service.\n// All implementations must embed UnimplementedStoveKafkaObserverServiceServer\n// for forward compatibility.\ntype StoveKafkaObserverServiceServer interface {\n\tHealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)\n\t// buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n\tOnConsumedMessage(context.Context, *ConsumedMessage) (*Reply, error)\n\t// buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n\tOnPublishedMessage(context.Context, *PublishedMessage) (*Reply, error)\n\t// buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n\tOnCommittedMessage(context.Context, *CommittedMessage) (*Reply, error)\n\t// buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n\tOnAcknowledgedMessage(context.Context, *AcknowledgedMessage) (*Reply, error)\n\tmustEmbedUnimplementedStoveKafkaObserverServiceServer()\n}\n\n// UnimplementedStoveKafkaObserverServiceServer must be embedded to have\n// forward compatible implementations.\n//\n// NOTE: this should be embedded by value instead of pointer to avoid a nil\n// pointer dereference when methods are called.\ntype UnimplementedStoveKafkaObserverServiceServer struct{}\n\nfunc (UnimplementedStoveKafkaObserverServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method HealthCheck not implemented\")\n}\nfunc (UnimplementedStoveKafkaObserverServiceServer) OnConsumedMessage(context.Context, *ConsumedMessage) (*Reply, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method OnConsumedMessage not implemented\")\n}\nfunc (UnimplementedStoveKafkaObserverServiceServer) OnPublishedMessage(context.Context, *PublishedMessage) (*Reply, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method OnPublishedMessage not implemented\")\n}\nfunc (UnimplementedStoveKafkaObserverServiceServer) OnCommittedMessage(context.Context, *CommittedMessage) (*Reply, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method OnCommittedMessage not implemented\")\n}\nfunc (UnimplementedStoveKafkaObserverServiceServer) OnAcknowledgedMessage(context.Context, *AcknowledgedMessage) (*Reply, error) {\n\treturn nil, status.Error(codes.Unimplemented, \"method OnAcknowledgedMessage not implemented\")\n}\nfunc (UnimplementedStoveKafkaObserverServiceServer) mustEmbedUnimplementedStoveKafkaObserverServiceServer() {\n}\nfunc (UnimplementedStoveKafkaObserverServiceServer) testEmbeddedByValue() {}\n\n// UnsafeStoveKafkaObserverServiceServer may be embedded to opt out of forward compatibility for this service.\n// Use of this interface is not recommended, as added methods to StoveKafkaObserverServiceServer will\n// result in compilation errors.\ntype UnsafeStoveKafkaObserverServiceServer interface {\n\tmustEmbedUnimplementedStoveKafkaObserverServiceServer()\n}\n\nfunc RegisterStoveKafkaObserverServiceServer(s grpc.ServiceRegistrar, srv StoveKafkaObserverServiceServer) {\n\t// If the following call panics, it indicates UnimplementedStoveKafkaObserverServiceServer was\n\t// embedded by pointer and is nil.  This will cause panics if an\n\t// unimplemented method is ever invoked, so we test this at initialization\n\t// time to prevent it from happening at runtime later due to I/O.\n\tif t, ok := srv.(interface{ testEmbeddedByValue() }); ok {\n\t\tt.testEmbeddedByValue()\n\t}\n\ts.RegisterService(&StoveKafkaObserverService_ServiceDesc, srv)\n}\n\nfunc _StoveKafkaObserverService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(HealthCheckRequest)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(StoveKafkaObserverServiceServer).HealthCheck(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: StoveKafkaObserverService_HealthCheck_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(StoveKafkaObserverServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _StoveKafkaObserverService_OnConsumedMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(ConsumedMessage)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(StoveKafkaObserverServiceServer).OnConsumedMessage(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: StoveKafkaObserverService_OnConsumedMessage_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(StoveKafkaObserverServiceServer).OnConsumedMessage(ctx, req.(*ConsumedMessage))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _StoveKafkaObserverService_OnPublishedMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(PublishedMessage)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(StoveKafkaObserverServiceServer).OnPublishedMessage(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: StoveKafkaObserverService_OnPublishedMessage_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(StoveKafkaObserverServiceServer).OnPublishedMessage(ctx, req.(*PublishedMessage))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _StoveKafkaObserverService_OnCommittedMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(CommittedMessage)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(StoveKafkaObserverServiceServer).OnCommittedMessage(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: StoveKafkaObserverService_OnCommittedMessage_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(StoveKafkaObserverServiceServer).OnCommittedMessage(ctx, req.(*CommittedMessage))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\nfunc _StoveKafkaObserverService_OnAcknowledgedMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {\n\tin := new(AcknowledgedMessage)\n\tif err := dec(in); err != nil {\n\t\treturn nil, err\n\t}\n\tif interceptor == nil {\n\t\treturn srv.(StoveKafkaObserverServiceServer).OnAcknowledgedMessage(ctx, in)\n\t}\n\tinfo := &grpc.UnaryServerInfo{\n\t\tServer:     srv,\n\t\tFullMethod: StoveKafkaObserverService_OnAcknowledgedMessage_FullMethodName,\n\t}\n\thandler := func(ctx context.Context, req interface{}) (interface{}, error) {\n\t\treturn srv.(StoveKafkaObserverServiceServer).OnAcknowledgedMessage(ctx, req.(*AcknowledgedMessage))\n\t}\n\treturn interceptor(ctx, in, info, handler)\n}\n\n// StoveKafkaObserverService_ServiceDesc is the grpc.ServiceDesc for StoveKafkaObserverService service.\n// It's only intended for direct use with grpc.RegisterService,\n// and not to be introspected or modified (even as a copy)\nvar StoveKafkaObserverService_ServiceDesc = grpc.ServiceDesc{\n\tServiceName: \"com.trendyol.stove.kafka.StoveKafkaObserverService\",\n\tHandlerType: (*StoveKafkaObserverServiceServer)(nil),\n\tMethods: []grpc.MethodDesc{\n\t\t{\n\t\t\tMethodName: \"healthCheck\",\n\t\t\tHandler:    _StoveKafkaObserverService_HealthCheck_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"onConsumedMessage\",\n\t\t\tHandler:    _StoveKafkaObserverService_OnConsumedMessage_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"onPublishedMessage\",\n\t\t\tHandler:    _StoveKafkaObserverService_OnPublishedMessage_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"onCommittedMessage\",\n\t\t\tHandler:    _StoveKafkaObserverService_OnCommittedMessage_Handler,\n\t\t},\n\t\t{\n\t\t\tMethodName: \"onAcknowledgedMessage\",\n\t\t\tHandler:    _StoveKafkaObserverService_OnAcknowledgedMessage_Handler,\n\t\t},\n\t},\n\tStreams:  []grpc.StreamDesc{},\n\tMetadata: \"messages.proto\",\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nkotlin = \"2.3.21\"\nkotlinx = \"1.10.2\"\nexposed = \"1.2.0\"\nhikari = \"7.0.2\"\nspring-boot = \"2.7.18\"\nspring-boot-3x = \"3.5.14\"\nspring-boot-4x = \"4.0.6\"\nspring-dependency-management = \"1.1.7\"\nspring-kafka = \"2.9.13\"\nspring-kafka-3x = \"3.3.15\"\nspring-kafka-4x = \"4.0.5\"\ncouchbase = \"3.11.2\"\njackson = \"2.21.3\"\njackson3 = \"3.1.3\"\narrow = \"2.2.2.1\"\nio-reactor = \"3.8.5\"\nio-reactor-extensions = \"1.3.0\"\nslf4j = \"2.0.17\"\nkafka = \"4.2.0\"\nkafka-kotlin = \"0.4.1\"\nkafka-embedded = \"4.2.0\"\nkafka-streams-registry = \"8.2.0\"\nkover = \"0.9.8\"\nktor = \"3.4.3\"\nkoin = \"4.2.1\"\nquarkus = \"3.35.2\"\nr2dbc-postgresql = \"0.8.13.RELEASE\"\nelastic = \"9.4.0\"\nmongodb = \"5.7.0\"\nwiremock = \"3.13.2\"\ntestcontainers = \"2.0.5\"\ncassandra-driver = \"4.19.2\"\nmysql = \"9.7.0\"\nspotless = \"8.4.0\"\ndetekt = \"1.23.8\"\nwire = \"6.2.0\"\nio-grpc = \"1.81.0\"\nio-grpc-kotlin = \"1.5.0\"\ngoogle-protobuf = \"4.34.1\"\nhoplite = \"2.9.0\"\njunit-jupiter = \"6.0.3\"\nkotest = \"6.1.11\"\nmockito = \"6.3.0\"\nkotlinx-serialization = \"1.11.0\"\nkotlinBinaryCompatibilityValidator = \"0.18.1\"\nsnakeyaml = \"2.6\"\nmicronaut = \"4.10.23\"\nmicronaut-platform = \"4.10.13\"\nmicronaut-micrometer = \"1.3.1\"\nktlint = \"1.8.0\"\nopentelemetry = \"1.62.0\"\nopentelemetry-semconv = \"1.41.0\"\nopentelemetry-instrumentation = \"2.27.0\"\nbytebuddy = \"1.18.8\"\nmordant = \"3.0.2\"\n\n[libraries]\n# exposed\nexposed-core = { module = \"org.jetbrains.exposed:exposed-core\", version.ref = \"exposed\" }\nexposed-jdbc = { module = \"org.jetbrains.exposed:exposed-jdbc\", version.ref = \"exposed\" }\nexposed-java-time = { module = \"org.jetbrains.exposed:exposed-java-time\", version.ref = \"exposed\" }\n\n# hikari\nhikari = { module = \"com.zaxxer:HikariCP\", version.ref = \"hikari\" }\n\nkotlin-reflect = { module = \"org.jetbrains.kotlin:kotlin-reflect\", version.ref = \"kotlin\" }\nkotlinx-reactor = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-reactor\", version.ref = \"kotlinx\" }\nkotlinx-reactive = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-reactive\", version.ref = \"kotlinx\" }\nkotlinx-core = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-core\", version.ref = \"kotlinx\" }\nkotlinx-jdk8 = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-jdk8\", version.ref = \"kotlinx\" }\nkotlinx-slf4j = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-slf4j\", version.ref = \"kotlinx\" }\nkotlinx-io-reactor = { module = \"io.projectreactor:reactor-core\", version.ref = \"io-reactor\" }\nkotlinx-io-reactor-extensions = { module = \"io.projectreactor.kotlin:reactor-kotlin-extensions\", version.ref = \"io-reactor-extensions\" }\nkotlinx-serialization-json = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-json-jvm\", version.ref = \"kotlinx-serialization\" }\n\n# Micronaut\nsnakeyaml = { module = \"org.yaml:snakeyaml\", version.ref = \"snakeyaml\" }\nmicronaut-platform = { module = \"io.micronaut.platform:micronaut-platform\", version.ref = \"micronaut-platform\" }\nmicronaut-kotlin-runtime = { module = \"io.micronaut.kotlin:micronaut-kotlin-runtime\", version = \"4.7.0\" }\nmicronaut-serde-jackson = { module = \"io.micronaut.serde:micronaut-serde-jackson\", version = \"2.16.2\" }\nmicronaut-http-client = { module = \"io.micronaut:micronaut-http-client\", version.ref = \"micronaut\" }\nmicronaut-http-server-netty = { module = \"io.micronaut:micronaut-http-server-netty\", version.ref = \"micronaut\" }\nmicronaut-inject = { module = \"io.micronaut:micronaut-inject\", version.ref = \"micronaut\" }\nmicronaut-inject-kotlin = { module = \"io.micronaut:micronaut-inject-kotlin\", version.ref = \"micronaut\" }\nmicronaut-core = { module = \"io.micronaut:micronaut-core\", version.ref = \"micronaut\" }\nmicronaut-test-kotest = { module = \"io.micronaut.test:micronaut-test-kotest5\", version = \"4.10.3\" }\nmicronaut-micrometer-core = { module = \"io.micronaut.configuration:micronaut-micrometer-core\", version.ref = \"micronaut-micrometer\" }\nmicronaut-data-r2dbc = { module = \"io.micronaut.data:micronaut-data-r2dbc\", version = \"4.14.4\" }\n\n# Arrow\narrow-core = { module = \"io.arrow-kt:arrow-core\", version.ref = \"arrow\" }\n\n# Kafka\nkafka = { module = \"org.apache.kafka:kafka-clients\", version.ref = \"kafka\" }\nkafkaKotlin = { module = \"io.github.nomisrev:kotlin-kafka\", version.ref = \"kafka-kotlin\" }\nkafka-streams = { module = \"org.apache.kafka:kafka-streams\", version.ref = \"kafka\" }\nkafka-streams-protobuf-serde = { module = \"io.confluent:kafka-streams-protobuf-serde\", version.ref = \"kafka-streams-registry\" }\nkafka-embedded = { module = \"io.github.embeddedkafka:embedded-kafka_2.13\", version.ref = \"kafka-embedded\" }\n\n# Couchbase\ncouchbase-kotlin = { module = \"com.couchbase.client:kotlin-client\", version.ref = \"couchbase\" }\ncouchbase-client = { module = \"com.couchbase.client:java-client\", version.ref = \"couchbase\" }\ncouchbase-client-metrics = { module = \"com.couchbase.client:metrics-micrometer\", version.ref = \"couchbase\" }\n\n# Jackson\njackson-kotlin = { module = \"com.fasterxml.jackson.module:jackson-module-kotlin\", version.ref = \"jackson\" }\njackson-databind = { module = \"com.fasterxml.jackson.core:jackson-databind\", version.ref = \"jackson\" }\njackson-jsr310 = { module = \"com.fasterxml.jackson.datatype:jackson-datatype-jsr310\", version.ref = \"jackson\" }\n\n# Jackson 3\njackson3-kotlin = { module = \"tools.jackson.module:jackson-module-kotlin\", version.ref = \"jackson3\" }\n\n# Slfj4\nslf4j-simple = { module = \"org.slf4j:slf4j-simple\", version.ref = \"slf4j\" }\n\n# Ktor\nktor-server-host-common = { module = \"io.ktor:ktor-server-host-common\", version.ref = \"ktor\" }\nktor-server = { module = \"io.ktor:ktor-server\", version.ref = \"ktor\" }\nktor-server-call-logging = { module = \"io.ktor:ktor-server-call-logging\", version.ref = \"ktor\" }\nktor-server-netty = { module = \"io.ktor:ktor-server-netty\", version.ref = \"ktor\" }\nktor-server-cio = { module = \"io.ktor:ktor-server-cio\", version.ref = \"ktor\" }\nktor-server-di = { module = \"io.ktor:ktor-server-di\", version.ref = \"ktor\" }\nktor-serialization-kotlinx-json = { module = \"io.ktor:ktor-serialization-kotlinx-json\", version.ref = \"ktor\" }\nktor-client-core = { module = \"io.ktor:ktor-client-core\", version.ref = \"ktor\" }\nktor-client-okhttp = { module = \"io.ktor:ktor-client-okhttp\", version.ref = \"ktor\" }\nktor-client-plugins-logging = { module = \"io.ktor:ktor-client-logging\", version.ref = \"ktor\" }\nktor-client-content-negotiation = { module = \"io.ktor:ktor-client-content-negotiation\", version.ref = \"ktor\" }\nktor-serialization-jackson-json = { module = \"io.ktor:ktor-serialization-jackson\", version.ref = \"ktor\" }\nktor-client-websockets = { module = \"io.ktor:ktor-client-websockets\", version.ref = \"ktor\" }\nktor-server-websockets = { module = \"io.ktor:ktor-server-websockets\", version.ref = \"ktor\" }\n\n# koin\nkoin-ktor = { module = \"io.insert-koin:koin-ktor\", version.ref = \"koin\" }\nkoin-logger-slf4j = { module = \"io.insert-koin:koin-logger-slf4j\", version.ref = \"koin\" }\n\n# Quarkus\nquarkus = { module = \"io.quarkus:quarkus-bom\", version.ref = \"quarkus\" }\nquarkus-core = { module = \"io.quarkus:quarkus-core\", version.ref = \"quarkus\" }\nquarkus-rest = { module = \"io.quarkus:quarkus-rest\", version.ref = \"quarkus\" }\nquarkus-rest-jackson = { module = \"io.quarkus:quarkus-rest-jackson\", version.ref = \"quarkus\" }\nquarkus-arc = { module = \"io.quarkus:quarkus-arc\", version.ref = \"quarkus\" }\nquarkus-kotlin = { module = \"io.quarkus:quarkus-kotlin\", version.ref = \"quarkus\" }\nquarkus-agroal = { module = \"io.quarkus:quarkus-agroal\", version.ref = \"quarkus\" }\nquarkus-jdbc-postgresql = { module = \"io.quarkus:quarkus-jdbc-postgresql\", version.ref = \"quarkus\" }\nquarkus-flyway = { module = \"io.quarkus:quarkus-flyway\", version.ref = \"quarkus\" }\nquarkus-messaging-kafka = { module = \"io.quarkus:quarkus-messaging-kafka\", version.ref = \"quarkus\" }\n\n# r2dbc\nr2dbc-postgresql = { module = \"io.r2dbc:r2dbc-postgresql\", version.ref = \"r2dbc-postgresql\" }\nkotliquery = { module = \"com.github.seratch:kotliquery\", version = \"1.9.1\" }\nh2Database = { module = \"com.h2database:h2\", version = \"2.4.240\" }\n\n# postgres\npostgresql = { module = \"org.postgresql:postgresql\", version = \"42.7.11\" }\nmysql-connector = { module = \"com.mysql:mysql-connector-j\", version.ref = \"mysql\" }\n\n# elastic\nelastic = { module = \"co.elastic.clients:elasticsearch-java\", version.ref = \"elastic\" }\nelastic-rest-client = { module = \"org.elasticsearch.client:elasticsearch-rest-client\", version = \"9.4.0\" }\n\n# mongo\nmongodb-kotlin-coroutine = { module = \"org.mongodb:mongodb-driver-kotlin-coroutine\", version.ref = \"mongodb\" }\n\n# misc\nlettuce-core = { module = \"io.lettuce:lettuce-core\", version = \"7.5.1.RELEASE\" }\nlogback-classic = { module = \"ch.qos.logback:logback-classic\", version = \"1.5.32\" }\nmicrosoft-sqlserver-jdbc = { module = \"com.microsoft.sqlserver:mssql-jdbc\", version = \"13.4.0.jre11\" }\n\n### Testing\nwiremock-standalone = { module = \"org.wiremock:wiremock-standalone\", version.ref = \"wiremock\" }\ntestcontainers = { module = \"org.testcontainers:testcontainers\", version.ref = \"testcontainers\" }\ntestcontainers-jdbc = { module = \"org.testcontainers:testcontainers-jdbc\", version.ref = \"testcontainers\" }\ntestcontainers-kafka = { module = \"org.testcontainers:testcontainers-kafka\", version.ref = \"testcontainers\" }\ntestcontainers-couchbase = { module = \"org.testcontainers:testcontainers-couchbase\", version.ref = \"testcontainers\" }\ntestcontainers-postgres = { module = \"org.testcontainers:testcontainers-postgresql\", version.ref = \"testcontainers\" }\ntestcontainers-elasticsearch = { module = \"org.testcontainers:testcontainers-elasticsearch\", version.ref = \"testcontainers\" }\ntestcontainers-mongodb = { module = \"org.testcontainers:testcontainers-mongodb\", version.ref = \"testcontainers\" }\ntestcontainers-mssql = { module = \"org.testcontainers:testcontainers-mssqlserver\", version.ref = \"testcontainers\" }\ntestcontainers-mysql = { module = \"org.testcontainers:testcontainers-mysql\", version.ref = \"testcontainers\" }\ntestcontainers-redis = { module = \"com.redis.testcontainers:testcontainers-redis\", version = \"1.6.4\" }\ntestcontainers-cassandra = { module = \"org.testcontainers:testcontainers-cassandra\", version.ref = \"testcontainers\" }\n\n# Cassandra\ncassandra-driver-core = { module = \"org.apache.cassandra:java-driver-core\", version.ref = \"cassandra-driver\" }\n\n# spring-boot 2x\nspring-boot = { module = \"org.springframework.boot:spring-boot\", version.ref = \"spring-boot\" }\nspring-boot-autoconfigure = { module = \"org.springframework.boot:spring-boot-autoconfigure\", version.ref = \"spring-boot\" }\nspring-boot-annotationProcessor = { module = \"org.springframework.boot:spring-boot-configuration-processor\", version.ref = \"spring-boot\" }\nspring-boot-kafka = { module = \"org.springframework.kafka:spring-kafka\", version.ref = \"spring-kafka\" }\n\n# spring-boot 3x\nspring-boot-three = { module = \"org.springframework.boot:spring-boot\", version.ref = \"spring-boot-3x\" }\nspring-boot-three-web = { module = \"org.springframework.boot:spring-boot-starter-web\", version.ref = \"spring-boot-3x\" }\nspring-boot-three-webflux = { module = \"org.springframework.boot:spring-boot-starter-webflux\", version.ref = \"spring-boot-3x\" }\nspring-boot-three-actuator = { module = \"org.springframework.boot:spring-boot-starter-actuator\", version.ref = \"spring-boot-3x\" }\nspring-boot-three-autoconfigure = { module = \"org.springframework.boot:spring-boot-autoconfigure\", version.ref = \"spring-boot-3x\" }\nspring-boot-three-annotationProcessor = { module = \"org.springframework.boot:spring-boot-configuration-processor\", version.ref = \"spring-boot-3x\" }\nspring-boot-three-kafka = { module = \"org.springframework.kafka:spring-kafka\", version.ref = \"spring-kafka-3x\" }\nspring-boot-three-data-r2dbc = { module = \"org.springframework.boot:spring-boot-starter-data-r2dbc\", version.ref = \"spring-boot-3x\" }\nspring-boot-three-data-jpa = { module = \"org.springframework.boot:spring-boot-starter-data-jpa\", version.ref = \"spring-boot-3x\" }\n\n# spring-boot 4x\nspring-boot-four = { module = \"org.springframework.boot:spring-boot\", version.ref = \"spring-boot-4x\" }\nspring-boot-four-webflux = { module = \"org.springframework.boot:spring-boot-starter-webflux\", version.ref = \"spring-boot-4x\" }\nspring-boot-four-actuator = { module = \"org.springframework.boot:spring-boot-starter-actuator\", version.ref = \"spring-boot-4x\" }\nspring-boot-four-autoconfigure = { module = \"org.springframework.boot:spring-boot-autoconfigure\", version.ref = \"spring-boot-4x\" }\nspring-boot-four-annotationProcessor = { module = \"org.springframework.boot:spring-boot-configuration-processor\", version.ref = \"spring-boot-4x\" }\nspring-boot-four-kafka = { module = \"org.springframework.kafka:spring-kafka\", version.ref = \"spring-kafka-4x\" }\n\ndetekt-formatting = { module = \"io.gitlab.arturbosch.detekt:detekt-formatting\", version.ref = \"detekt\" }\nwire-grpc-server = { module = \"com.squareup.wiregrpcserver:server\", version = \"1.0.0-alpha04\" }\nwire-grpc-server-generator = { module = \"com.squareup.wiregrpcserver:server-generator\", version = \"1.0.0-alpha04\" }\nwire-grpc-client = { module = \"com.squareup.wire:wire-grpc-client\", version.ref = \"wire\" }\nwire-grpc-runtime = { module = \"com.squareup.wire:wire-runtime\", version.ref = \"wire\" }\nio-grpc = { module = \"io.grpc:grpc-core\", version.ref = \"io-grpc\" }\nio-grpc-stub = { module = \"io.grpc:grpc-stub\", version.ref = \"io-grpc\" }\nio-grpc-protobuf = { module = \"io.grpc:grpc-protobuf\", version.ref = \"io-grpc\" }\nio-grpc-netty = { module = \"io.grpc:grpc-netty\", version.ref = \"io-grpc\" }\nio-grpc-kotlin = { module = \"io.grpc:grpc-kotlin-stub\", version.ref = \"io-grpc-kotlin\" }\ngrpc-protoc-gen-java = { module = \"io.grpc:protoc-gen-grpc-java\", version.ref = \"io-grpc\" }\ngrpc-protoc-gen-kotlin = { module = \"io.grpc:protoc-gen-grpc-kotlin\", version.ref = \"io-grpc-kotlin\" }\ngoogle-protobuf-kotlin = { module = \"com.google.protobuf:protobuf-kotlin\", version.ref = \"google-protobuf\" }\ngoogle-protobuf-util = { module = \"com.google.protobuf:protobuf-java-util\", version.ref = \"google-protobuf\" }\nprotoc = { module = \"com.google.protobuf:protoc\", version.ref = \"google-protobuf\" }\nhoplite-yaml = { module = \"com.sksamuel.hoplite:hoplite-yaml\", version.ref = \"hoplite\" }\ngoogle-gson = { module = \"com.google.code.gson:gson\", version = \"2.14.0\" }\ncaffeine = { module = \"com.github.ben-manes.caffeine:caffeine\", version = \"3.2.4\" }\npprint = { module = \"io.exoquery:pprint-kotlin\", version = \"3.0.0\" }\nktlint-cli = { module = \"com.pinterest.ktlint:ktlint-cli\", version.ref = \"ktlint\" }\nmordant = { module = \"com.github.ajalt.mordant:mordant\", version.ref = \"mordant\" }\n\n# OpenTelemetry\nopentelemetry-api = { module = \"io.opentelemetry:opentelemetry-api\", version.ref = \"opentelemetry\" }\nopentelemetry-sdk = { module = \"io.opentelemetry:opentelemetry-sdk\", version.ref = \"opentelemetry\" }\nopentelemetry-sdk-trace = { module = \"io.opentelemetry:opentelemetry-sdk-trace\", version.ref = \"opentelemetry\" }\nopentelemetry-semconv = { module = \"io.opentelemetry.semconv:opentelemetry-semconv\", version.ref = \"opentelemetry-semconv\" }\nopentelemetry-extension-trace-propagators = { module = \"io.opentelemetry:opentelemetry-extension-trace-propagators\", version.ref = \"opentelemetry\" }\nopentelemetry-instrumentation-annotations = { module = \"io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations\", version.ref = \"opentelemetry-instrumentation\" }\nopentelemetry-javaagent = { module = \"io.opentelemetry.javaagent:opentelemetry-javaagent\", version.ref = \"opentelemetry-instrumentation\" }\nopentelemetry-exporter-otlp = { module = \"io.opentelemetry:opentelemetry-exporter-otlp\", version.ref = \"opentelemetry\" }\nopentelemetry-proto = { module = \"io.opentelemetry.proto:opentelemetry-proto\", version = \"1.9.0-alpha\" }\n\n# ByteBuddy\nbytebuddy = { module = \"net.bytebuddy:byte-buddy\", version.ref = \"bytebuddy\" }\nbytebuddy-agent = { module = \"net.bytebuddy:byte-buddy-agent\", version.ref = \"bytebuddy\" }\n\n# testing\nmockito-kotlin = { module = \"org.mockito.kotlin:mockito-kotlin\", version.ref = \"mockito\" }\nkotest-runner-junit5 = { module = \"io.kotest:kotest-runner-junit5\", version.ref = \"kotest\" }\nkotest-framework-engine = { module = \"io.kotest:kotest-framework-engine\", version.ref = \"kotest\" }\nkotest-assertions-core = { module = \"io.kotest:kotest-property\", version.ref = \"kotest\" }\nkotest-arrow = { module = \"io.kotest:kotest-assertions-arrow\", version.ref = \"kotest\" }\njunit-jupiter-api = { module = \"org.junit.jupiter:junit-jupiter-api\", version.ref = \"junit-jupiter\" }\n\n[plugins]\nallopen = { id = \"org.jetbrains.kotlin.plugin.allopen\", version.ref = \"kotlin\" }\nspring-plugin = { id = \"org.jetbrains.kotlin.plugin.spring\", version.ref = \"kotlin\" }\nspring-boot = { id = \"org.springframework.boot\", version.ref = \"spring-boot\" }\nspring-boot-three = { id = \"org.springframework.boot\", version.ref = \"spring-boot-3x\" }\nspring-boot-four = { id = \"org.springframework.boot\", version.ref = \"spring-boot-4x\" }\nspring-dependencyManagement = { id = \"io.spring.dependency-management\", version.ref = \"spring-dependency-management\" }\nkover = { id = \"org.jetbrains.kotlinx.kover\", version.ref = \"kover\" }\nspotless = { id = \"com.diffplug.spotless\", version.ref = \"spotless\" }\ndetekt = { id = \"io.gitlab.arturbosch.detekt\", version.ref = \"detekt\" }\nwire = { id = \"com.squareup.wire\", version.ref = \"wire\" }\ntestLogger = { id = \"com.adarshr.test-logger\", version = \"4.0.0\" }\nprotobuf = { id = \"com.google.protobuf\", version = \"0.10.0\" }\nquarkus = { id = \"io.quarkus\", version.ref = \"quarkus\" }\nkotlinx-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\nbinaryCompatibilityValidator = { id = \"org.jetbrains.kotlinx.binary-compatibility-validator\", version.ref = \"kotlinBinaryCompatibilityValidator\" }\ngoogle-ksp = { id = \"com.google.devtools.ksp\", version = \"2.3.7\" }\nmicronaut-application = { id = \"io.micronaut.application\", version = \"4.6.2\" }\nmicronaut-aot = { id = \"io.micronaut.aot\", version = \"4.6.2\" }\nmicronaut-library = { id = \"io.micronaut.library\", version = \"4.6.2\" }\nmaven-publish = { id = \"com.vanniktech.maven.publish\", version = \"0.36.0\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.5.0-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "org.gradle.caching=true\norg.gradle.configuration-cache=true\norg.gradle.configuration-cache.parallel=true\norg.gradle.daemon=true\norg.gradle.parallel=true\n# Increased heap and metaspace for parallel test execution\norg.gradle.jvmargs=-XX:+UseParallelGC -Xmx3g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError\n# Number of parallel workers (adjust based on CI runner cores)\norg.gradle.workers.max=4\nprojectDescription=The easiest way of e2e testing in Kotlin\nprojectUrl=https://github.com/Trendyol/stove\nlicenceUrl=https://github.com/Trendyol/stove/blob/master/LICENCE\nlicence=Apache-2.0 license\nsnapshot=1.0.0\nversion=0.24.0\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "jreleaser.yml",
    "content": "project:\n  name: stove\n  description: The easiest way of e2e testing in Kotlin\n  license: Apache-2.0\n  links:\n    homepage: https://github.com/Trendyol/stove\n    documentation: https://github.com/Trendyol/stove\n    bugTracker: https://github.com/Trendyol/stove/issues\n  authors:\n    - Oguzhan Soykan\n  inceptionYear: \"2022\"\n\nrelease:\n  github:\n    owner: Trendyol\n    name: stove\n    tagName: \"v{{projectVersion}}\"\n    releaseName: \"Stove v{{projectVersion}}\"\n    overwrite: false\n    skipTag: false\n    changelog:\n      enabled: true\n      formatted: ALWAYS\n      preset: conventional-commits\n      contributors:\n        enabled: false\n      hide:\n        categories:\n          - merge\n        contributors:\n          - \"[bot]\"\n          - \"GitHub\"\n"
  },
  {
    "path": "lib/stove/api/stove.api",
    "content": "public abstract interface class com/trendyol/stove/containers/ContainerOptions {\n\tpublic abstract fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic abstract fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic abstract fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic abstract fun getRegistry ()Ljava/lang/String;\n\tpublic abstract fun getTag ()Ljava/lang/String;\n\tpublic abstract fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n}\n\npublic final class com/trendyol/stove/containers/ContainerOptions$DefaultImpls {\n\tpublic static fun getImageWithTag (Lcom/trendyol/stove/containers/ContainerOptions;)Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/containers/ExecResult {\n\tpublic fun <init> (ILjava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (ILjava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/containers/ExecResult;ILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getExitCode ()I\n\tpublic final fun getStderr ()Ljava/lang/String;\n\tpublic final fun getStdout ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/containers/ProvidedRegistryKt {\n\tpublic static final fun getDEFAULT_REGISTRY ()Ljava/lang/String;\n\tpublic static final fun setDEFAULT_REGISTRY (Ljava/lang/String;)V\n\tpublic static final fun withProvidedRegistry (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;\n\tpublic static synthetic fun withProvidedRegistry$default (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/lang/Object;\n}\n\npublic abstract interface class com/trendyol/stove/containers/StoveContainer : com/trendyol/stove/system/abstractions/SystemRuntime {\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic static synthetic fun execCommand$default (Lcom/trendyol/stove/containers/StoveContainer;[Ljava/lang/String;JILjava/lang/Object;)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic abstract fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\npublic final class com/trendyol/stove/containers/StoveContainer$DefaultImpls {\n\tpublic static fun execCommand (Lcom/trendyol/stove/containers/StoveContainer;[Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic static synthetic fun execCommand$default (Lcom/trendyol/stove/containers/StoveContainer;[Ljava/lang/String;JILjava/lang/Object;)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic static fun getContainerIdAccess (Lcom/trendyol/stove/containers/StoveContainer;)Ljava/lang/String;\n\tpublic static fun getDockerClientAccess (Lcom/trendyol/stove/containers/StoveContainer;)Lkotlin/Lazy;\n\tpublic static fun inspect (Lcom/trendyol/stove/containers/StoveContainer;)Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic static fun pause (Lcom/trendyol/stove/containers/StoveContainer;)V\n\tpublic static fun unpause (Lcom/trendyol/stove/containers/StoveContainer;)V\n}\n\npublic final class com/trendyol/stove/containers/StoveContainerInspectInformation {\n\tpublic fun <init> (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZZZLjava/lang/String;Ljava/lang/String;JLjava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component10 ()J\n\tpublic final fun component11 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/util/Map;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Z\n\tpublic final fun component6 ()Z\n\tpublic final fun component7 ()Z\n\tpublic final fun component8 ()Ljava/lang/String;\n\tpublic final fun component9 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZZZLjava/lang/String;Ljava/lang/String;JLjava/lang/String;)Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/containers/StoveContainerInspectInformation;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;ZZZLjava/lang/String;Ljava/lang/String;JLjava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getError ()Ljava/lang/String;\n\tpublic final fun getExitCode ()J\n\tpublic final fun getFinishedAt ()Ljava/lang/String;\n\tpublic final fun getId ()Ljava/lang/String;\n\tpublic final fun getLabels ()Ljava/util/Map;\n\tpublic final fun getName ()Ljava/lang/String;\n\tpublic final fun getPaused ()Z\n\tpublic final fun getRestarting ()Z\n\tpublic final fun getRunning ()Z\n\tpublic final fun getStartedAt ()Ljava/lang/String;\n\tpublic final fun getState ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract interface class com/trendyol/stove/database/migrations/DatabaseMigration {\n\tpublic abstract fun execute (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun getOrder ()I\n}\n\npublic final class com/trendyol/stove/database/migrations/MigrationCollection {\n\tpublic fun <init> ()V\n\tpublic final fun register (Lkotlin/reflect/KClass;)Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic final fun register (Lkotlin/reflect/KClass;Lcom/trendyol/stove/database/migrations/DatabaseMigration;)Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic final fun replace (Lkotlin/reflect/KClass;Lcom/trendyol/stove/database/migrations/DatabaseMigration;)Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic final fun run (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/database/migrations/MigrationPriority : java/lang/Enum {\n\tpublic static final field HIGHEST Lcom/trendyol/stove/database/migrations/MigrationPriority;\n\tpublic static final field LOWEST Lcom/trendyol/stove/database/migrations/MigrationPriority;\n\tpublic static fun getEntries ()Lkotlin/enums/EnumEntries;\n\tpublic final fun getValue ()I\n\tpublic static fun valueOf (Ljava/lang/String;)Lcom/trendyol/stove/database/migrations/MigrationPriority;\n\tpublic static fun values ()[Lcom/trendyol/stove/database/migrations/MigrationPriority;\n}\n\npublic abstract interface class com/trendyol/stove/database/migrations/SupportsMigrations {\n\tpublic abstract fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n}\n\npublic final class com/trendyol/stove/database/migrations/SupportsMigrations$DefaultImpls {\n\tpublic static fun migrations (Lcom/trendyol/stove/database/migrations/SupportsMigrations;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n}\n\npublic final class com/trendyol/stove/functional/ExtensionsKt {\n\tpublic static final fun evert (Larrow/core/Option;)Lcom/trendyol/stove/functional/Try;\n\tpublic static final fun evert (Lcom/trendyol/stove/functional/Try;)Larrow/core/Option;\n\tpublic static final fun flatten (Larrow/core/Option;)Larrow/core/Option;\n\tpublic static final fun flatten (Larrow/core/Option;)Ljava/util/List;\n\tpublic static final fun flatten (Lcom/trendyol/stove/functional/Try;)Larrow/core/Option;\n\tpublic static final fun flatten (Ljava/lang/Iterable;)Ljava/util/List;\n\tpublic static final fun get (Larrow/core/Option;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/functional/Failure : com/trendyol/stove/functional/Try {\n\tpublic fun <init> (Ljava/lang/Throwable;)V\n\tpublic final fun component1 ()Ljava/lang/Throwable;\n\tpublic final fun copy (Ljava/lang/Throwable;)Lcom/trendyol/stove/functional/Failure;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/functional/Failure;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/trendyol/stove/functional/Failure;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic synthetic fun get ()Ljava/lang/Object;\n\tpublic fun get ()Ljava/lang/Void;\n\tpublic final fun getException ()Ljava/lang/Throwable;\n\tpublic fun getFailed ()Lcom/trendyol/stove/functional/Try;\n\tpublic synthetic fun getOrNull ()Ljava/lang/Object;\n\tpublic fun getOrNull ()Ljava/lang/Void;\n\tpublic fun hashCode ()I\n\tpublic fun isFailure ()Z\n\tpublic fun isSuccess ()Z\n\tpublic fun toEither ()Larrow/core/Either;\n\tpublic fun toOption ()Larrow/core/Option;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/functional/Reflect {\n\tpublic static final field Companion Lcom/trendyol/stove/functional/Reflect$Companion;\n\tpublic fun <init> (Ljava/lang/Object;)V\n\tpublic final fun getInstance ()Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/functional/Reflect$Companion {\n}\n\npublic final class com/trendyol/stove/functional/Reflect$OnGoingReflect {\n\tpublic fun <init> (Lcom/trendyol/stove/functional/Reflect;Ljava/lang/Object;Ljava/lang/String;)V\n\tpublic final fun then (Ljava/lang/Object;)V\n}\n\npublic final class com/trendyol/stove/functional/Success : com/trendyol/stove/functional/Try {\n\tpublic fun <init> (Ljava/lang/Object;)V\n\tpublic final fun component1 ()Ljava/lang/Object;\n\tpublic final fun copy (Ljava/lang/Object;)Lcom/trendyol/stove/functional/Success;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/functional/Success;Ljava/lang/Object;ILjava/lang/Object;)Lcom/trendyol/stove/functional/Success;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun get ()Ljava/lang/Object;\n\tpublic fun getFailed ()Lcom/trendyol/stove/functional/Try;\n\tpublic fun getOrNull ()Ljava/lang/Object;\n\tpublic final fun getValue ()Ljava/lang/Object;\n\tpublic fun hashCode ()I\n\tpublic fun isFailure ()Z\n\tpublic fun isSuccess ()Z\n\tpublic fun toEither ()Larrow/core/Either;\n\tpublic fun toOption ()Larrow/core/Option;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract class com/trendyol/stove/functional/Try {\n\tpublic static final field Companion Lcom/trendyol/stove/functional/Try$Companion;\n\tpublic final fun filter (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/functional/Try;\n\tpublic final fun filterNot (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/functional/Try;\n\tpublic final fun filterOrElse (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/functional/Try;\n\tpublic final fun flatMap (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/functional/Try;\n\tpublic final fun fold (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;\n\tpublic final fun forEach (Lkotlin/jvm/functions/Function1;)V\n\tpublic abstract fun get ()Ljava/lang/Object;\n\tpublic abstract fun getFailed ()Lcom/trendyol/stove/functional/Try;\n\tpublic abstract fun getOrNull ()Ljava/lang/Object;\n\tpublic abstract fun isFailure ()Z\n\tpublic abstract fun isSuccess ()Z\n\tpublic final fun map (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/functional/Try;\n\tpublic abstract fun toEither ()Larrow/core/Either;\n\tpublic abstract fun toOption ()Larrow/core/Option;\n\tpublic final fun transform (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/functional/Try;\n\tpublic final fun zip (Lcom/trendyol/stove/functional/Try;)Lcom/trendyol/stove/functional/Try;\n\tpublic final fun zip (Lcom/trendyol/stove/functional/Try;Lkotlin/jvm/functions/Function2;)Lcom/trendyol/stove/functional/Try;\n}\n\npublic final class com/trendyol/stove/functional/Try$Companion {\n\tpublic final fun invoke (Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/functional/Try;\n}\n\npublic final class com/trendyol/stove/functional/TryKt {\n\tpublic static final fun filterNotNull (Lcom/trendyol/stove/functional/Try;)Lcom/trendyol/stove/functional/Try;\n\tpublic static final fun flatten (Lcom/trendyol/stove/functional/Try;)Lcom/trendyol/stove/functional/Try;\n\tpublic static final fun getOrElse (Lcom/trendyol/stove/functional/Try;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;\n\tpublic static final fun orElse (Lcom/trendyol/stove/functional/Try;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/functional/Try;\n\tpublic static final fun recover (Lcom/trendyol/stove/functional/Try;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/functional/Try;\n\tpublic static final fun recoverWith (Lcom/trendyol/stove/functional/Try;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/functional/Try;\n}\n\npublic abstract class com/trendyol/stove/http/StoveHttpResponse {\n\tpublic synthetic fun <init> (ILjava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getHeaders ()Ljava/util/Map;\n\tpublic fun getStatus ()I\n}\n\npublic final class com/trendyol/stove/http/StoveHttpResponse$Bodiless : com/trendyol/stove/http/StoveHttpResponse {\n\tpublic fun <init> (ILjava/util/Map;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()Ljava/util/Map;\n\tpublic final fun copy (ILjava/util/Map;)Lcom/trendyol/stove/http/StoveHttpResponse$Bodiless;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/http/StoveHttpResponse$Bodiless;ILjava/util/Map;ILjava/lang/Object;)Lcom/trendyol/stove/http/StoveHttpResponse$Bodiless;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getHeaders ()Ljava/util/Map;\n\tpublic fun getStatus ()I\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/http/StoveHttpResponse$WithBody : com/trendyol/stove/http/StoveHttpResponse {\n\tpublic fun <init> (ILjava/util/Map;Lkotlin/jvm/functions/Function1;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()Ljava/util/Map;\n\tpublic final fun component3 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (ILjava/util/Map;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/http/StoveHttpResponse$WithBody;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/http/StoveHttpResponse$WithBody;ILjava/util/Map;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/http/StoveHttpResponse$WithBody;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBody ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getHeaders ()Ljava/util/Map;\n\tpublic fun getStatus ()I\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/messaging/FailedObservedMessage : com/trendyol/stove/messaging/ObservedMessage {\n\tpublic fun <init> (Ljava/lang/Object;Lcom/trendyol/stove/messaging/MessageMetadata;Ljava/lang/Throwable;)V\n\tpublic final fun component1 ()Ljava/lang/Object;\n\tpublic final fun component2 ()Lcom/trendyol/stove/messaging/MessageMetadata;\n\tpublic final fun component3 ()Ljava/lang/Throwable;\n\tpublic final fun copy (Ljava/lang/Object;Lcom/trendyol/stove/messaging/MessageMetadata;Ljava/lang/Throwable;)Lcom/trendyol/stove/messaging/FailedObservedMessage;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/messaging/FailedObservedMessage;Ljava/lang/Object;Lcom/trendyol/stove/messaging/MessageMetadata;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/trendyol/stove/messaging/FailedObservedMessage;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getActual ()Ljava/lang/Object;\n\tpublic fun getMetadata ()Lcom/trendyol/stove/messaging/MessageMetadata;\n\tpublic final fun getReason ()Ljava/lang/Throwable;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/messaging/FailedParsedMessage : com/trendyol/stove/messaging/ParsedMessage {\n\tpublic fun <init> (Larrow/core/Option;Lcom/trendyol/stove/messaging/MessageMetadata;Ljava/lang/Throwable;)V\n\tpublic fun getMessage ()Larrow/core/Option;\n\tpublic fun getMetadata ()Lcom/trendyol/stove/messaging/MessageMetadata;\n\tpublic final fun getReason ()Ljava/lang/Throwable;\n}\n\npublic final class com/trendyol/stove/messaging/Failure {\n\tpublic fun <init> (Lcom/trendyol/stove/messaging/ObservedMessage;Ljava/lang/Throwable;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/messaging/ObservedMessage;\n\tpublic final fun component2 ()Ljava/lang/Throwable;\n\tpublic final fun copy (Lcom/trendyol/stove/messaging/ObservedMessage;Ljava/lang/Throwable;)Lcom/trendyol/stove/messaging/Failure;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/messaging/Failure;Lcom/trendyol/stove/messaging/ObservedMessage;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/trendyol/stove/messaging/Failure;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getMessage ()Lcom/trendyol/stove/messaging/ObservedMessage;\n\tpublic final fun getReason ()Ljava/lang/Throwable;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/messaging/MessageMetadata {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/util/Map;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lcom/trendyol/stove/messaging/MessageMetadata;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/messaging/MessageMetadata;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/trendyol/stove/messaging/MessageMetadata;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getHeaders ()Ljava/util/Map;\n\tpublic final fun getKey ()Ljava/lang/String;\n\tpublic final fun getTopic ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic class com/trendyol/stove/messaging/ObservedMessage {\n\tpublic fun <init> (Ljava/lang/Object;Lcom/trendyol/stove/messaging/MessageMetadata;)V\n\tpublic fun getActual ()Ljava/lang/Object;\n\tpublic fun getMetadata ()Lcom/trendyol/stove/messaging/MessageMetadata;\n}\n\npublic abstract interface class com/trendyol/stove/messaging/ParsedMessage {\n\tpublic abstract fun getMessage ()Larrow/core/Option;\n\tpublic abstract fun getMetadata ()Lcom/trendyol/stove/messaging/MessageMetadata;\n}\n\npublic final class com/trendyol/stove/messaging/SuccessfulParsedMessage : com/trendyol/stove/messaging/ParsedMessage {\n\tpublic fun <init> (Larrow/core/Option;Lcom/trendyol/stove/messaging/MessageMetadata;)V\n\tpublic fun getMessage ()Larrow/core/Option;\n\tpublic fun getMetadata ()Lcom/trendyol/stove/messaging/MessageMetadata;\n}\n\npublic final class com/trendyol/stove/reporting/AssertionResult : java/lang/Enum {\n\tpublic static final field Companion Lcom/trendyol/stove/reporting/AssertionResult$Companion;\n\tpublic static final field FAILED Lcom/trendyol/stove/reporting/AssertionResult;\n\tpublic static final field PASSED Lcom/trendyol/stove/reporting/AssertionResult;\n\tpublic static fun getEntries ()Lkotlin/enums/EnumEntries;\n\tpublic static fun valueOf (Ljava/lang/String;)Lcom/trendyol/stove/reporting/AssertionResult;\n\tpublic static fun values ()[Lcom/trendyol/stove/reporting/AssertionResult;\n}\n\npublic final class com/trendyol/stove/reporting/AssertionResult$Companion {\n\tpublic final fun of (Z)Lcom/trendyol/stove/reporting/AssertionResult;\n}\n\npublic final class com/trendyol/stove/reporting/JsonReportEntry {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component10 ()Ljava/lang/String;\n\tpublic final fun component11 ()Ljava/lang/Object;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Ljava/lang/Object;\n\tpublic final fun component6 ()Ljava/lang/Object;\n\tpublic final fun component7 ()Ljava/util/Map;\n\tpublic final fun component8 ()Ljava/lang/Object;\n\tpublic final fun component9 ()Ljava/lang/Object;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;)Lcom/trendyol/stove/reporting/JsonReportEntry;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/reporting/JsonReportEntry;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/util/Map;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;ILjava/lang/Object;)Lcom/trendyol/stove/reporting/JsonReportEntry;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAction ()Ljava/lang/String;\n\tpublic final fun getActual ()Ljava/lang/Object;\n\tpublic final fun getError ()Ljava/lang/Object;\n\tpublic final fun getExpected ()Ljava/lang/Object;\n\tpublic final fun getInput ()Ljava/lang/Object;\n\tpublic final fun getMetadata ()Ljava/util/Map;\n\tpublic final fun getOutput ()Ljava/lang/Object;\n\tpublic final fun getResult ()Ljava/lang/String;\n\tpublic final fun getSystem ()Ljava/lang/String;\n\tpublic final fun getTestId ()Ljava/lang/String;\n\tpublic final fun getTimestamp ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/reporting/JsonReportRenderer : com/trendyol/stove/reporting/ReportRenderer {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/reporting/JsonReportRenderer;\n\tpublic fun render (Lcom/trendyol/stove/reporting/TestReport;Ljava/util/List;)Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/reporting/JsonSummary {\n\tpublic fun <init> (III)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()I\n\tpublic final fun component3 ()I\n\tpublic final fun copy (III)Lcom/trendyol/stove/reporting/JsonSummary;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/reporting/JsonSummary;IIIILjava/lang/Object;)Lcom/trendyol/stove/reporting/JsonSummary;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getFailed ()I\n\tpublic final fun getPassed ()I\n\tpublic final fun getTotal ()I\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/reporting/JsonTestReport {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Lcom/trendyol/stove/reporting/JsonSummary;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/util/List;\n\tpublic final fun component5 ()Ljava/util/Map;\n\tpublic final fun component6 ()Lcom/trendyol/stove/reporting/JsonSummary;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Lcom/trendyol/stove/reporting/JsonSummary;)Lcom/trendyol/stove/reporting/JsonTestReport;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/reporting/JsonTestReport;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/util/Map;Lcom/trendyol/stove/reporting/JsonSummary;ILjava/lang/Object;)Lcom/trendyol/stove/reporting/JsonTestReport;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getEntries ()Ljava/util/List;\n\tpublic final fun getSummary ()Lcom/trendyol/stove/reporting/JsonSummary;\n\tpublic final fun getSystemSnapshots ()Ljava/util/Map;\n\tpublic final fun getTestId ()Ljava/lang/String;\n\tpublic final fun getTestName ()Ljava/lang/String;\n\tpublic final fun getTimestamp ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/reporting/PrettyConsoleRenderer : com/trendyol/stove/reporting/ReportRenderer {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/reporting/PrettyConsoleRenderer;\n\tpublic fun render (Lcom/trendyol/stove/reporting/TestReport;Ljava/util/List;)Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/reporting/ReportEntry {\n\tpublic static final field Companion Lcom/trendyol/stove/reporting/ReportEntry$Companion;\n\tpublic fun <init> (Ljava/time/Instant;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/reporting/AssertionResult;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;)V\n\tpublic synthetic fun <init> (Ljava/time/Instant;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/reporting/AssertionResult;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/time/Instant;\n\tpublic final fun component10 ()Larrow/core/Option;\n\tpublic final fun component11 ()Larrow/core/Option;\n\tpublic final fun component12 ()Larrow/core/Option;\n\tpublic final fun component13 ()Larrow/core/Option;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lcom/trendyol/stove/reporting/AssertionResult;\n\tpublic final fun component6 ()Larrow/core/Option;\n\tpublic final fun component7 ()Larrow/core/Option;\n\tpublic final fun component8 ()Ljava/util/Map;\n\tpublic final fun component9 ()Larrow/core/Option;\n\tpublic final fun copy (Ljava/time/Instant;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/reporting/AssertionResult;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;)Lcom/trendyol/stove/reporting/ReportEntry;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/reporting/ReportEntry;Ljava/time/Instant;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/reporting/AssertionResult;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;ILjava/lang/Object;)Lcom/trendyol/stove/reporting/ReportEntry;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAction ()Ljava/lang/String;\n\tpublic final fun getActual ()Larrow/core/Option;\n\tpublic final fun getError ()Larrow/core/Option;\n\tpublic final fun getExecutionTrace ()Larrow/core/Option;\n\tpublic final fun getExpected ()Larrow/core/Option;\n\tpublic final fun getHasTrace ()Z\n\tpublic final fun getInput ()Larrow/core/Option;\n\tpublic final fun getMetadata ()Ljava/util/Map;\n\tpublic final fun getOutput ()Larrow/core/Option;\n\tpublic final fun getResult ()Lcom/trendyol/stove/reporting/AssertionResult;\n\tpublic final fun getSummary ()Ljava/lang/String;\n\tpublic final fun getSystem ()Ljava/lang/String;\n\tpublic final fun getTestId ()Ljava/lang/String;\n\tpublic final fun getTimestamp ()Ljava/time/Instant;\n\tpublic final fun getTraceId ()Larrow/core/Option;\n\tpublic fun hashCode ()I\n\tpublic final fun isFailed ()Z\n\tpublic final fun isPassed ()Z\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/reporting/ReportEntry$Companion {\n\tpublic final fun action (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLarrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;)Lcom/trendyol/stove/reporting/ReportEntry;\n\tpublic static synthetic fun action$default (Lcom/trendyol/stove/reporting/ReportEntry$Companion;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLarrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;ILjava/lang/Object;)Lcom/trendyol/stove/reporting/ReportEntry;\n\tpublic final fun failure (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;)Lcom/trendyol/stove/reporting/ReportEntry;\n\tpublic static synthetic fun failure$default (Lcom/trendyol/stove/reporting/ReportEntry$Companion;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Larrow/core/Option;ILjava/lang/Object;)Lcom/trendyol/stove/reporting/ReportEntry;\n\tpublic final fun success (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;)Lcom/trendyol/stove/reporting/ReportEntry;\n\tpublic static synthetic fun success$default (Lcom/trendyol/stove/reporting/ReportEntry$Companion;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;ILjava/lang/Object;)Lcom/trendyol/stove/reporting/ReportEntry;\n}\n\npublic abstract interface class com/trendyol/stove/reporting/ReportEventListener {\n\tpublic fun onEntryRecorded (Lcom/trendyol/stove/reporting/ReportEntry;)V\n\tpublic fun onTestEnded (Ljava/lang/String;)V\n\tpublic fun onTestFailed (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic fun onTestStarted (Lcom/trendyol/stove/reporting/StoveTestContext;)V\n}\n\npublic final class com/trendyol/stove/reporting/ReportEventListener$DefaultImpls {\n\tpublic static fun onEntryRecorded (Lcom/trendyol/stove/reporting/ReportEventListener;Lcom/trendyol/stove/reporting/ReportEntry;)V\n\tpublic static fun onTestEnded (Lcom/trendyol/stove/reporting/ReportEventListener;Ljava/lang/String;)V\n\tpublic static fun onTestFailed (Lcom/trendyol/stove/reporting/ReportEventListener;Ljava/lang/String;Ljava/lang/String;)V\n\tpublic static fun onTestStarted (Lcom/trendyol/stove/reporting/ReportEventListener;Lcom/trendyol/stove/reporting/StoveTestContext;)V\n}\n\npublic abstract interface class com/trendyol/stove/reporting/ReportRenderer {\n\tpublic abstract fun render (Lcom/trendyol/stove/reporting/TestReport;Ljava/util/List;)Ljava/lang/String;\n}\n\npublic abstract interface class com/trendyol/stove/reporting/Reports {\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun report$default (Lcom/trendyol/stove/reporting/Reports;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n}\n\npublic final class com/trendyol/stove/reporting/Reports$DefaultImpls {\n\tpublic static fun getReportSystemName (Lcom/trendyol/stove/reporting/Reports;)Ljava/lang/String;\n\tpublic static fun getReporter (Lcom/trendyol/stove/reporting/Reports;)Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic static fun report (Lcom/trendyol/stove/reporting/Reports;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun report$default (Lcom/trendyol/stove/reporting/Reports;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic static fun snapshot (Lcom/trendyol/stove/reporting/Reports;)Lcom/trendyol/stove/reporting/SystemSnapshot;\n}\n\npublic abstract interface class com/trendyol/stove/reporting/SpanEventListener {\n\tpublic fun onSpanRecorded (Lcom/trendyol/stove/tracing/SpanInfo;)V\n}\n\npublic final class com/trendyol/stove/reporting/SpanEventListener$DefaultImpls {\n\tpublic static fun onSpanRecorded (Lcom/trendyol/stove/reporting/SpanEventListener;Lcom/trendyol/stove/tracing/SpanInfo;)V\n}\n\npublic abstract interface class com/trendyol/stove/reporting/SpanListenerRegistry {\n\tpublic abstract fun addSpanListener (Lcom/trendyol/stove/reporting/SpanEventListener;)V\n}\n\npublic final class com/trendyol/stove/reporting/StoveReporter {\n\tpublic static final field Companion Lcom/trendyol/stove/reporting/StoveReporter$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Z)V\n\tpublic synthetic fun <init> (ZILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun addListener (Lcom/trendyol/stove/reporting/ReportEventListener;)V\n\tpublic final fun clear ()V\n\tpublic final fun clear (Ljava/lang/String;)V\n\tpublic final fun collectSnapshots ()Ljava/util/List;\n\tpublic final fun currentTest ()Lcom/trendyol/stove/reporting/TestReport;\n\tpublic final fun currentTestId ()Ljava/lang/String;\n\tpublic final fun currentTestOrNull ()Lcom/trendyol/stove/reporting/TestReport;\n\tpublic final fun dump (Lcom/trendyol/stove/reporting/ReportRenderer;)Ljava/lang/String;\n\tpublic final fun dumpIfFailed (Lcom/trendyol/stove/reporting/ReportRenderer;)Ljava/lang/String;\n\tpublic static synthetic fun dumpIfFailed$default (Lcom/trendyol/stove/reporting/StoveReporter;Lcom/trendyol/stove/reporting/ReportRenderer;ILjava/lang/Object;)Ljava/lang/String;\n\tpublic final fun endTest ()V\n\tpublic final fun hasFailures ()Z\n\tpublic final fun isEnabled ()Z\n\tpublic final fun printIfFailed (Lcom/trendyol/stove/reporting/ReportRenderer;)V\n\tpublic static synthetic fun printIfFailed$default (Lcom/trendyol/stove/reporting/StoveReporter;Lcom/trendyol/stove/reporting/ReportRenderer;ILjava/lang/Object;)V\n\tpublic final fun record (Lcom/trendyol/stove/reporting/ReportEntry;)V\n\tpublic final fun removeListener (Lcom/trendyol/stove/reporting/ReportEventListener;)V\n\tpublic final fun reportFailure (Ljava/lang/String;)V\n\tpublic final fun startTest (Lcom/trendyol/stove/reporting/StoveTestContext;)V\n}\n\npublic final class com/trendyol/stove/reporting/StoveReporter$Companion {\n}\n\npublic final class com/trendyol/stove/reporting/StoveTestContext : kotlin/coroutines/AbstractCoroutineContextElement {\n\tpublic static final field Key Lcom/trendyol/stove/reporting/StoveTestContext$Key;\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/util/List;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/trendyol/stove/reporting/StoveTestContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/reporting/StoveTestContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/reporting/StoveTestContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getSpecName ()Ljava/lang/String;\n\tpublic final fun getTestId ()Ljava/lang/String;\n\tpublic final fun getTestName ()Ljava/lang/String;\n\tpublic final fun getTestPath ()Ljava/util/List;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/reporting/StoveTestContext$Key : kotlin/coroutines/CoroutineContext$Key {\n}\n\npublic final class com/trendyol/stove/reporting/StoveTestContextHolder {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/reporting/StoveTestContextHolder;\n\tpublic final fun clear ()V\n\tpublic final fun get ()Lcom/trendyol/stove/reporting/StoveTestContext;\n\tpublic final fun set (Lcom/trendyol/stove/reporting/StoveTestContext;)V\n}\n\npublic final class com/trendyol/stove/reporting/StoveTestContextKt {\n\tpublic static final fun currentStoveTestContext (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/reporting/StoveTestErrorException : java/lang/Exception {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n}\n\npublic final class com/trendyol/stove/reporting/StoveTestFailureException : java/lang/AssertionError {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n}\n\npublic final class com/trendyol/stove/reporting/SystemSnapshot {\n\tpublic fun <init> (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/util/Map;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/reporting/SystemSnapshot;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getState ()Ljava/util/Map;\n\tpublic final fun getSummary ()Ljava/lang/String;\n\tpublic final fun getSystem ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/reporting/TestReport {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun clear ()V\n\tpublic final fun entries ()Ljava/util/List;\n\tpublic final fun entriesForThisTest ()Ljava/util/List;\n\tpublic final fun failures ()Ljava/util/List;\n\tpublic final fun failuresForThisTest ()Ljava/util/List;\n\tpublic final fun getTestId ()Ljava/lang/String;\n\tpublic final fun getTestName ()Ljava/lang/String;\n\tpublic final fun hasFailures ()Z\n\tpublic final fun record (Lcom/trendyol/stove/reporting/ReportEntry;)V\n}\n\npublic final class com/trendyol/stove/reporting/TestReportKt {\n\tpublic static final fun failures (Ljava/util/List;)Ljava/util/List;\n\tpublic static final fun forSystem (Ljava/util/List;Ljava/lang/String;)Ljava/util/List;\n\tpublic static final fun forTest (Ljava/util/List;Ljava/lang/String;)Ljava/util/List;\n\tpublic static final fun passed (Ljava/util/List;)Ljava/util/List;\n}\n\npublic abstract interface class com/trendyol/stove/reporting/TraceProvider {\n\tpublic abstract fun getTraceVisualizationForCurrentTest (J)Larrow/core/Option;\n\tpublic static synthetic fun getTraceVisualizationForCurrentTest$default (Lcom/trendyol/stove/reporting/TraceProvider;JILjava/lang/Object;)Larrow/core/Option;\n}\n\npublic final class com/trendyol/stove/reporting/TraceProvider$DefaultImpls {\n\tpublic static synthetic fun getTraceVisualizationForCurrentTest$default (Lcom/trendyol/stove/reporting/TraceProvider;JILjava/lang/Object;)Larrow/core/Option;\n}\n\npublic final class com/trendyol/stove/serialization/E2eObjectMapperConfig {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/serialization/E2eObjectMapperConfig;\n\tpublic final fun createObjectMapperWithDefaults ()Lcom/fasterxml/jackson/databind/ObjectMapper;\n}\n\npublic final class com/trendyol/stove/serialization/IsoInstantDeserializer : com/fasterxml/jackson/databind/JsonDeserializer {\n\tpublic fun <init> ()V\n\tpublic synthetic fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/lang/Object;\n\tpublic fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/time/Instant;\n}\n\npublic final class com/trendyol/stove/serialization/IsoInstantSerializer : com/fasterxml/jackson/databind/JsonSerializer {\n\tpublic fun <init> ()V\n\tpublic synthetic fun serialize (Ljava/lang/Object;Lcom/fasterxml/jackson/core/JsonGenerator;Lcom/fasterxml/jackson/databind/SerializerProvider;)V\n\tpublic fun serialize (Ljava/time/Instant;Lcom/fasterxml/jackson/core/JsonGenerator;Lcom/fasterxml/jackson/databind/SerializerProvider;)V\n}\n\npublic final class com/trendyol/stove/serialization/StoveGson {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/serialization/StoveGson;\n\tpublic final fun anyByteArraySerde (Lcom/google/gson/Gson;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic static synthetic fun anyByteArraySerde$default (Lcom/trendyol/stove/serialization/StoveGson;Lcom/google/gson/Gson;ILjava/lang/Object;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic final fun anyJsonStringSerde (Lcom/google/gson/Gson;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic static synthetic fun anyJsonStringSerde$default (Lcom/trendyol/stove/serialization/StoveGson;Lcom/google/gson/Gson;ILjava/lang/Object;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic final fun byConfiguring (Lkotlin/jvm/functions/Function1;)Lcom/google/gson/Gson;\n\tpublic final fun getDefault ()Lcom/google/gson/Gson;\n}\n\npublic final class com/trendyol/stove/serialization/StoveGsonByteArraySerializer : com/trendyol/stove/serialization/StoveSerde {\n\tpublic fun <init> (Lcom/google/gson/Gson;)V\n\tpublic synthetic fun deserialize (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic fun deserialize ([BLjava/lang/Class;)Ljava/lang/Object;\n\tpublic synthetic fun deserializeEither (Ljava/lang/Object;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic fun deserializeEither ([BLjava/lang/Class;)Larrow/core/Either;\n\tpublic synthetic fun serialize (Ljava/lang/Object;)Ljava/lang/Object;\n\tpublic fun serialize (Ljava/lang/Object;)[B\n}\n\npublic final class com/trendyol/stove/serialization/StoveGsonStringSerializer : com/trendyol/stove/serialization/StoveSerde {\n\tpublic fun <init> (Lcom/google/gson/Gson;)V\n\tpublic synthetic fun deserialize (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic fun deserialize (Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic synthetic fun deserializeEither (Ljava/lang/Object;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic fun deserializeEither (Ljava/lang/String;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic synthetic fun serialize (Ljava/lang/Object;)Ljava/lang/Object;\n\tpublic fun serialize (Ljava/lang/Object;)Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/serialization/StoveJackson {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/serialization/StoveJackson;\n\tpublic final fun anyByteArraySerde (Lcom/fasterxml/jackson/databind/ObjectMapper;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic static synthetic fun anyByteArraySerde$default (Lcom/trendyol/stove/serialization/StoveJackson;Lcom/fasterxml/jackson/databind/ObjectMapper;ILjava/lang/Object;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic final fun anyJsonStringSerde (Lcom/fasterxml/jackson/databind/ObjectMapper;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic static synthetic fun anyJsonStringSerde$default (Lcom/trendyol/stove/serialization/StoveJackson;Lcom/fasterxml/jackson/databind/ObjectMapper;ILjava/lang/Object;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic final fun byConfiguring (Lkotlin/jvm/functions/Function1;)Lcom/fasterxml/jackson/databind/ObjectMapper;\n\tpublic final fun getDefault ()Lcom/fasterxml/jackson/databind/ObjectMapper;\n}\n\npublic final class com/trendyol/stove/serialization/StoveJacksonByteArraySerializer : com/trendyol/stove/serialization/StoveSerde {\n\tpublic fun <init> (Lcom/fasterxml/jackson/databind/ObjectMapper;)V\n\tpublic synthetic fun deserialize (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic fun deserialize ([BLjava/lang/Class;)Ljava/lang/Object;\n\tpublic synthetic fun deserializeEither (Ljava/lang/Object;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic fun deserializeEither ([BLjava/lang/Class;)Larrow/core/Either;\n\tpublic synthetic fun serialize (Ljava/lang/Object;)Ljava/lang/Object;\n\tpublic fun serialize (Ljava/lang/Object;)[B\n}\n\npublic final class com/trendyol/stove/serialization/StoveJacksonStringSerializer : com/trendyol/stove/serialization/StoveSerde {\n\tpublic fun <init> (Lcom/fasterxml/jackson/databind/ObjectMapper;)V\n\tpublic synthetic fun deserialize (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic fun deserialize (Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic synthetic fun deserializeEither (Ljava/lang/Object;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic fun deserializeEither (Ljava/lang/String;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic synthetic fun serialize (Ljava/lang/Object;)Ljava/lang/Object;\n\tpublic fun serialize (Ljava/lang/Object;)Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/serialization/StoveKotlinx {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/serialization/StoveKotlinx;\n\tpublic final fun anyByteArraySerde (Lkotlinx/serialization/json/Json;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic static synthetic fun anyByteArraySerde$default (Lcom/trendyol/stove/serialization/StoveKotlinx;Lkotlinx/serialization/json/Json;ILjava/lang/Object;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic final fun anyJsonStringSerde (Lkotlinx/serialization/json/Json;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic static synthetic fun anyJsonStringSerde$default (Lcom/trendyol/stove/serialization/StoveKotlinx;Lkotlinx/serialization/json/Json;ILjava/lang/Object;)Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic final fun byConfiguring (Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/Json;\n\tpublic final fun getDefault ()Lkotlinx/serialization/json/Json;\n}\n\npublic final class com/trendyol/stove/serialization/StoveKotlinxByteArraySerializer : com/trendyol/stove/serialization/StoveSerde {\n\tpublic fun <init> (Lkotlinx/serialization/json/Json;)V\n\tpublic synthetic fun deserialize (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic fun deserialize ([BLjava/lang/Class;)Ljava/lang/Object;\n\tpublic synthetic fun deserializeEither (Ljava/lang/Object;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic fun deserializeEither ([BLjava/lang/Class;)Larrow/core/Either;\n\tpublic synthetic fun serialize (Ljava/lang/Object;)Ljava/lang/Object;\n\tpublic fun serialize (Ljava/lang/Object;)[B\n}\n\npublic final class com/trendyol/stove/serialization/StoveKotlinxStringSerializer : com/trendyol/stove/serialization/StoveSerde {\n\tpublic fun <init> (Lkotlinx/serialization/json/Json;)V\n\tpublic synthetic fun deserialize (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic fun deserialize (Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic synthetic fun deserializeEither (Ljava/lang/Object;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic fun deserializeEither (Ljava/lang/String;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic synthetic fun serialize (Ljava/lang/Object;)Ljava/lang/Object;\n\tpublic fun serialize (Ljava/lang/Object;)Ljava/lang/String;\n}\n\npublic abstract interface class com/trendyol/stove/serialization/StoveSerde {\n\tpublic static final field Companion Lcom/trendyol/stove/serialization/StoveSerde$Companion;\n\tpublic abstract fun deserialize (Ljava/lang/Object;Ljava/lang/Class;)Ljava/lang/Object;\n\tpublic fun deserializeEither (Ljava/lang/Object;Ljava/lang/Class;)Larrow/core/Either;\n\tpublic abstract fun serialize (Ljava/lang/Object;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/serialization/StoveSerde$Companion {\n\tpublic final fun getGson ()Lcom/trendyol/stove/serialization/StoveGson;\n\tpublic final fun getJackson ()Lcom/trendyol/stove/serialization/StoveJackson;\n\tpublic final fun getKotlinx ()Lcom/trendyol/stove/serialization/StoveKotlinx;\n}\n\npublic final class com/trendyol/stove/serialization/StoveSerde$DefaultImpls {\n\tpublic static fun deserializeEither (Lcom/trendyol/stove/serialization/StoveSerde;Ljava/lang/Object;Ljava/lang/Class;)Larrow/core/Either;\n}\n\npublic abstract class com/trendyol/stove/serialization/StoveSerde$StoveSerdeProblem : java/lang/RuntimeException {\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V\n}\n\npublic final class com/trendyol/stove/serialization/StoveSerde$StoveSerdeProblem$BecauseOfDeserialization : com/trendyol/stove/serialization/StoveSerde$StoveSerdeProblem {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n}\n\npublic final class com/trendyol/stove/serialization/StoveSerde$StoveSerdeProblem$BecauseOfDeserializationButExpected : com/trendyol/stove/serialization/StoveSerde$StoveSerdeProblem {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n}\n\npublic final class com/trendyol/stove/serialization/StoveSerde$StoveSerdeProblem$BecauseOfSerialization : com/trendyol/stove/serialization/StoveSerde$StoveSerdeProblem {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n}\n\npublic abstract class com/trendyol/stove/system/BridgeSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/AfterRunAwareWithContext, com/trendyol/stove/system/abstractions/PluggedSystem {\n\tprotected field ctx Ljava/lang/Object;\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;)V\n\tpublic fun afterRun (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun close ()V\n\tpublic final fun ensureContextInitialized ()V\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun get (Lkotlin/reflect/KClass;)Ljava/lang/Object;\n\tpublic fun getByType (Lkotlin/reflect/KType;)Ljava/lang/Object;\n\tprotected final fun getCtx ()Ljava/lang/Object;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tprotected final fun setCtx (Ljava/lang/Object;)V\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/system/BridgeSystemKt {\n\tpublic static final fun bridge (Lcom/trendyol/stove/system/Stove;)Lcom/trendyol/stove/system/BridgeSystem;\n\tpublic static final fun bridge-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/BridgeSystem;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun withBridgeSystem (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/BridgeSystem;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/system/PortFinder {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/system/PortFinder;\n\tpublic static final fun findAvailablePort ()I\n\tpublic static final fun findAvailablePortAsString ()Ljava/lang/String;\n\tpublic static final fun findAvailablePortFrom (I)I\n\tpublic static final fun findAvailablePortFromAsString (I)Ljava/lang/String;\n\tpublic static final fun isPortAvailable (I)Z\n}\n\npublic final class com/trendyol/stove/system/PropertiesFile {\n\tpublic static final field Companion Lcom/trendyol/stove/system/PropertiesFile$Companion;\n\tpublic static final field REUSE_ENABLED Ljava/lang/String;\n\tpublic fun <init> ()V\n\tpublic final fun detectAndLogStatus ()V\n\tpublic final fun enable ()V\n}\n\npublic final class com/trendyol/stove/system/PropertiesFile$Companion {\n}\n\npublic final class com/trendyol/stove/system/ProvidedApplicationOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lcom/trendyol/stove/system/ReadinessStrategy;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/ReadinessStrategy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic final fun copy (Lcom/trendyol/stove/system/ReadinessStrategy;)Lcom/trendyol/stove/system/ProvidedApplicationOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/system/ProvidedApplicationOptions;Lcom/trendyol/stove/system/ReadinessStrategy;ILjava/lang/Object;)Lcom/trendyol/stove/system/ProvidedApplicationOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getReadiness ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/system/ProvidedApplicationUnderTest : com/trendyol/stove/system/abstractions/ApplicationUnderTest {\n\tpublic fun <init> (Lcom/trendyol/stove/system/ProvidedApplicationOptions;)V\n\tpublic fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/system/ProvidedApplicationUnderTestKt {\n\tpublic static final fun providedApplication-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n\tpublic static synthetic fun providedApplication-ypJx7X8$default (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n}\n\npublic final class com/trendyol/stove/system/ReadinessChecker {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/system/ReadinessChecker;\n\tpublic final fun check (Lcom/trendyol/stove/system/ReadinessStrategy;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract interface class com/trendyol/stove/system/ReadinessStrategy {\n}\n\npublic final class com/trendyol/stove/system/ReadinessStrategy$FixedDelay : com/trendyol/stove/system/ReadinessStrategy {\n\tpublic synthetic fun <init> (JILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic synthetic fun <init> (JLkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1-UwyO8pc ()J\n\tpublic final fun copy-LRDsOJo (J)Lcom/trendyol/stove/system/ReadinessStrategy$FixedDelay;\n\tpublic static synthetic fun copy-LRDsOJo$default (Lcom/trendyol/stove/system/ReadinessStrategy$FixedDelay;JILjava/lang/Object;)Lcom/trendyol/stove/system/ReadinessStrategy$FixedDelay;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getDelay-UwyO8pc ()J\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/system/ReadinessStrategy$HttpGet : com/trendyol/stove/system/ReadinessStrategy {\n\tpublic synthetic fun <init> (Ljava/lang/String;JIJLjava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;JIJLjava/util/Set;Lkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2-UwyO8pc ()J\n\tpublic final fun component3 ()I\n\tpublic final fun component4-UwyO8pc ()J\n\tpublic final fun component5 ()Ljava/util/Set;\n\tpublic final fun copy-7Q0yyfQ (Ljava/lang/String;JIJLjava/util/Set;)Lcom/trendyol/stove/system/ReadinessStrategy$HttpGet;\n\tpublic static synthetic fun copy-7Q0yyfQ$default (Lcom/trendyol/stove/system/ReadinessStrategy$HttpGet;Ljava/lang/String;JIJLjava/util/Set;ILjava/lang/Object;)Lcom/trendyol/stove/system/ReadinessStrategy$HttpGet;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getExpectedStatusCodes ()Ljava/util/Set;\n\tpublic final fun getRetries ()I\n\tpublic final fun getRetryDelay-UwyO8pc ()J\n\tpublic final fun getTimeout-UwyO8pc ()J\n\tpublic final fun getUrl ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/system/ReadinessStrategy$Probe : com/trendyol/stove/system/ReadinessStrategy {\n\tpublic synthetic fun <init> (IJLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic synthetic fun <init> (IJLkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2-UwyO8pc ()J\n\tpublic final fun component3 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy-8Mi8wO0 (IJLkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/system/ReadinessStrategy$Probe;\n\tpublic static synthetic fun copy-8Mi8wO0$default (Lcom/trendyol/stove/system/ReadinessStrategy$Probe;IJLkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/system/ReadinessStrategy$Probe;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getCheck ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getRetries ()I\n\tpublic final fun getRetryDelay-UwyO8pc ()J\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/system/ReadinessStrategy$TcpPort : com/trendyol/stove/system/ReadinessStrategy {\n\tpublic synthetic fun <init> (IIJILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic synthetic fun <init> (IIJLkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()I\n\tpublic final fun component3-UwyO8pc ()J\n\tpublic final fun copy-SxA4cEA (IIJ)Lcom/trendyol/stove/system/ReadinessStrategy$TcpPort;\n\tpublic static synthetic fun copy-SxA4cEA$default (Lcom/trendyol/stove/system/ReadinessStrategy$TcpPort;IIJILjava/lang/Object;)Lcom/trendyol/stove/system/ReadinessStrategy$TcpPort;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getPort ()I\n\tpublic final fun getRetries ()I\n\tpublic final fun getRetryDelay-UwyO8pc ()J\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/system/ReportingDsl {\n\tpublic fun <init> (Lcom/trendyol/stove/system/StoveOptionsDsl;)V\n\tpublic final fun disabled ()Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic final fun dumpOnFailure (Z)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic static synthetic fun dumpOnFailure$default (Lcom/trendyol/stove/system/ReportingDsl;ZILjava/lang/Object;)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic final fun enabled (Z)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic static synthetic fun enabled$default (Lcom/trendyol/stove/system/ReportingDsl;ZILjava/lang/Object;)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic final fun failureRenderer (Lcom/trendyol/stove/reporting/ReportRenderer;)Lcom/trendyol/stove/system/StoveOptionsDsl;\n}\n\npublic final class com/trendyol/stove/system/Stove : com/trendyol/stove/system/abstractions/ReadyStove, java/lang/AutoCloseable {\n\tpublic static final field Companion Lcom/trendyol/stove/system/Stove$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun addReportListener (Lcom/trendyol/stove/reporting/ReportEventListener;)V\n\tpublic final fun allRegisteredSystems ()Ljava/util/Collection;\n\tpublic final fun allSystems ()Ljava/util/Collection;\n\tpublic final fun applicationUnderTest (Lcom/trendyol/stove/system/abstractions/ApplicationUnderTest;)Lcom/trendyol/stove/system/Stove;\n\tpublic final fun applicationUnderTestContext ()Ljava/lang/Object;\n\tpublic fun close ()V\n\tpublic final fun endTest ()V\n\tpublic final fun getActiveSystems ()Ljava/util/Map;\n\tpublic final fun getKeepDependenciesRunning ()Z\n\tpublic final fun getKeyedSystems ()Ljava/util/Map;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/system/StoveOptions;\n\tpublic final fun getRunMigrationsAlways ()Z\n\tpublic final fun recordReport (Lcom/trendyol/stove/reporting/ReportEntry;)V\n\tpublic final fun registerForDispose (Ljava/lang/AutoCloseable;)Ljava/lang/AutoCloseable;\n\tpublic final fun removeReportListener (Lcom/trendyol/stove/reporting/ReportEventListener;)V\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun startTest (Lcom/trendyol/stove/reporting/StoveTestContext;)V\n\tpublic final fun with (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/system/Stove$Companion {\n\tpublic final fun getSystem (Lkotlin/reflect/KClass;)Lcom/trendyol/stove/system/abstractions/PluggedSystem;\n\tpublic final fun getSystemOrNone (Lkotlin/reflect/KClass;)Larrow/core/Option;\n\tpublic final fun instanceInitialized ()Z\n\tpublic final fun options ()Lcom/trendyol/stove/system/StoveOptions;\n\tpublic final fun reporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic final fun stop ()V\n}\n\npublic final class com/trendyol/stove/system/StoveKt {\n\tpublic static final fun stove (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/system/StoveOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (ZLcom/trendyol/stove/system/abstractions/StateStorageFactory;ZZZZLcom/trendyol/stove/reporting/ReportRenderer;Lcom/trendyol/stove/reporting/ReportRenderer;Lcom/trendyol/stove/reporting/ReportRenderer;ZZLjava/lang/String;)V\n\tpublic synthetic fun <init> (ZLcom/trendyol/stove/system/abstractions/StateStorageFactory;ZZZZLcom/trendyol/stove/reporting/ReportRenderer;Lcom/trendyol/stove/reporting/ReportRenderer;Lcom/trendyol/stove/reporting/ReportRenderer;ZZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Z\n\tpublic final fun component10 ()Z\n\tpublic final fun component11 ()Z\n\tpublic final fun component12 ()Ljava/lang/String;\n\tpublic final fun component2 ()Lcom/trendyol/stove/system/abstractions/StateStorageFactory;\n\tpublic final fun component3 ()Z\n\tpublic final fun component4 ()Z\n\tpublic final fun component5 ()Z\n\tpublic final fun component6 ()Z\n\tpublic final fun component7 ()Lcom/trendyol/stove/reporting/ReportRenderer;\n\tpublic final fun component8 ()Lcom/trendyol/stove/reporting/ReportRenderer;\n\tpublic final fun component9 ()Lcom/trendyol/stove/reporting/ReportRenderer;\n\tpublic final fun copy (ZLcom/trendyol/stove/system/abstractions/StateStorageFactory;ZZZZLcom/trendyol/stove/reporting/ReportRenderer;Lcom/trendyol/stove/reporting/ReportRenderer;Lcom/trendyol/stove/reporting/ReportRenderer;ZZLjava/lang/String;)Lcom/trendyol/stove/system/StoveOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/system/StoveOptions;ZLcom/trendyol/stove/system/abstractions/StateStorageFactory;ZZZZLcom/trendyol/stove/reporting/ReportRenderer;Lcom/trendyol/stove/reporting/ReportRenderer;Lcom/trendyol/stove/reporting/ReportRenderer;ZZLjava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/system/StoveOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getDefaultRenderer ()Lcom/trendyol/stove/reporting/ReportRenderer;\n\tpublic final fun getDumpReportOnStop ()Z\n\tpublic final fun getDumpReportOnTestFailure ()Z\n\tpublic final fun getFailureRenderer ()Lcom/trendyol/stove/reporting/ReportRenderer;\n\tpublic final fun getFileRenderer ()Lcom/trendyol/stove/reporting/ReportRenderer;\n\tpublic final fun getKeepDependenciesRunning ()Z\n\tpublic final fun getReportFilePath ()Ljava/lang/String;\n\tpublic final fun getReportToConsole ()Z\n\tpublic final fun getReportToFile ()Z\n\tpublic final fun getReportingEnabled ()Z\n\tpublic final fun getRunMigrationsAlways ()Z\n\tpublic final fun getStateStorageFactory ()Lcom/trendyol/stove/system/abstractions/StateStorageFactory;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/system/StoveOptionsDsl {\n\tpublic static final field Companion Lcom/trendyol/stove/system/StoveOptionsDsl$Companion;\n\tpublic fun <init> ()V\n\tpublic final fun dumpReportOnTestFailure (Z)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic static synthetic fun dumpReportOnTestFailure$default (Lcom/trendyol/stove/system/StoveOptionsDsl;ZILjava/lang/Object;)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic final fun enableReuseForTestContainers ()V\n\tpublic final fun failureRenderer (Lcom/trendyol/stove/reporting/ReportRenderer;)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic final fun isRunningLocally ()Z\n\tpublic final fun keepDependenciesRunning ()Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic final fun reporting (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic final fun reportingEnabled (Z)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic static synthetic fun reportingEnabled$default (Lcom/trendyol/stove/system/StoveOptionsDsl;ZILjava/lang/Object;)Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic final fun runMigrationsAlways ()Lcom/trendyol/stove/system/StoveOptionsDsl;\n\tpublic final fun stateStorage (Lcom/trendyol/stove/system/abstractions/StateStorageFactory;)Lcom/trendyol/stove/system/StoveOptionsDsl;\n}\n\npublic final class com/trendyol/stove/system/StoveOptionsDsl$Companion {\n}\n\npublic final class com/trendyol/stove/system/ValidationDsl {\n\tpublic static final synthetic fun box-impl (Lcom/trendyol/stove/system/Stove;)Lcom/trendyol/stove/system/ValidationDsl;\n\tpublic static fun constructor-impl (Lcom/trendyol/stove/system/Stove;)Lcom/trendyol/stove/system/Stove;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic static fun equals-impl (Lcom/trendyol/stove/system/Stove;Ljava/lang/Object;)Z\n\tpublic static final fun equals-impl0 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/Stove;)Z\n\tpublic final fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic fun hashCode ()I\n\tpublic static fun hashCode-impl (Lcom/trendyol/stove/system/Stove;)I\n\tpublic fun toString ()Ljava/lang/String;\n\tpublic static fun toString-impl (Lcom/trendyol/stove/system/Stove;)Ljava/lang/String;\n\tpublic final synthetic fun unbox-impl ()Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/system/WithDsl {\n\tpublic static final fun applicationUnderTest-impl (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/ApplicationUnderTest;)V\n\tpublic static final synthetic fun box-impl (Lcom/trendyol/stove/system/Stove;)Lcom/trendyol/stove/system/WithDsl;\n\tpublic static fun constructor-impl (Lcom/trendyol/stove/system/Stove;)Lcom/trendyol/stove/system/Stove;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic static fun equals-impl (Lcom/trendyol/stove/system/Stove;Ljava/lang/Object;)Z\n\tpublic static final fun equals-impl0 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/Stove;)Z\n\tpublic final fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic fun hashCode ()I\n\tpublic static fun hashCode-impl (Lcom/trendyol/stove/system/Stove;)I\n\tpublic fun toString ()Ljava/lang/String;\n\tpublic static fun toString-impl (Lcom/trendyol/stove/system/Stove;)Ljava/lang/String;\n\tpublic final synthetic fun unbox-impl ()Lcom/trendyol/stove/system/Stove;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/AfterRunAware {\n\tpublic abstract fun afterRun (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/AfterRunAwareWithContext {\n\tpublic abstract fun afterRun (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/ApplicationUnderTest {\n\tpublic abstract fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/BeforeRunAware {\n\tpublic abstract fun beforeRun (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration {\n\tpublic abstract fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/ExposedConfiguration {\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/ExposesConfiguration {\n\tpublic abstract fun configuration ()Ljava/util/List;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/PluggedSystem : com/trendyol/stove/system/abstractions/ThenSystemContinuation, java/lang/AutoCloseable {\n}\n\npublic final class com/trendyol/stove/system/abstractions/PluggedSystem$DefaultImpls {\n\tpublic static fun executeWithReuseCheck (Lcom/trendyol/stove/system/abstractions/PluggedSystem;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static fun then (Lcom/trendyol/stove/system/abstractions/PluggedSystem;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/system/abstractions/ProvidedRuntime : com/trendyol/stove/system/abstractions/SystemRuntime {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/system/abstractions/ProvidedRuntime;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic abstract fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic abstract fun getRunMigrationsForProvided ()Z\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/ReadyStove {\n\tpublic abstract fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/RunAware {\n\tpublic abstract fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/RunnableSystemWithContext : com/trendyol/stove/system/abstractions/AfterRunAwareWithContext, com/trendyol/stove/system/abstractions/BeforeRunAware, com/trendyol/stove/system/abstractions/RunAware, java/lang/AutoCloseable {\n\tpublic fun close ()V\n}\n\npublic final class com/trendyol/stove/system/abstractions/RunnableSystemWithContext$DefaultImpls {\n\tpublic static fun close (Lcom/trendyol/stove/system/abstractions/RunnableSystemWithContext;)V\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/StateStorage {\n\tpublic abstract fun capture (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun isSubsequentRun ()Z\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/StateStorageFactory {\n\tpublic static final field Companion Lcom/trendyol/stove/system/abstractions/StateStorageFactory$Companion;\n\tpublic fun DefaultStateStorage (Lcom/trendyol/stove/system/StoveOptions;Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lcom/trendyol/stove/system/abstractions/StateStorage;\n\tpublic fun createWithKey (Lcom/trendyol/stove/system/StoveOptions;Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;Ljava/lang/String;)Lcom/trendyol/stove/system/abstractions/StateStorage;\n\tpublic abstract fun invoke (Lcom/trendyol/stove/system/StoveOptions;Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lcom/trendyol/stove/system/abstractions/StateStorage;\n}\n\npublic final class com/trendyol/stove/system/abstractions/StateStorageFactory$Companion {\n\tpublic final fun Default ()Lcom/trendyol/stove/system/abstractions/StateStorageFactory;\n}\n\npublic final class com/trendyol/stove/system/abstractions/StateStorageFactory$DefaultImpls {\n\tpublic static fun DefaultStateStorage (Lcom/trendyol/stove/system/abstractions/StateStorageFactory;Lcom/trendyol/stove/system/StoveOptions;Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lcom/trendyol/stove/system/abstractions/StateStorage;\n\tpublic static fun createWithKey (Lcom/trendyol/stove/system/abstractions/StateStorageFactory;Lcom/trendyol/stove/system/StoveOptions;Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;Ljava/lang/String;)Lcom/trendyol/stove/system/abstractions/StateStorage;\n}\n\npublic final class com/trendyol/stove/system/abstractions/StateWithProcess {\n\tpublic fun <init> (Ljava/lang/Object;J)V\n\tpublic final fun component1 ()Ljava/lang/Object;\n\tpublic final fun component2 ()J\n\tpublic final fun copy (Ljava/lang/Object;J)Lcom/trendyol/stove/system/abstractions/StateWithProcess;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/system/abstractions/StateWithProcess;Ljava/lang/Object;JILjava/lang/Object;)Lcom/trendyol/stove/system/abstractions/StateWithProcess;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getProcessId ()J\n\tpublic final fun getState ()Ljava/lang/Object;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/system/abstractions/SystemConfigurationException : java/lang/Throwable {\n\tpublic fun <init> (Lkotlin/reflect/KClass;Ljava/lang/String;)V\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/SystemKey {\n}\n\npublic final class com/trendyol/stove/system/abstractions/SystemKeyKt {\n\tpublic static final fun keyDisplayName (Lcom/trendyol/stove/system/abstractions/SystemKey;)Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/system/abstractions/SystemNotInitializedException : java/lang/Throwable {\n\tpublic fun <init> (Lkotlin/reflect/KClass;)V\n}\n\npublic final class com/trendyol/stove/system/abstractions/SystemNotRegisteredException : java/lang/Throwable {\n\tpublic fun <init> (Lkotlin/reflect/KClass;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lkotlin/reflect/KClass;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/SystemOptions {\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/SystemRuntime {\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/ThenSystemContinuation {\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/system/abstractions/ThenSystemContinuation$DefaultImpls {\n\tpublic static fun executeWithReuseCheck (Lcom/trendyol/stove/system/abstractions/ThenSystemContinuation;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static fun then (Lcom/trendyol/stove/system/abstractions/ThenSystemContinuation;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic abstract interface class com/trendyol/stove/system/abstractions/ValidatedSystem {\n\tpublic abstract fun validate (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract interface annotation class com/trendyol/stove/system/annotations/StoveDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/system/application/ApplicationConfigurationsKt {\n\tpublic static final fun toConfigurationMap (Ljava/util/List;)Ljava/util/Map;\n}\n\npublic final class com/trendyol/stove/system/application/ArgsMapperBuilder {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun arg (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun arg (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V\n\tpublic static synthetic fun arg$default (Lcom/trendyol/stove/system/application/ArgsMapperBuilder;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V\n\tpublic final fun build ()Lcom/trendyol/stove/system/application/ArgsProvider;\n\tpublic final fun map (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun to (Ljava/lang/String;Ljava/lang/String;)V\n}\n\npublic abstract interface class com/trendyol/stove/system/application/ArgsProvider {\n\tpublic static final field Companion Lcom/trendyol/stove/system/application/ArgsProvider$Companion;\n\tpublic abstract fun provide (Ljava/util/Map;)Ljava/util/List;\n}\n\npublic final class com/trendyol/stove/system/application/ArgsProvider$Companion {\n\tpublic final fun empty ()Lcom/trendyol/stove/system/application/ArgsProvider;\n}\n\npublic final class com/trendyol/stove/system/application/ArgsProviderKt {\n\tpublic static final fun argsMapper (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/system/application/ArgsProvider;\n\tpublic static synthetic fun argsMapper$default (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/system/application/ArgsProvider;\n}\n\npublic final class com/trendyol/stove/system/application/EnvMapperBuilder {\n\tpublic fun <init> ()V\n\tpublic final fun build ()Lcom/trendyol/stove/system/application/EnvProvider;\n\tpublic final fun env (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun env (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V\n\tpublic final fun map (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun to (Ljava/lang/String;Ljava/lang/String;)V\n}\n\npublic abstract interface class com/trendyol/stove/system/application/EnvProvider {\n\tpublic static final field Companion Lcom/trendyol/stove/system/application/EnvProvider$Companion;\n\tpublic abstract fun provide (Ljava/util/Map;)Ljava/util/Map;\n}\n\npublic final class com/trendyol/stove/system/application/EnvProvider$Companion {\n\tpublic final fun empty ()Lcom/trendyol/stove/system/application/EnvProvider;\n}\n\npublic final class com/trendyol/stove/system/application/EnvProviderKt {\n\tpublic static final fun envMapper (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/system/application/EnvProvider;\n}\n\npublic final class com/trendyol/stove/tracing/ExceptionInfo {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/util/List;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/trendyol/stove/tracing/ExceptionInfo;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/tracing/ExceptionInfo;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/tracing/ExceptionInfo;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getMessage ()Ljava/lang/String;\n\tpublic final fun getStackTrace ()Ljava/util/List;\n\tpublic final fun getType ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/tracing/SpanInfo {\n\tpublic static final field Companion Lcom/trendyol/stove/tracing/SpanInfo$Companion;\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JJLcom/trendyol/stove/tracing/SpanStatus;Ljava/util/Map;Lcom/trendyol/stove/tracing/ExceptionInfo;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JJLcom/trendyol/stove/tracing/SpanStatus;Ljava/util/Map;Lcom/trendyol/stove/tracing/ExceptionInfo;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component10 ()Lcom/trendyol/stove/tracing/ExceptionInfo;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Ljava/lang/String;\n\tpublic final fun component6 ()J\n\tpublic final fun component7 ()J\n\tpublic final fun component8 ()Lcom/trendyol/stove/tracing/SpanStatus;\n\tpublic final fun component9 ()Ljava/util/Map;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JJLcom/trendyol/stove/tracing/SpanStatus;Ljava/util/Map;Lcom/trendyol/stove/tracing/ExceptionInfo;)Lcom/trendyol/stove/tracing/SpanInfo;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/tracing/SpanInfo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JJLcom/trendyol/stove/tracing/SpanStatus;Ljava/util/Map;Lcom/trendyol/stove/tracing/ExceptionInfo;ILjava/lang/Object;)Lcom/trendyol/stove/tracing/SpanInfo;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAttributes ()Ljava/util/Map;\n\tpublic final fun getDurationMs ()J\n\tpublic final fun getDurationNanos ()J\n\tpublic final fun getEndTimeNanos ()J\n\tpublic final fun getException ()Lcom/trendyol/stove/tracing/ExceptionInfo;\n\tpublic final fun getOperationName ()Ljava/lang/String;\n\tpublic final fun getParentSpanId ()Ljava/lang/String;\n\tpublic final fun getServiceName ()Ljava/lang/String;\n\tpublic final fun getSpanId ()Ljava/lang/String;\n\tpublic final fun getStartTimeNanos ()J\n\tpublic final fun getStatus ()Lcom/trendyol/stove/tracing/SpanStatus;\n\tpublic final fun getTraceId ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic final fun isFailed ()Z\n\tpublic final fun isSuccess ()Z\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/tracing/SpanInfo$Companion {\n}\n\npublic final class com/trendyol/stove/tracing/SpanNode {\n\tpublic fun <init> (Lcom/trendyol/stove/tracing/SpanInfo;Ljava/util/List;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/tracing/SpanInfo;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/tracing/SpanInfo;\n\tpublic final fun component2 ()Ljava/util/List;\n\tpublic final fun copy (Lcom/trendyol/stove/tracing/SpanInfo;Ljava/util/List;)Lcom/trendyol/stove/tracing/SpanNode;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/tracing/SpanNode;Lcom/trendyol/stove/tracing/SpanInfo;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/tracing/SpanNode;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun findFailurePoint ()Lcom/trendyol/stove/tracing/SpanNode;\n\tpublic final fun flatten ()Ljava/util/List;\n\tpublic final fun getChildren ()Ljava/util/List;\n\tpublic final fun getDepth ()I\n\tpublic final fun getHasFailedDescendants ()Z\n\tpublic final fun getSpan ()Lcom/trendyol/stove/tracing/SpanInfo;\n\tpublic final fun getSpanCount ()I\n\tpublic final fun getTotalDurationMs ()J\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/tracing/SpanStatus : java/lang/Enum {\n\tpublic static final field ERROR Lcom/trendyol/stove/tracing/SpanStatus;\n\tpublic static final field OK Lcom/trendyol/stove/tracing/SpanStatus;\n\tpublic static final field UNSET Lcom/trendyol/stove/tracing/SpanStatus;\n\tpublic static fun getEntries ()Lkotlin/enums/EnumEntries;\n\tpublic static fun valueOf (Ljava/lang/String;)Lcom/trendyol/stove/tracing/SpanStatus;\n\tpublic static fun values ()[Lcom/trendyol/stove/tracing/SpanStatus;\n}\n\npublic final class com/trendyol/stove/tracing/SpanTree {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/tracing/SpanTree;\n\tpublic final fun build (Ljava/util/List;)Lcom/trendyol/stove/tracing/SpanNode;\n\tpublic final fun filterSpans (Lcom/trendyol/stove/tracing/SpanNode;Lkotlin/jvm/functions/Function1;)Ljava/util/List;\n\tpublic final fun findSpan (Lcom/trendyol/stove/tracing/SpanNode;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/tracing/SpanNode;\n}\n\npublic final class com/trendyol/stove/tracing/TraceContext {\n\tpublic static final field BAGGAGE_TEST_ID_KEY Ljava/lang/String;\n\tpublic static final field Companion Lcom/trendyol/stove/tracing/TraceContext$Companion;\n\tpublic static final field STOVE_TEST_ID_HEADER Ljava/lang/String;\n\tpublic static final field TRACEPARENT_HEADER Ljava/lang/String;\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/tracing/TraceContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/tracing/TraceContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getRootSpanId ()Ljava/lang/String;\n\tpublic final fun getTestId ()Ljava/lang/String;\n\tpublic final fun getTraceId ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n\tpublic final fun toTraceparent ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/tracing/TraceContext$Companion {\n\tpublic final fun clear ()V\n\tpublic final fun current ()Lcom/trendyol/stove/tracing/TraceContext;\n\tpublic final fun generateSpanId ()Ljava/lang/String;\n\tpublic final fun generateTraceId ()Ljava/lang/String;\n\tpublic final fun parseTraceparent (Ljava/lang/String;)Lkotlin/Pair;\n\tpublic final fun sanitizeToAscii (Ljava/lang/String;)Ljava/lang/String;\n\tpublic final fun start (Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceContext;\n\tpublic final fun use (Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;\n\tpublic final fun withCurrentPropagation (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun withPropagation (Lcom/trendyol/stove/tracing/TraceContext;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/tracing/TraceTreeRenderer {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/tracing/TraceTreeRenderer;\n\tpublic final fun render (Lcom/trendyol/stove/tracing/SpanNode;ZLjava/util/List;)Ljava/lang/String;\n\tpublic static synthetic fun render$default (Lcom/trendyol/stove/tracing/TraceTreeRenderer;Lcom/trendyol/stove/tracing/SpanNode;ZLjava/util/List;ILjava/lang/Object;)Ljava/lang/String;\n\tpublic final fun renderColored (Lcom/trendyol/stove/tracing/SpanNode;ZLjava/util/List;)Ljava/lang/String;\n\tpublic static synthetic fun renderColored$default (Lcom/trendyol/stove/tracing/TraceTreeRenderer;Lcom/trendyol/stove/tracing/SpanNode;ZLjava/util/List;ILjava/lang/Object;)Ljava/lang/String;\n\tpublic final fun renderCompact (Lcom/trendyol/stove/tracing/SpanNode;)Ljava/lang/String;\n\tpublic final fun renderSummary (Lcom/trendyol/stove/tracing/SpanNode;)Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/tracing/TraceVisualization {\n\tpublic static final field Companion Lcom/trendyol/stove/tracing/TraceVisualization$Companion;\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;IILjava/util/List;Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()I\n\tpublic final fun component4 ()I\n\tpublic final fun component5 ()Ljava/util/List;\n\tpublic final fun component6 ()Ljava/lang/String;\n\tpublic final fun component7 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;IILjava/util/List;Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceVisualization;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/tracing/TraceVisualization;Ljava/lang/String;Ljava/lang/String;IILjava/util/List;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/tracing/TraceVisualization;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getColoredTree ()Ljava/lang/String;\n\tpublic final fun getFailedSpans ()I\n\tpublic final fun getSpans ()Ljava/util/List;\n\tpublic final fun getTestId ()Ljava/lang/String;\n\tpublic final fun getTotalSpans ()I\n\tpublic final fun getTraceId ()Ljava/lang/String;\n\tpublic final fun getTree ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/tracing/TraceVisualization$Companion {\n\tpublic final fun from (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lcom/trendyol/stove/tracing/TraceVisualization;\n}\n\npublic final class com/trendyol/stove/tracing/VisualSpan {\n\tpublic static final field Companion Lcom/trendyol/stove/tracing/VisualSpan$Companion;\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;DLjava/lang/String;Ljava/util/Map;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()D\n\tpublic final fun component6 ()Ljava/lang/String;\n\tpublic final fun component7 ()Ljava/util/Map;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;DLjava/lang/String;Ljava/util/Map;)Lcom/trendyol/stove/tracing/VisualSpan;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/tracing/VisualSpan;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;DLjava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lcom/trendyol/stove/tracing/VisualSpan;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAttributes ()Ljava/util/Map;\n\tpublic final fun getDurationMs ()D\n\tpublic final fun getOperationName ()Ljava/lang/String;\n\tpublic final fun getParentSpanId ()Ljava/lang/String;\n\tpublic final fun getServiceName ()Ljava/lang/String;\n\tpublic final fun getSpanId ()Ljava/lang/String;\n\tpublic final fun getStatus ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/tracing/VisualSpan$Companion {\n\tpublic final fun from (Lcom/trendyol/stove/tracing/SpanInfo;)Lcom/trendyol/stove/tracing/VisualSpan;\n}\n\n"
  },
  {
    "path": "lib/stove/build.gradle.kts",
    "content": "plugins {\n  `java-test-fixtures`\n  alias(libs.plugins.kotlinx.serialization)\n}\n\ndependencies {\n  api(libs.arrow.core)\n  api(libs.kotlinx.core)\n  api(libs.jackson.kotlin)\n  api(libs.jackson.databind)\n  api(libs.google.gson)\n  api(libs.kotlinx.serialization.json)\n  api(libs.testcontainers) {\n    version {\n      require(libs.testcontainers.asProvider().get().version!!)\n    }\n  }\n  implementation(libs.mordant)\n  // OTel API for setting trace context and baggage so the Java Agent\n  // creates child spans with Stove's trace ID and propagates test metadata.\n  // No-op when agent is not present.\n  implementation(libs.opentelemetry.api)\n}\n\ndependencies {\n  testImplementation(libs.kotest.arrow)\n  testImplementation(libs.kotest.runner.junit5)\n  testImplementation(libs.kotest.framework.engine)\n  testImplementation(libs.kotest.assertions.core)\n  testFixturesImplementation(libs.kotest.runner.junit5)\n}\n\nval javaComponent = components[\"java\"] as AdhocComponentWithVariants\njavaComponent.withVariantsFromConfiguration(configurations[\"testFixturesApiElements\"]) { skip() }\njavaComponent.withVariantsFromConfiguration(configurations[\"testFixturesRuntimeElements\"]) { skip() }\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/containers/ContainerOptions.kt",
    "content": "package com.trendyol.stove.containers\n\nimport org.testcontainers.utility.DockerImageName\n\ntypealias ContainerFn<TIn> = TIn.() -> Unit\n\ntypealias UseContainerFn<TContainer> = (DockerImageName) -> TContainer\n\n/**\n * Container options to run\n */\ninterface ContainerOptions<TContainer : StoveContainer> {\n  val registry: String\n\n  val image: String\n\n  val tag: String\n\n  val imageWithTag: String get() = \"$image:$tag\"\n\n  val compatibleSubstitute: String?\n\n  val useContainerFn: UseContainerFn<TContainer>\n\n  val containerFn: ContainerFn<TContainer>\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/containers/ProvidedRegistry.kt",
    "content": "package com.trendyol.stove.containers\n\nimport org.testcontainers.utility.DockerImageName\n\n/**\n * Can be set globally\n */\n@Suppress(\"ktlint:standard:property-naming\")\nvar DEFAULT_REGISTRY = \"docker.io\"\n\n/**\n * Allows a docker image to be sourced from a different registry. [DEFAULT_REGISTRY]\n * Example:\n * ```kotlin\n *  withProvidedRegistry(\"couchbase/server\", registry) {\n *             CouchbaseContainer(it).withBucket(bucketDefinition)\n *         }\n * ```\n */\nfun <T> withProvidedRegistry(\n  imageName: String,\n  registry: String = DEFAULT_REGISTRY,\n  compatibleSubstitute: String? = null,\n  containerBuilder: (DockerImageName) -> T\n): T {\n  val trimmedRegistry = registry.trim('/')\n  val trimmedImage = imageName.trim('/')\n\n  // Skip prepending the registry when the image already contains a registry\n  // (e.g. \"mcr.microsoft.com/mssql/server\") or when the registry is blank.\n  val fullImage = if (trimmedRegistry.isBlank() || containsRegistry(trimmedImage)) {\n    trimmedImage\n  } else {\n    \"$trimmedRegistry/$trimmedImage\"\n  }\n\n  return containerBuilder(\n    DockerImageName\n      .parse(fullImage)\n      .asCompatibleSubstituteFor(compatibleSubstitute ?: imageName)\n  )\n}\n\n/**\n * Heuristic: an image name contains a registry if the part before the first `/`\n * includes a dot (e.g. `mcr.microsoft.com`, `ghcr.io`, `registry.example.com`)\n * or a colon for port (e.g. `localhost:5000`).\n */\nprivate fun containsRegistry(imageName: String): Boolean {\n  val firstSegment = imageName.substringBefore('/')\n  return firstSegment != imageName && (firstSegment.contains('.') || firstSegment.contains(':'))\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/containers/StoveContainer.kt",
    "content": "package com.trendyol.stove.containers\n\nimport arrow.core.*\nimport com.github.dockerjava.api.DockerClient\nimport com.github.dockerjava.api.async.ResultCallback\nimport com.github.dockerjava.api.model.*\nimport com.trendyol.stove.system.abstractions.SystemRuntime\nimport org.testcontainers.DockerClientFactory\nimport org.testcontainers.utility.DockerImageName\nimport java.io.ByteArrayOutputStream\nimport java.util.concurrent.*\n\n/**\n * Interface for Stove-managed Docker containers with extended functionality.\n *\n * This interface wraps Testcontainers and provides additional capabilities like:\n * - Pausing/unpausing containers for fault injection tests\n * - Executing commands inside running containers\n * - Inspecting container state\n *\n * ## Implemented By\n *\n * All Stove database and infrastructure containers implement this interface:\n * - PostgreSQL, MongoDB, Couchbase, Elasticsearch, MSSQL, Redis containers\n * - Kafka containers\n *\n * ## Pause/Unpause for Fault Injection\n *\n * Simulate network partitions or service unavailability:\n *\n * ```kotlin\n * stove {\n *     // Pause the database to simulate outage\n *     postgresql {\n *         pause()\n *     }\n *\n *     // Test application behavior during outage\n *     http {\n *         getResponse(\"/health\") { response ->\n *             response.status shouldBe 503\n *         }\n *     }\n *\n *     // Restore the database\n *     postgresql {\n *         unpause()\n *     }\n *\n *     // Verify recovery\n *     http {\n *         getResponse(\"/health\") { response ->\n *             response.status shouldBe 200\n *         }\n *     }\n * }\n * ```\n *\n * ## Execute Commands Inside Container\n *\n * Run commands inside the container for debugging or setup:\n *\n * ```kotlin\n * postgresql {\n *     val result = execCommand(\"psql\", \"-U\", \"test\", \"-c\", \"SELECT 1\")\n *     result.exitCode shouldBe 0\n *     result.stdout shouldContain \"1\"\n * }\n * ```\n *\n * ## Inspect Container State\n *\n * Check container health and status:\n *\n * ```kotlin\n * couchbase {\n *     val info = inspect()\n *     info.running shouldBe true\n *     info.paused shouldBe false\n * }\n * ```\n *\n * @see SystemRuntime\n * @see ExecResult\n * @see StoveContainerInspectInformation\n */\ninterface StoveContainer : SystemRuntime {\n  val imageNameAccess: DockerImageName\n\n  val containerIdAccess: String\n    get() = dockerClientAccess.value\n      .listContainersCmd()\n      .exec()\n      .firstOrNone { it.image == imageNameAccess.asCanonicalNameString() }\n      .getOrElse { error(\"Container with image ${imageNameAccess.asCanonicalNameString()} not found\") }\n      .id\n\n  val dockerClientAccess: Lazy<DockerClient>\n    get() = lazy { DockerClientFactory.lazyClient() }\n\n  /**\n   * Pauses the container. This method is idempotent - if the container is already paused, it does nothing.\n   */\n  fun pause() {\n    if (!inspect().paused) {\n      dockerClientAccess.value.pauseContainerCmd(containerIdAccess).exec()\n    }\n  }\n\n  /**\n   * Unpauses the container. This method is idempotent - if the container is not paused, it does nothing.\n   */\n  fun unpause() {\n    if (inspect().paused) {\n      dockerClientAccess.value.unpauseContainerCmd(containerIdAccess).exec()\n    }\n  }\n\n  /**\n   * Executes a command inside the running container using Docker client directly.\n   * This method works even when the testcontainer instance wasn't started (e.g., on subsequent runs with reuse).\n   *\n   * @param command The command and its arguments to execute\n   * @param timeoutSeconds Maximum time to wait for command completion (default: 60 seconds)\n   * @return [ExecResult] containing exit code, stdout, and stderr\n   */\n  fun execCommand(\n    vararg command: String,\n    timeoutSeconds: Long = 60\n  ): ExecResult {\n    val docker = dockerClientAccess.value\n    val containerId = containerIdAccess\n\n    // Create exec instance\n    val execCreate = docker\n      .execCreateCmd(containerId)\n      .withAttachStdout(true)\n      .withAttachStderr(true)\n      .withCmd(*command)\n      .exec()\n\n    val stdout = ByteArrayOutputStream()\n    val stderr = ByteArrayOutputStream()\n    val latch = CountDownLatch(1)\n\n    // Start exec and capture output\n    docker\n      .execStartCmd(execCreate.id)\n      .exec(object : ResultCallback.Adapter<Frame>() {\n        override fun onNext(frame: Frame) {\n          when (frame.streamType) {\n            StreamType.STDOUT -> {\n              stdout.write(frame.payload)\n            }\n\n            StreamType.STDERR -> {\n              stderr.write(frame.payload)\n            }\n\n            else -> {} // Ignore other stream types\n          }\n        }\n\n        override fun onComplete() {\n          latch.countDown()\n        }\n\n        override fun onError(throwable: Throwable) {\n          latch.countDown()\n        }\n      })\n\n    if (!latch.await(timeoutSeconds, TimeUnit.SECONDS)) {\n      return ExecResult(\n        exitCode = -1,\n        stdout = stdout.toString(Charsets.UTF_8),\n        stderr = \"Command timed out after $timeoutSeconds seconds\"\n      )\n    }\n\n    val execInspect = docker.inspectExecCmd(execCreate.id).exec()\n    val exitCode = execInspect.exitCodeLong?.toInt() ?: -1\n\n    return ExecResult(\n      exitCode = exitCode,\n      stdout = stdout.toString(Charsets.UTF_8),\n      stderr = stderr.toString(Charsets.UTF_8)\n    )\n  }\n\n  fun inspect(): StoveContainerInspectInformation = dockerClientAccess.value\n    .inspectContainerCmd(containerIdAccess)\n    .exec()\n    .let {\n      StoveContainerInspectInformation(\n        id = it.id,\n        labels = it.config.labels ?: emptyMap(),\n        name = it.name,\n        state = it.state.toString(),\n        running = it.state.running ?: false,\n        paused = it.state.paused ?: false,\n        restarting = it.state.restarting ?: false,\n        startedAt = it.state.startedAt.toString(),\n        finishedAt = it.state.finishedAt.toString(),\n        exitCode = it.state.exitCodeLong ?: 0,\n        error = it.state.error.toString()\n      )\n    }\n}\n\n/**\n * Result of executing a command in a container.\n */\ndata class ExecResult(\n  val exitCode: Int,\n  val stdout: String,\n  val stderr: String\n)\n\ndata class StoveContainerInspectInformation(\n  val id: String,\n  val labels: Map<String, String>,\n  val name: String,\n  val state: String,\n  val running: Boolean,\n  val paused: Boolean,\n  val restarting: Boolean,\n  val startedAt: String,\n  val finishedAt: String,\n  val exitCode: Long,\n  val error: String\n)\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/database/migrations/DatabaseMigration.kt",
    "content": "package com.trendyol.stove.database.migrations\n\nimport com.trendyol.stove.system.abstractions.AfterRunAware\n\n/**\n * Interface for database schema migrations and test data setup.\n *\n * Migrations run after the database container starts and before tests execute.\n * Use migrations for:\n * - Creating database schemas and tables\n * - Setting up indexes\n * - Seeding reference data\n * - Any setup that requires a running database\n *\n * ## Module-Specific Type Aliases\n *\n * Each Stove module provides a convenience type alias so you don't need to\n * remember the generic `DatabaseMigration<XyzContext>` form:\n *\n * | Module          | Type Alias                | Resolves To                                    |\n * |-----------------|---------------------------|------------------------------------------------|\n * | stove-postgres  | `PostgresqlMigration`     | `DatabaseMigration<PostgresSqlMigrationContext>`|\n * | stove-mysql     | `MySqlMigration`          | `DatabaseMigration<MySqlMigrationContext>`      |\n * | stove-mssql     | `MsSqlMigration`          | `DatabaseMigration<SqlMigrationContext>`        |\n * | stove-mongodb   | `MongodbMigration`        | `DatabaseMigration<MongodbMigrationContext>`    |\n * | stove-couchbase | `CouchbaseMigration`      | `DatabaseMigration<Cluster>`                   |\n * | stove-elasticsearch | `ElasticsearchMigration` | `DatabaseMigration<ElasticsearchClient>`    |\n * | stove-redis     | `RedisMigration`          | `DatabaseMigration<RedisMigrationContext>`      |\n * | stove-kafka     | `KafkaMigration`          | `DatabaseMigration<KafkaMigrationContext>`      |\n *\n * ## Creating a Migration\n *\n * ```kotlin\n * // Using the module-specific type alias (recommended):\n * class CreateUsersTableMigration : PostgresqlMigration {\n *     override val order: Int = MigrationPriority.HIGHEST.value\n *\n *     override suspend fun execute(connection: PostgresSqlMigrationContext) {\n *         connection.operations.execute(\"\"\"\n *             CREATE TABLE IF NOT EXISTS users (\n *                 id SERIAL PRIMARY KEY,\n *                 name VARCHAR(255) NOT NULL,\n *                 email VARCHAR(255) UNIQUE NOT NULL\n *             )\n *         \"\"\")\n *     }\n * }\n *\n * // Or using the generic interface directly:\n * class SeedTestDataMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n *     override val order: Int = 100  // Run after schema creation\n *\n *     override suspend fun execute(connection: PostgresSqlMigrationContext) {\n *         connection.operations.execute(\n *             \"INSERT INTO users (name, email) VALUES ('Test User', 'test@example.com')\"\n *         )\n *     }\n * }\n * ```\n *\n * ## Registering Migrations\n *\n * ```kotlin\n * postgresql {\n *     PostgresqlOptions(\n *         configureExposedConfiguration = { /* ... */ }\n *     ).migrations {\n *         register<CreateUsersTableMigration>()\n *         register<CreateOrdersTableMigration>()\n *         register<SeedTestDataMigration>()\n *     }\n * }\n * ```\n *\n * ## Migration Order\n *\n * Migrations execute in ascending order of the [order] property:\n * - Use [MigrationPriority.HIGHEST] for schema creation\n * - Use [MigrationPriority.LOWEST] for cleanup or final setup\n * - Use intermediate values (e.g., 1, 2, 3, 100) for ordered execution\n *\n * ## Important Notes\n *\n * - Migrations cannot have constructor dependencies (use `object` or no-arg constructors)\n * - Migrations run after [AfterRunAware.afterRun]\n * - Connection is managed by Stove - don't close it manually\n * - Use idempotent statements (`IF NOT EXISTS`) for safety\n *\n * @param TConnection The database connection type (e.g., `Connection` for JDBC, `MongoClient` for MongoDB)\n * @see MigrationPriority\n * @see AfterRunAware.afterRun\n */\ninterface DatabaseMigration<in TConnection> {\n  /**\n   * Executes the migration using the provided connection.\n   *\n   * The [connection] is already established and ready for use.\n   * Do not close or dispose the connection - Stove manages its lifecycle.\n   *\n   * @param connection An active database connection.\n   */\n  suspend fun execute(connection: TConnection)\n\n  /**\n   * The execution order of this migration.\n   *\n   * Lower values execute first. Use [MigrationPriority] constants\n   * or specific integer values for fine-grained control.\n   *\n   * @see MigrationPriority\n   */\n  val order: Int\n}\n\n/**\n * Predefined priority values for migration ordering.\n *\n * ## Usage\n *\n * ```kotlin\n * class SchemaCreation : PostgresqlMigration {\n *     override val order = MigrationPriority.HIGHEST.value  // Runs first\n *     // ...\n * }\n *\n * class DataSeeding : PostgresqlMigration {\n *     override val order = MigrationPriority.LOWEST.value   // Runs last\n *     // ...\n * }\n *\n * class MiddleMigration : PostgresqlMigration {\n *     override val order = 50  // Custom priority\n *     // ...\n * }\n * ```\n */\nenum class MigrationPriority(\n  /**\n   * The integer value representing this priority level.\n   */\n  val value: Int\n) {\n  /**\n   * Lowest priority - migration runs last.\n   *\n   * Use for cleanup, finalization, or dependent migrations.\n   */\n  LOWEST(Int.MAX_VALUE),\n\n  /**\n   * Highest priority - migration runs first.\n   *\n   * Use for schema creation, essential setup, or migrations\n   * that others depend on.\n   */\n  HIGHEST(Int.MIN_VALUE)\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/database/migrations/MigrationCollection.kt",
    "content": "package com.trendyol.stove.database.migrations\n\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport kotlin.reflect.KClass\nimport kotlin.reflect.full.createInstance\n\n/**\n * A registry for database migrations that manages registration, ordering, and execution.\n *\n * This class stores and executes [DatabaseMigration]s in the correct order.\n * Migrations are deduplicated by class type and executed sorted by their [DatabaseMigration.order].\n *\n * ## Registering Migrations\n *\n * ```kotlin\n * postgresql {\n *     PostgresqlOptions(\n *         configureExposedConfiguration = { /* ... */ }\n *     ).migrations {\n *         // Simple registration (uses no-arg constructor)\n *         register<CreateUsersTableMigration>()\n *         register<CreateOrdersTableMigration>()\n *\n *         // Registration with custom instance\n *         register<SeedDataMigration> {\n *             SeedDataMigration(testDataPath = \"/test-data.sql\")\n *         }\n *     }\n * }\n * ```\n *\n * ## Replacing Migrations\n *\n * Useful for test-specific migrations that override default behavior:\n *\n * ```kotlin\n * .migrations {\n *     register<ProductionSeedMigration>()\n *\n *     // Replace with test-specific seed data\n *     replace<ProductionSeedMigration, TestSeedMigration>()\n *\n *     // Or replace with custom instance\n *     replace<ProductionSeedMigration> {\n *         MinimalSeedMigration()\n *     }\n * }\n * ```\n *\n * ## Execution Order\n *\n * Migrations execute in ascending order of [DatabaseMigration.order]:\n *\n * ```kotlin\n * class SchemaCreation : DatabaseMigration<Connection> {\n *     override val order = MigrationPriority.HIGHEST.value  // -2147483648\n * }\n *\n * class DataSeeding : DatabaseMigration<Connection> {\n *     override val order = 100  // After schema\n * }\n *\n * class IndexCreation : DatabaseMigration<Connection> {\n *     override val order = MigrationPriority.LOWEST.value   // 2147483647\n * }\n * ```\n *\n * @param TConnection The database connection type (e.g., `Connection`, `MongoClient`).\n * @see DatabaseMigration\n * @see MigrationPriority\n */\n@StoveDsl\nclass MigrationCollection<TConnection> {\n  private val types: MutableMap<KClass<*>, DatabaseMigration<TConnection>> = mutableMapOf()\n\n  /**\n   * Registers a migration by its class, creating an instance using reflection.\n   *\n   * The migration class must have a no-argument constructor.\n   * If a migration of this type is already registered, it won't be replaced.\n   *\n   * @param clazz The migration class to register.\n   * @return This collection for fluent chaining.\n   */\n  fun <T : DatabaseMigration<TConnection>> register(clazz: KClass<T>): MigrationCollection<TConnection> =\n    types\n      .putIfAbsent(clazz, clazz.createInstance() as DatabaseMigration<TConnection>)\n      .let { this }\n\n  /**\n   * Registers a migration with a specific instance.\n   *\n   * Use this when your migration requires constructor parameters\n   * or custom initialization.\n   *\n   * @param clazz The migration class (used as the registry key).\n   * @param migrator The migration instance to register.\n   * @return This collection for fluent chaining.\n   */\n  fun <T : DatabaseMigration<TConnection>> register(\n    clazz: KClass<T>,\n    migrator: DatabaseMigration<TConnection>\n  ): MigrationCollection<TConnection> =\n    types\n      .put(clazz, migrator)\n      .let { this }\n\n  /**\n   * Registers a migration using a factory function.\n   *\n   * ```kotlin\n   * register<ConfigurableMigration> {\n   *     ConfigurableMigration(batchSize = 1000)\n   * }\n   * ```\n   *\n   * @param instance Factory function that creates the migration instance.\n   * @return This collection for fluent chaining.\n   */\n  inline fun <reified T : DatabaseMigration<TConnection>> register(\n    instance: () -> DatabaseMigration<TConnection>\n  ): MigrationCollection<TConnection> = this.register(T::class, instance()).let { this }\n\n  /**\n   * Replaces an existing migration with a new instance.\n   *\n   * @param clazz The migration class to replace (registry key).\n   * @param migrator The new migration instance.\n   * @return This collection for fluent chaining.\n   */\n  fun <T : DatabaseMigration<TConnection>> replace(\n    clazz: KClass<T>,\n    migrator: DatabaseMigration<TConnection>\n  ): MigrationCollection<TConnection> =\n    types\n      .replace(clazz, migrator)\n      .let { this }\n\n  /**\n   * Registers a migration using its reified type parameter.\n   *\n   * This is the most common way to register migrations:\n   *\n   * ```kotlin\n   * migrations {\n   *     register<CreateTablesMigration>()\n   *     register<SeedDataMigration>()\n   * }\n   * ```\n   *\n   * @return This collection for fluent chaining.\n   */\n  inline fun <reified T : DatabaseMigration<TConnection>> register(): MigrationCollection<TConnection> =\n    this.register(T::class).let { this }\n\n  /**\n   * Replaces an existing migration using a factory function.\n   *\n   * ```kotlin\n   * replace<ProductionMigration> {\n   *     TestMigration()\n   * }\n   * ```\n   *\n   * @param instance Factory function that creates the replacement migration.\n   * @return This collection for fluent chaining.\n   */\n  inline fun <reified T : DatabaseMigration<TConnection>> replace(\n    instance: () -> DatabaseMigration<TConnection>\n  ): MigrationCollection<TConnection> = this.replace(T::class, instance()).let { this }\n\n  /**\n   * Replaces one migration type with another.\n   *\n   * The new migration class must have a no-argument constructor.\n   *\n   * ```kotlin\n   * // Replace production migration with test-specific one\n   * replace<ProductionSeedMigration, TestSeedMigration>()\n   * ```\n   *\n   * @param TOld The migration type to replace.\n   * @param TNew The new migration type.\n   * @return This collection for fluent chaining.\n   */\n  inline fun <\n    reified TOld : DatabaseMigration<TConnection>,\n    reified TNew : DatabaseMigration<TConnection>\n    > replace(): MigrationCollection<TConnection> =\n    this.replace(TOld::class, TNew::class.createInstance()).let { this }\n\n  /**\n   * Executes all registered migrations in order.\n   *\n   * Migrations are sorted by [DatabaseMigration.order] (ascending)\n   * and executed sequentially.\n   *\n   * @param connection The active database connection for executing migrations.\n   */\n  suspend fun run(connection: TConnection): Unit = types\n    .map {\n      it.value\n    }.sortedBy {\n      it.order\n    }.forEach { it.execute(connection) }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/database/migrations/SupportsMigrations.kt",
    "content": "package com.trendyol.stove.database.migrations\n\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n/**\n * Interface for system options that support migrations.\n *\n * Implement this interface to add migration support to your system options.\n * The [TContext] type parameter represents the context passed to migrations\n * (e.g., database connection, admin client, etc.).\n *\n * Example implementation:\n * ```kotlin\n * class MySystemOptions(\n *   override val configureExposedConfiguration: (MyExposedConfig) -> List<String>\n * ) : SystemOptions, SupportsMigrations<MyMigrationContext, MySystemOptions> {\n *\n *   override val migrationCollection: MigrationCollection<MyMigrationContext> = MigrationCollection()\n * }\n * ```\n *\n * Usage:\n * ```kotlin\n * mySystem {\n *   MySystemOptions(\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   ).migrations {\n *     register<MyMigration>()\n *   }\n * }\n * ```\n *\n * @param TContext The type of context passed to migrations (e.g., database connection)\n * @param TSelf The concrete type of the implementing class (for fluent API)\n */\ninterface SupportsMigrations<TContext, TSelf : SupportsMigrations<TContext, TSelf>> {\n  /**\n   * The collection of migrations to run.\n   */\n  val migrationCollection: MigrationCollection<TContext>\n\n  /**\n   * Configures migrations for this system.\n   *\n   * Example:\n   * ```kotlin\n   * options.migrations {\n   *   register<CreateTablesMigration>()\n   *   register<SeedDataMigration>()\n   * }\n   * ```\n   *\n   * @param migration Configuration block for the migration collection\n   * @return This options instance for fluent chaining\n   */\n  @Suppress(\"UNCHECKED_CAST\")\n  fun migrations(\n    migration: @StoveDsl MigrationCollection<TContext>.() -> Unit\n  ): TSelf {\n    migration(migrationCollection)\n    return this as TSelf\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/functional/Extensions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.functional\n\nimport arrow.core.*\n\n/** Extracts a [T] element if exists, otherwise throws [NoSuchElementException] */\nfun <T> Option<T>.get(): T = this.getOrElse { throw NoSuchElementException(\"get() on Option<Nothing> does not exist\") }\n\n/**\n * Extracts an [Option] nested in the [Try] to a not nested [Option].\n *\n * @return [Option] nested in a [Success] or [None] if this is a [Failure].\n */\nfun <T> Try<Option<T>>.flatten(): Option<T> =\n  when (this) {\n    is Success -> value\n    is Failure -> None\n  }\n\n/**\n * Returns [Some] if this [Some] contains a [Success]. Otherwise, returns [None].\n *\n * @return [Some] if this [Some] contains a [Success]. Otherwise, returns [None].\n */\nfun <T> Option<Try<T>>.flatten(): Option<T> = if (isNone()) None else get().toOption()\n\n/**\n * Returns nested [List] if this is [Some]. Otherwise, returns an empty [List].\n *\n * @return Nested [List] if this is [Some]. Otherwise, returns an empty [List].\n */\nfun <T> Option<Iterable<T>>.flatten(): List<T> = if (isNone()) emptyList() else get().toList()\n\n/**\n * Returns [List] of values of each [Some] in this [Iterable].\n *\n * @return [List] of values of each [Some] in this [Iterable].\n */\nfun <T> Iterable<Option<T>>.flatten(): List<T> = flatMap { it.toList() }\n\n/**\n * Moves inner [Option] outside of the outer [Try].\n *\n * @return [Try] nested in an [Option] for an [Option] nested in a [Try].\n *\n * @since 1.4.0\n */\nfun <T> Try<Option<T>>.evert(): Option<Try<T>> =\n  when (this) {\n    is Success -> value.map { Success(it) }\n    is Failure -> Some(this)\n  }\n\n/**\n * Moves inner [Try] outside of the outer [Option].\n *\n * @return [Option] nested in a [Try] for a [Try] nested in an [Option].\n *\n * @since 1.4.0\n */\nfun <T> Option<Try<T>>.evert(): Try<Option<T>> =\n  when (this) {\n    is Some -> value.map { Some(it) }\n    is None -> Success(None)\n  }\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/functional/Reflect.kt",
    "content": "package com.trendyol.stove.functional\n\nimport kotlin.reflect.KProperty\n\nclass Reflect<T : Any>(\n  val instance: T\n) {\n  inner class OnGoingReflect<R>(\n    private val instance: T,\n    private val property: String\n  ) {\n    infix fun then(value: R) {\n      val prop = instance::class.java.getDeclaredField(property)\n      prop.isAccessible = true\n      prop.set(instance, value)\n    }\n  }\n\n  inline fun <reified R> on(propertySelector: T.() -> KProperty<R>): OnGoingReflect<R> =\n    OnGoingReflect(instance, propertySelector(instance).name)\n\n  inline fun <reified R> on(property: String): OnGoingReflect<R> = OnGoingReflect(instance, property)\n\n  companion object {\n    inline operator fun <reified T : Any> invoke(\n      instance: T,\n      block: Reflect<T>.() -> Unit\n    ): Reflect<T> {\n      val ref = Reflect(instance)\n      block(ref)\n      return ref\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/functional/Try.kt",
    "content": "@file:Suppress(\"unused\", \"TooGenericExceptionCaught\")\n\npackage com.trendyol.stove.functional\n\nimport arrow.core.*\nimport arrow.core.Either.Left\nimport arrow.core.Either.Right\n\n// https://github.com/sczerwinski/kotlin-util\n\n/**\n * Representation of an operation that might successfully return a value or throw an exception.\n *\n * An instance of [Try] may be either a [Success] or a [Failure].\n *\n * This type is based on `scala.util.Try`.\n *\n * _Note: Only non-fatal exceptions are caught by the combinators on `Try` (see [NonFatal]). Serious\n * system errors, on the other hand, will be thrown._\n *\n * @param T Type of the value of a successful operation.\n */\nsealed class Try<out T> {\n  /**\n   * Returns `true` if this is a [Success] or `false` if this is [Failure].\n   *\n   * @return `true` if this is a [Success] or `false` if this is [Failure].\n   */\n  abstract val isSuccess: Boolean\n\n  /**\n   * Returns `true` if this is a [Failure] or `false` if this is [Success].\n   *\n   * @return `true` if this is a [Failure] or `false` if this is [Success].\n   */\n  abstract val isFailure: Boolean\n\n  /**\n   * Returns a [Success] with an exception it this is a [Failure] or a [Failure] if this is a\n   * [Success].\n   *\n   * @return A [Success] with an exception it this is a [Failure] or a [Failure] if this is a\n   * [Success].\n   */\n  abstract val failed: Try<Throwable>\n\n  /**\n   * Gets the value of a [Success] or throw an exception from a [Failure].\n   *\n   * @return Value of a [Success].\n   *\n   * @throws Throwable If this is a [Failure].\n   */\n  abstract fun get(): T\n\n  /**\n   * Gets the value of a [Success] or `null` if this is a [Failure].\n   *\n   * @return Value of a [Success] or `null`.\n   */\n  abstract fun getOrNull(): T?\n\n  /**\n   * Runs [action] if this is a [Success]. Returns [Unit] without any action if this is a [Failure].\n   *\n   * @param action Action to be run on a value of a [Success].\n   */\n  inline fun forEach(action: (T) -> Unit) {\n    if (isSuccess) action(get())\n  }\n\n  /**\n   * Maps value of a [Success] using [transform] or returns the same [Try] if this is a [Failure].\n   *\n   * @param transform Function transforming value of a [Success].\n   *\n   * @return [Try] with a value mapped using [transform] or this object if this is a [Failure].\n   */\n  inline fun <R> map(transform: (T) -> R): Try<R> =\n    when (this) {\n      is Success -> Try { transform(value) }\n      is Failure -> this\n    }\n\n  /**\n   * Maps value of a [Success] to a new [Try] using [transform] or returns the same [Try] if this is\n   * a [Failure].\n   *\n   * @param transform Function transforming value of a [Success] to a [Try].\n   *\n   * @return [Try] returned by [transform] or this object if this is a [Failure].\n   */\n  inline fun <R> flatMap(transform: (T) -> Try<R>): Try<R> =\n    when (this) {\n      is Success -> {\n        try {\n          transform(value)\n        } catch (exception: Throwable) {\n          if (NonFatal(exception)) Failure(exception) else throw exception\n        }\n      }\n\n      is Failure -> {\n        this\n      }\n    }\n\n  /**\n   * Returns the same [Success] if the [predicate] is satisfied for the value. Otherwise, returns a\n   * [Failure].\n   *\n   * @param predicate Predicate function.\n   *\n   * @return The same [Success] if the [predicate] is satisfied for the value. Otherwise, returns a\n   * [Failure].\n   */\n  inline fun filter(predicate: (T) -> Boolean): Try<T> =\n    when (this) {\n      is Success -> {\n        try {\n          if (predicate(value)) {\n            this\n          } else {\n            throw NoSuchElementException(\"Predicate not satisfied for $value\")\n          }\n        } catch (exception: Throwable) {\n          if (NonFatal(exception)) Failure(exception) else throw exception\n        }\n      }\n\n      is Failure -> {\n        this\n      }\n    }\n\n  /**\n   * Returns the same [Success] if the [predicate] is not satisfied for the value. Otherwise,\n   * returns a [Failure].\n   *\n   * @param predicate Predicate function.\n   *\n   * @return The same [Success] if the [predicate] is not satisfied for the value. Otherwise,\n   * returns a [Failure].\n   */\n  inline fun filterNot(predicate: (T) -> Boolean): Try<T> =\n    when (this) {\n      is Success -> {\n        try {\n          if (!predicate(value)) {\n            this\n          } else {\n            throw NoSuchElementException(\"Predicate not satisfied for $value\")\n          }\n        } catch (exception: Throwable) {\n          if (NonFatal(exception)) Failure(exception) else throw exception\n        }\n      }\n\n      is Failure -> {\n        this\n      }\n    }\n\n  /**\n   * Returns the same [Success] cast to type [R] if it is [R]. Otherwise, returns a [Failure].\n   *\n   * @param R Required type of the optional value.\n   *\n   * @return The same [Success] cast to type [R] if it is [R]. Otherwise, returns a [Failure].\n   */\n  inline fun <reified R> filterIsInstance(): Try<R> =\n    when (this) {\n      is Success -> {\n        try {\n          if (value is R) {\n            Success<R>(value)\n          } else {\n            throw NoSuchElementException(\"Not an instance of ${R::class}\")\n          }\n        } catch (exception: Throwable) {\n          if (NonFatal(exception)) Failure(exception) else throw exception\n        }\n      }\n\n      is Failure -> {\n        this\n      }\n    }\n\n  /**\n   * Returns the same [Success] if the [predicate] is satisfied for the value. Otherwise, returns a\n   * [Failure] containing the given [throwable].\n   *\n   * @param predicate Predicate function.\n   * @param throwable Function providing a throwable to be used when the [predicate] is not\n   * satisfied.\n   *\n   * @return The same [Success] if the [predicate] is satisfied for the value. Otherwise, returns a\n   * [Failure] containing the given [throwable].\n   *\n   * @since 1.2\n   */\n  inline fun filterOrElse(\n    predicate: (T) -> Boolean,\n    throwable: (T) -> Throwable\n  ): Try<T> =\n    when (this) {\n      is Success -> {\n        try {\n          if (predicate(value)) this else throw throwable(value)\n        } catch (exception: Throwable) {\n          if (NonFatal(exception)) Failure(exception) else throw exception\n        }\n      }\n\n      is Failure -> {\n        this\n      }\n    }\n\n  /**\n   * Transforms a [Success] using [successTransform] or a [Failure] using [failureTransform].\n   *\n   * @param successTransform Function transforming value of a [Success] to a new [Try].\n   * @param failureTransform Function transforming exception from a [Failure] to a new [Try].\n   *\n   * @return Result of applying [successTransform] on [Success] or [failureTransform] on [Failure] .\n   */\n  inline fun <R> fold(\n    successTransform: (T) -> R,\n    failureTransform: (Throwable) -> R\n  ): R =\n    when (this) {\n      is Success -> {\n        try {\n          successTransform(value)\n        } catch (exception: Throwable) {\n          if (NonFatal(exception)) failureTransform(exception) else throw exception\n        }\n      }\n\n      is Failure -> {\n        failureTransform(exception)\n      }\n    }\n\n  /**\n   * Transforms a [Success] using [successTransform] or a [Failure] using [failureTransform].\n   *\n   * @param successTransform Function transforming value of a [Success] to a new [Try].\n   * @param failureTransform Function transforming exception from a [Failure] to a new [Try].\n   *\n   * @return New [Try] being a result of a transformation of a [Success] with [successTransform] or\n   * a [Failure] with [failureTransform].\n   */\n  inline fun <R> transform(\n    successTransform: (T) -> Try<R>,\n    failureTransform: (Throwable) -> Try<R>\n  ): Try<R> =\n    try {\n      when (this) {\n        is Success -> successTransform(value)\n        is Failure -> failureTransform(exception)\n      }\n    } catch (exception: Throwable) {\n      if (NonFatal(exception)) Failure(exception) else throw exception\n    }\n\n  /**\n   * Returns [Success] containing a `Pair` of values of this and [other] [Try] if both instances of\n   * [Try] are [Success]. Otherwise, returns first [Failure].\n   *\n   * @param other Other [Try].\n   *\n   * @return [Success] containing a `Pair` of values of this and [other] [Try] if both instances of\n   * [Try] are [Success]. Otherwise, returns first [Failure].\n   *\n   * @since 1.1\n   */\n  infix fun <R> zip(other: Try<R>): Try<Pair<T, R>> = Try { get() to other.get() }\n\n  /**\n   * Returns [Success] containing the result of applying [transform] to both values of this and\n   * [other] [Try] if both instances of [Try] are [Success]. Otherwise, returns first [Failure].\n   *\n   * @param other Other [Try].\n   * @param transform Function transforming values of both instances of [Success].\n   *\n   * @return [Success] containing the result of applying [transform] to both values of this and\n   * [other] [Try] if both instances of [Try] are [Success]. Otherwise, returns first [Failure].\n   *\n   * @since 1.1\n   */\n  inline fun <T1, R> zip(\n    other: Try<T1>,\n    transform: (T, T1) -> R\n  ): Try<R> = Try { transform(get(), other.get()) }\n\n  /**\n   * Converts this [Try] to [Either].\n   *\n   * @return [Left] if this is [Failure] or [Right] if this is [Success].\n   */\n  abstract fun toEither(): Either<Throwable, T>\n\n  /**\n   * Converts this [Try] to [Option].\n   *\n   * @return [None] if this is [Failure] or [Some] if this is [Success].\n   */\n  abstract fun toOption(): Option<T>\n\n  companion object {\n    /**\n     * Creates a new [Try] based on the result of the [callable].\n     *\n     * @param callable A callable operation.\n     *\n     * @return An instance of [Success] or [Failure], depending on whether the operation.\n     */\n    inline operator fun <T> invoke(callable: () -> T): Try<T> =\n      try {\n        Success(callable())\n      } catch (exception: Throwable) {\n        if (NonFatal(exception)) Failure(exception) else throw exception\n      }\n  }\n}\n\n/**\n * Gets the value of a [Success] or [default] value if this is a [Failure].\n *\n * @param default Default value provider.\n *\n * @return Value of a [Success] or [default] value.\n */\ninline fun <T> Try<T>.getOrElse(default: () -> T): T = if (isSuccess) get() else default()\n\n/**\n * Returns this [Try] if this is a [Success] or [default] if this is a [Failure].\n *\n * @param default Default [Try] provider.\n *\n * @return This [Success] or [default].\n */\ninline fun <T> Try<T>.orElse(default: () -> Try<T>): Try<T> =\n  if (isSuccess) {\n    this\n  } else {\n    try {\n      default()\n    } catch (exception: Throwable) {\n      if (NonFatal(exception)) Failure(exception) else throw exception\n    }\n  }\n\n/**\n * Transforms a nested [Try] to a not nested [Try].\n *\n * @return [Try] nested in a [Success] or this object if this is a [Failure].\n */\nfun <T> Try<Try<T>>.flatten(): Try<T> =\n  when (this) {\n    is Success -> value\n    is Failure -> this\n  }\n\n/**\n * Returns this [Try] if this is a [Success] or a [Try] created for the [rescue] operation if this\n * is a [Failure].\n *\n * @param rescue Function creating a new value from the exception to a [Failure].\n *\n * @return This [Try] if this is a [Success] or a [Try] created for the [rescue] operation if this\n * is a [Failure].\n */\ninline fun <T> Try<T>.recover(rescue: (Throwable) -> T): Try<T> =\n  when (this) {\n    is Success -> {\n      this\n    }\n\n    is Failure -> {\n      try {\n        Success(rescue(exception))\n      } catch (exception: Throwable) {\n        if (NonFatal(exception)) Failure(exception) else throw exception\n      }\n    }\n  }\n\n/**\n * Returns this [Try] if this is a [Success] or a [Try] created by the [rescue] function if this is\n * a [Failure].\n *\n * @param rescue Function creating a new [Try] from the exception to a [Failure].\n *\n * @return This [Try] if this is a [Success] or a [Try] created by the [rescue] function if this is\n * a [Failure].\n */\ninline fun <T> Try<T>.recoverWith(rescue: (Throwable) -> Try<T>): Try<T> =\n  when (this) {\n    is Success -> {\n      this\n    }\n\n    is Failure -> {\n      try {\n        rescue(exception)\n      } catch (exception: Throwable) {\n        if (NonFatal(exception)) Failure(exception) else throw exception\n      }\n    }\n  }\n\n/**\n * Returns the same [Success] if its value is not `null`. Otherwise, returns a [Failure].\n *\n * @return The same [Success] if its value is not `null`. Otherwise, returns a [Failure].\n */\nfun <T> Try<T?>.filterNotNull(): Try<T> =\n  when (this) {\n    is Success -> {\n      try {\n        if (value != null) Success(value) else throw NoSuchElementException(\"Value is null\")\n      } catch (exception: Throwable) {\n        if (NonFatal(exception)) Failure(exception) else throw exception\n      }\n    }\n\n    is Failure -> {\n      this\n    }\n  }\n\ndata class Success<out T>(\n  val value: T\n) : Try<T>() {\n  override val isSuccess: Boolean\n    get() = true\n\n  override val isFailure: Boolean\n    get() = false\n\n  override val failed: Try<Throwable>\n    get() = Failure(UnsupportedOperationException(\"Unsupported operation: Success::failed\"))\n\n  override fun get(): T = value\n\n  override fun getOrNull(): T? = value\n\n  override fun toEither(): Either<Throwable, T> = Right(value)\n\n  override fun toOption(): Option<T> = Some(value)\n}\n\ndata class Failure(\n  val exception: Throwable\n) : Try<Nothing>() {\n  override val isSuccess: Boolean\n    get() = false\n\n  override val isFailure: Boolean\n    get() = true\n\n  override val failed: Try<Throwable>\n    get() = Success(exception)\n\n  override fun get(): Nothing = throw exception\n\n  override fun getOrNull(): Nothing? = null\n\n  override fun toEither(): Either<Throwable, Nothing> = Left(exception)\n\n  override fun toOption(): Option<Nothing> = None\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/http/StoveHttpResponse.kt",
    "content": "package com.trendyol.stove.http\n\nsealed class StoveHttpResponse(\n  open val status: Int,\n  open val headers: Map<String, Any>\n) {\n  data class Bodiless(\n    override val status: Int,\n    override val headers: Map<String, Any>\n  ) : StoveHttpResponse(status, headers)\n\n  data class WithBody<T>(\n    override val status: Int,\n    override val headers: Map<String, Any>,\n    val body: suspend () -> T\n  ) : StoveHttpResponse(status, headers)\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/messaging/Observation.kt",
    "content": "package com.trendyol.stove.messaging\n\nimport arrow.core.Option\nimport com.trendyol.stove.system.annotations.StoveDsl\n\ndata class MessageMetadata(\n  val topic: String,\n  val key: String,\n  val headers: Map<String, Any>\n)\n\nsealed interface ParsedMessage<T> {\n  val message: Option<T>\n  val metadata: MessageMetadata\n}\n\nclass SuccessfulParsedMessage<T>(\n  override val message: Option<T>,\n  override val metadata: MessageMetadata\n) : ParsedMessage<T>\n\nclass FailedParsedMessage<T>(\n  override val message: Option<T>,\n  override val metadata: MessageMetadata,\n  val reason: Throwable\n) : ParsedMessage<T>\n\n@StoveDsl\nopen class ObservedMessage<T>(\n  open val actual: T,\n  open val metadata: MessageMetadata\n)\n\n@StoveDsl\ndata class FailedObservedMessage<T>(\n  override val actual: T,\n  override val metadata: MessageMetadata,\n  val reason: Throwable\n) : ObservedMessage<T>(actual, metadata)\n\n@StoveDsl\ndata class Failure<T>(\n  val message: ObservedMessage<T>,\n  val reason: Throwable\n)\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/JsonReportRenderer.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport arrow.core.Option\nimport arrow.core.getOrElse\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.databind.SerializationFeature\nimport java.time.Instant\nimport java.time.format.DateTimeFormatter\n\n/**\n * JSON renderer for machine-parseable test reports.\n * Useful for CI integration, log aggregation, and programmatic analysis.\n */\nobject JsonReportRenderer : ReportRenderer {\n  private val mapper = ObjectMapper().apply {\n    enable(SerializationFeature.INDENT_OUTPUT)\n  }\n\n  private val timestampFormatter = DateTimeFormatter.ISO_INSTANT\n\n  override fun render(report: TestReport, snapshots: List<SystemSnapshot>): String {\n    val entries = report.entries()\n    val jsonReport = JsonTestReport(\n      testId = report.testId,\n      testName = report.testName,\n      timestamp = timestampFormatter.format(Instant.now()),\n      entries = entries.map { it.toJsonEntry() },\n      systemSnapshots = snapshots.associate { it.system to it.state },\n      summary = JsonSummary(\n        total = entries.size,\n        passed = entries.count { it.isPassed },\n        failed = entries.count { it.isFailed }\n      )\n    )\n    return mapper.writeValueAsString(jsonReport)\n  }\n\n  private fun ReportEntry.toJsonEntry(): JsonReportEntry = JsonReportEntry(\n    timestamp = timestampFormatter.format(timestamp),\n    system = system,\n    testId = testId,\n    action = action,\n    input = input.toJsonValue(),\n    output = output.toJsonValue(),\n    metadata = metadata,\n    expected = expected.toJsonValue(),\n    actual = actual.toJsonValue(),\n    result = result.name,\n    error = error.toJsonValue()\n  )\n\n  /** Convert Option to JSON-friendly value - empty string for None */\n  private fun <T : Any> Option<T>.toJsonValue(): Any = getOrElse { \"\" }\n}\n\n/**\n * JSON representation of a test report.\n */\ndata class JsonTestReport(\n  val testId: String,\n  val testName: String,\n  val timestamp: String,\n  val entries: List<JsonReportEntry>,\n  val systemSnapshots: Map<String, Any>,\n  val summary: JsonSummary\n)\n\n/**\n * JSON representation of a report entry.\n * No nullable fields - uses empty string/map for absent values.\n */\ndata class JsonReportEntry(\n  val timestamp: String,\n  val system: String,\n  val testId: String,\n  val action: String,\n  val input: Any,\n  val output: Any,\n  val metadata: Map<String, Any>,\n  val expected: Any,\n  val actual: Any,\n  val result: String,\n  val error: Any\n)\n\n/**\n * JSON representation of report summary.\n */\ndata class JsonSummary(\n  val total: Int,\n  val passed: Int,\n  val failed: Int\n)\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/PrettyConsoleRenderer.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport arrow.core.Option\nimport arrow.core.getOrElse\nimport com.github.ajalt.mordant.rendering.AnsiLevel\nimport com.github.ajalt.mordant.rendering.BorderType.Companion.ROUNDED\nimport com.github.ajalt.mordant.rendering.BorderType.Companion.SQUARE\nimport com.github.ajalt.mordant.rendering.TextColors.brightBlue\nimport com.github.ajalt.mordant.rendering.TextColors.brightCyan\nimport com.github.ajalt.mordant.rendering.TextColors.brightGreen\nimport com.github.ajalt.mordant.rendering.TextColors.brightMagenta\nimport com.github.ajalt.mordant.rendering.TextColors.brightRed\nimport com.github.ajalt.mordant.rendering.TextColors.brightWhite\nimport com.github.ajalt.mordant.rendering.TextColors.brightYellow\nimport com.github.ajalt.mordant.rendering.TextColors.cyan\nimport com.github.ajalt.mordant.rendering.TextColors.green\nimport com.github.ajalt.mordant.rendering.TextColors.magenta\nimport com.github.ajalt.mordant.rendering.TextColors.red\nimport com.github.ajalt.mordant.rendering.TextColors.white\nimport com.github.ajalt.mordant.rendering.TextColors.yellow\nimport com.github.ajalt.mordant.rendering.TextStyle\nimport com.github.ajalt.mordant.rendering.TextStyles.bold\nimport com.github.ajalt.mordant.rendering.TextStyles.dim\nimport com.github.ajalt.mordant.rendering.Whitespace\nimport com.github.ajalt.mordant.rendering.Widget\nimport com.github.ajalt.mordant.table.verticalLayout\nimport com.github.ajalt.mordant.terminal.Terminal\nimport com.github.ajalt.mordant.widgets.Panel\nimport com.github.ajalt.mordant.widgets.Text\nimport com.trendyol.stove.tracing.TraceVisualization\nimport java.time.ZoneId\nimport java.time.format.DateTimeFormatter\n\n/**\n * Mordant-based renderer for rich, terminal-friendly Stove test reports.\n */\n@Suppress(\"TooManyFunctions\")\nobject PrettyConsoleRenderer : ReportRenderer {\n  private const val MIN_RENDER_WIDTH = 72\n  private const val MAX_RENDER_WIDTH = 160\n  private const val PANEL_CHROME_WIDTH = 6\n  private const val NESTED_PANEL_CHROME_WIDTH = 12\n  private const val SNAPSHOT_INDENT_STEP = 4\n  private const val DETAIL_INDENT_STEP = 2\n  private const val VALUE_PREVIEW_LIMIT = 6\n  private const val LABEL_WRAP_INDENT_LIMIT = 32\n  private const val MIN_BREAK_SEARCH_WINDOW = 12\n\n  private val timeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern(\"HH:mm:ss.SSS\")\n\n  private data class SummaryStats(\n    val passed: Int,\n    val failed: Int,\n    val total: Int\n  ) {\n    val hasFailures: Boolean = failed > 0\n    val statusLabel: String = if (hasFailures) \"FAILED\" else \"IN PROGRESS\"\n    val statusColor: TextStyle = if (hasFailures) brightRed else brightBlue\n    val borderColor: TextStyle = if (hasFailures) brightMagenta else brightCyan\n  }\n\n  private data class PreparedSnapshot(\n    val snapshot: SystemSnapshot,\n    val text: String\n  )\n\n  private data class PreparedReport(\n    val report: TestReport,\n    val entries: List<ReportEntry>,\n    val summary: SummaryStats,\n    val summaryText: String,\n    val timelineText: String,\n    val snapshots: List<PreparedSnapshot>\n  )\n\n  override fun render(report: TestReport, snapshots: List<SystemSnapshot>): String {\n    val prepared = prepareReport(report, snapshots)\n    val renderWidth = calculateRenderWidth(prepared)\n    val terminal = createTerminal(renderWidth)\n    val panelContentWidth = renderWidth - PANEL_CHROME_WIDTH\n    val snapshotContentWidth = (renderWidth - NESTED_PANEL_CHROME_WIDTH).coerceAtLeast(MIN_RENDER_WIDTH - PANEL_CHROME_WIDTH)\n\n    val widgets = buildList {\n      add(buildSummaryPanel(prepared, panelContentWidth))\n      add(buildTimelinePanel(prepared, panelContentWidth))\n      if (prepared.snapshots.isNotEmpty()) add(buildSnapshotsPanel(prepared.snapshots, snapshotContentWidth))\n    }\n\n    return widgets.joinToString(separator = \"\\n\\n\") { terminal.render(it) }\n  }\n\n  private fun prepareReport(report: TestReport, snapshots: List<SystemSnapshot>): PreparedReport {\n    val entries = report.entries()\n    val summary = buildSummaryStats(entries)\n    return PreparedReport(\n      report = report,\n      entries = entries,\n      summary = summary,\n      summaryText = buildSummaryText(report, summary),\n      timelineText = buildTimelineText(entries),\n      snapshots = snapshots.map { PreparedSnapshot(it, buildSnapshotText(it)) }\n    )\n  }\n\n  private fun buildSummaryStats(entries: List<ReportEntry>): SummaryStats = SummaryStats(\n    passed = entries.count { it.isPassed },\n    failed = entries.count { it.isFailed },\n    total = entries.size\n  )\n\n  private fun createTerminal(width: Int): Terminal = Terminal(\n    ansiLevel = AnsiLevel.TRUECOLOR,\n    width = width,\n    nonInteractiveWidth = width,\n    interactive = true\n  )\n\n  private fun buildSummaryPanel(prepared: PreparedReport, contentWidth: Int): Widget = Panel(\n    title = Text((bold + brightWhite)(\"STOVE TEST EXECUTION REPORT\")),\n    bottomTitle = Text((bold + prepared.summary.statusColor)(prepared.summary.statusLabel)),\n    borderType = ROUNDED,\n    borderStyle = prepared.summary.borderColor,\n    expand = true,\n    content = Text(wrapTextBlock(prepared.summaryText, contentWidth), whitespace = Whitespace.PRE)\n  )\n\n  private fun buildSummaryText(report: TestReport, summary: SummaryStats): String = buildString {\n    appendLine(\"${bold(\"Test\")}: ${brightYellow(report.testName)}\")\n    appendLine(\"${bold(\"ID\")}: ${dim(report.testId)}\")\n    appendLine(\"${bold(\"Status\")}: ${(bold + summary.statusColor)(summary.statusLabel)}\")\n    appendLine()\n    appendLine(\n      \"${bold(\"Summary\")}: \" +\n        brightGreen(\"${summary.passed} passed\") +\n        \"  ·  \" +\n        (if (summary.failed > 0) brightRed(\"${summary.failed} failed\") else brightGreen(\"0 failed\")) +\n        \"  ·  \" +\n        brightCyan(\"${summary.total} total\")\n    )\n  }.trimEnd()\n\n  private fun buildTimelinePanel(prepared: PreparedReport, contentWidth: Int): Widget {\n    val content =\n      if (prepared.entries.isEmpty()) {\n        Text(dim(\"No actions recorded yet.\"), whitespace = Whitespace.PRE)\n      } else {\n        Text(wrapTextBlock(prepared.timelineText, contentWidth), whitespace = Whitespace.PRE)\n      }\n\n    return Panel(\n      title = Text((bold + brightCyan)(\"TIMELINE\")),\n      bottomTitle = Text(dim(\"${prepared.entries.size} step(s)\")),\n      borderType = ROUNDED,\n      borderStyle = cyan,\n      expand = true,\n      content = content\n    )\n  }\n\n  private fun calculateRenderWidth(prepared: PreparedReport): Int {\n    val candidateLines = buildList {\n      add(\"STOVE TEST EXECUTION REPORT\")\n      add(prepared.report.testName)\n      add(prepared.report.testId)\n      add(prepared.summary.statusLabel)\n      addAll(prepared.summaryText.lines())\n      add(\"TIMELINE\")\n      addAll(prepared.timelineText.lines())\n      if (prepared.snapshots.isNotEmpty()) {\n        add(\"SYSTEM SNAPSHOTS\")\n        prepared.snapshots.forEach { preparedSnapshot ->\n          add(preparedSnapshot.snapshot.system.uppercase())\n          addAll(preparedSnapshot.text.lines())\n        }\n      }\n    }\n\n    val longestLine = candidateLines.maxOfOrNull { visibleLength(it) } ?: MIN_RENDER_WIDTH\n    return (longestLine + PANEL_CHROME_WIDTH).coerceIn(MIN_RENDER_WIDTH, MAX_RENDER_WIDTH)\n  }\n\n  private fun groupSequentialBySystem(entries: List<ReportEntry>): List<List<IndexedValue<ReportEntry>>> {\n    val groups = mutableListOf<MutableList<IndexedValue<ReportEntry>>>()\n\n    entries.withIndex().forEach { indexedEntry ->\n      val lastGroup = groups.lastOrNull()\n      if (lastGroup != null && lastGroup.first().value.system == indexedEntry.value.system) {\n        lastGroup += indexedEntry\n      } else {\n        groups += mutableListOf(indexedEntry)\n      }\n    }\n\n    return groups.map { it.toList() }\n  }\n\n  private fun buildTimelineText(entries: List<ReportEntry>): String =\n    groupSequentialBySystem(entries)\n      .flatMapIndexed { groupIndex, group ->\n        val groupHeader = buildTimelineGroupHeader(group)\n        val renderedEntries = group.flatMap { indexedEntry -> buildTimelineEntryLines(indexedEntry.index + 1, indexedEntry.value) }\n        if (groupIndex == 0) listOf(groupHeader) + renderedEntries else listOf(\"\", groupHeader) + renderedEntries\n      }.joinToString(\"\\n\")\n\n  private fun buildTimelineGroupHeader(group: List<IndexedValue<ReportEntry>>): String {\n    val system = group.first().value.system\n    val style = bold + styleForSystem(system)\n    val failedCount = group.count { it.value.isFailed }\n    val passedCount = group.size - failedCount\n    val summary =\n      if (failedCount > 0) {\n        \"${brightGreen(\"$passedCount passed\")} · ${brightRed(\"$failedCount failed\")}\"\n      } else {\n        brightGreen(\"${group.size} passed\")\n      }\n\n    return \"${style(\"${system.uppercase()} · ${group.size} step(s)\")}${dim(\"  $summary\")}\"\n  }\n\n  private fun buildTimelineEntryLines(index: Int, entry: ReportEntry): List<String> {\n    val statusColor = if (entry.isFailed) brightRed else brightGreen\n    val statusText = if (entry.isFailed) \"✗ FAILED\" else \"✓ PASSED\"\n    val header = \"  ${(bold + statusColor)(\n      \"#$index $statusText\"\n    )} ${brightWhite(sanitize(entry.action))} ${dim(\"(${formatTimestamp(entry)})\")}\"\n    val details = buildEntryDetails(entry).lines().map { \"      $it\" }\n    return listOf(header) + details\n  }\n\n  private fun buildEntryDetails(entry: ReportEntry): String = buildList {\n    add(\"${brightCyan(\"Action\")}: ${sanitize(entry.action)}\")\n\n    entry.input.fold({ }, { addAll(renderDetailBlock(yellow(\"Input\"), it)) })\n    entry.output.fold({ }, { addAll(renderDetailBlock(brightBlue(\"Output\"), it)) })\n\n    if (entry.metadata.isNotEmpty()) {\n      addAll(renderDetailBlock(dim(\"Metadata\"), entry.metadata))\n    }\n\n    if (entry.isFailed) {\n      entry.expected.fold({ }, { addAll(renderDetailBlock(green(\"Expected\"), it)) })\n      entry.actual.fold({ }, { addAll(renderDetailBlock(red(\"Actual\"), it)) })\n      entry.error.fold({ }, { add(\"${brightRed(\"Error\")}: ${sanitize(it)}\") })\n    }\n\n    entry.executionTrace.fold({ }, { addAll(renderTraceDetails(it)) })\n  }.joinToString(\"\\n\")\n\n  private fun renderTraceDetails(trace: TraceVisualization): List<String> {\n    val spanSummary =\n      if (trace.failedSpans > 0) {\n        \"${trace.totalSpans} total / ${brightRed(\"${trace.failedSpans} failed\")}\"\n      } else {\n        \"${trace.totalSpans} total / ${brightGreen(\"0 failed\")}\"\n      }\n\n    val styledTreeLines = trace.tree\n      .lines()\n      .map { line ->\n        when {\n          line.contains(\"✗\") -> brightRed(line)\n          line.contains(\"✓\") -> brightGreen(line)\n          line.startsWith(\"POST\") || line.startsWith(\"GET\") || line.startsWith(\"PUT\") || line.startsWith(\"DELETE\") -> brightCyan(line)\n          line.trimStart().startsWith(\"|\") -> magenta(line)\n          else -> dim(line)\n        }\n      }\n\n    return listOf(\n      \"\",\n      (bold + brightMagenta)(\"Execution Trace\"),\n      \"${dim(\"TraceId\")}: ${trace.traceId}\",\n      \"${dim(\"Spans\")}: $spanSummary\"\n    ) + styledTreeLines\n  }\n\n  private fun buildSnapshotsPanel(snapshots: List<PreparedSnapshot>, contentWidth: Int): Widget = Panel(\n    title = Text((bold + brightMagenta)(\"SYSTEM SNAPSHOTS\")),\n    bottomTitle = Text(dim(\"${snapshots.size} snapshot(s)\")),\n    borderType = ROUNDED,\n    borderStyle = brightMagenta,\n    expand = true,\n    content = verticalLayout {\n      spacing = 1\n      snapshots.forEach { preparedSnapshot ->\n        cell(buildSnapshotPanel(preparedSnapshot, contentWidth))\n      }\n    }\n  )\n\n  private fun buildSnapshotPanel(preparedSnapshot: PreparedSnapshot, contentWidth: Int): Widget = Panel(\n    title = Text((bold + brightWhite)(preparedSnapshot.snapshot.system.uppercase())),\n    borderType = SQUARE,\n    borderStyle = styleForSystem(preparedSnapshot.snapshot.system),\n    expand = true,\n    content = Text(wrapTextBlock(preparedSnapshot.text, contentWidth), whitespace = Whitespace.PRE)\n  )\n\n  private fun buildSnapshotText(snapshot: SystemSnapshot): String {\n    val summaryLines = snapshot.summary\n      .lines()\n      .map(::sanitize)\n      .filter { it.isNotBlank() }\n\n    val stateLines = renderSnapshotState(snapshot.state)\n\n    return buildString {\n      appendLine((bold + brightCyan)(\"Summary\"))\n      if (summaryLines.isEmpty()) {\n        appendLine(\"  ${dim(\"No summary available\")}\")\n      } else {\n        summaryLines.forEach { appendLine(\"  ${styleSummaryLine(it)}\") }\n      }\n\n      if (stateLines.isNotEmpty()) {\n        appendLine()\n        appendLine((bold + brightCyan)(\"State\"))\n        stateLines.forEach(::appendLine)\n      }\n    }.trimEnd()\n  }\n\n  private fun renderSnapshotState(state: Map<String, Any>, indent: Int = 4): List<String> =\n    state.flatMap { (key, value) -> renderSnapshotEntry(key, value, indent) }\n\n  private fun renderSnapshotEntry(key: String, value: Any?, indent: Int): List<String> {\n    val prefix = \" \".repeat(indent)\n    val keyLabel = yellow(key)\n\n    return when (value) {\n      is Collection<*> -> {\n        val count = \"$prefix$keyLabel: ${styleCollectionCount(key, value.size)}\"\n        val items = value.flatMapIndexed { index, item -> renderSnapshotItem(index, item, indent + SNAPSHOT_INDENT_STEP) }\n        listOf(count) + items\n      }\n\n      is Map<*, *> -> {\n        val header = \"$prefix$keyLabel:\"\n        val lines = value.entries.flatMap { (nestedKey, nestedValue) ->\n          renderSnapshotEntry(nestedKey.toString(), nestedValue, indent + SNAPSHOT_INDENT_STEP)\n        }\n        listOf(header) + lines\n      }\n\n      else -> {\n        listOf(\"$prefix$keyLabel: ${styleSnapshotValue(key, value)}\")\n      }\n    }\n  }\n\n  private fun renderSnapshotItem(index: Int, item: Any?, indent: Int): List<String> {\n    val prefix = \" \".repeat(indent)\n    val indexLabel = dim(\"[$index]\")\n\n    return when (item) {\n      is Map<*, *> -> {\n        val nested = item.entries.flatMap { (key, value) ->\n          renderSnapshotEntry(key.toString(), value, indent + SNAPSHOT_INDENT_STEP)\n        }\n        listOf(\"$prefix$indexLabel\") + nested\n      }\n\n      is Collection<*> -> {\n        listOf(\"$prefix$indexLabel ${brightCyan(\"${item.size} item(s)\")}\")\n      }\n\n      else -> {\n        listOf(\"$prefix$indexLabel ${formatValuePlain(item)}\")\n      }\n    }\n  }\n\n  private fun renderDetailBlock(label: String, value: Any?): List<String> {\n    val renderedValue = renderNestedValue(value)\n    return if (renderedValue.size == 1) {\n      listOf(\"$label: ${renderedValue.first().trimStart()}\")\n    } else {\n      listOf(\"$label:\") + renderedValue.map { \"  $it\" }\n    }\n  }\n\n  private fun renderNestedValue(value: Any?, indent: Int = 0): List<String> {\n    val prefix = \" \".repeat(indent)\n    return when (value) {\n      null -> {\n        listOf(\"${prefix}none\")\n      }\n\n      is Option<*> -> {\n        renderNestedValue(value.getOrElse { null }, indent)\n      }\n\n      is String -> {\n        sanitize(value).lines().map { \"$prefix$it\" }\n      }\n\n      is Number, is Boolean -> {\n        listOf(\"$prefix$value\")\n      }\n\n      is Map<*, *> -> {\n        if (value.isEmpty()) {\n          listOf(\"$prefix{}\")\n        } else {\n          value.entries.flatMap { (key, nestedValue) ->\n            when (nestedValue) {\n              is Map<*, *>, is Collection<*> -> listOf(\"$prefix$key:\") + renderNestedValue(nestedValue, indent + DETAIL_INDENT_STEP)\n              else -> listOf(\"$prefix$key: ${formatValuePlain(nestedValue)}\")\n            }\n          }\n        }\n      }\n\n      is Collection<*> -> {\n        if (value.isEmpty()) {\n          listOf(\"$prefix[]\")\n        } else {\n          value.flatMapIndexed { index, item ->\n            when (item) {\n              is Map<*, *>, is Collection<*> -> listOf(\"$prefix[$index]\") + renderNestedValue(item, indent + DETAIL_INDENT_STEP)\n              else -> listOf(\"$prefix[$index] ${formatValuePlain(item)}\")\n            }\n          }\n        }\n      }\n\n      else -> {\n        listOf(\"${prefix}${sanitize(value.toString())}\")\n      }\n    }\n  }\n\n  private fun styleForSystem(system: String): TextStyle {\n    val palette = listOf(brightBlue, brightMagenta, brightCyan, brightGreen, brightYellow)\n    val index = (system.lowercase().hashCode() and Int.MAX_VALUE) % palette.size\n    return palette[index]\n  }\n\n  private fun styleSummaryLine(line: String): String {\n    val lower = line.lowercase()\n    val number = extractLastNumber(lower)\n\n    return when {\n      \"failed\" in lower -> if ((number ?: 0) == 0) brightGreen(line) else brightRed(line)\n      \"passed\" in lower || \"success\" in lower -> brightGreen(line)\n      \"consumed\" in lower || \"produced\" in lower || \"published\" in lower || \"registered\" in lower || \"served\" in lower -> brightCyan(line)\n      else -> white(line)\n    }\n  }\n\n  private fun styleCollectionCount(key: String, size: Int): String {\n    val lower = key.lowercase()\n    return when {\n      \"fail\" in lower -> if (size == 0) brightGreen(\"0 item(s)\") else brightRed(\"$size item(s)\")\n      \"pass\" in lower || \"success\" in lower -> brightGreen(\"$size item(s)\")\n      else -> brightCyan(\"$size item(s)\")\n    }\n  }\n\n  private fun styleSnapshotValue(key: String, value: Any?): String {\n    val lower = key.lowercase()\n\n    return when (value) {\n      is Number -> {\n        val intValue = value.toInt()\n        when {\n          \"fail\" in lower -> if (intValue == 0) brightGreen(value.toString()) else brightRed(value.toString())\n          \"pass\" in lower || \"success\" in lower -> brightGreen(value.toString())\n          else -> brightYellow(value.toString())\n        }\n      }\n\n      is Boolean -> {\n        if (value) brightGreen(\"true\") else brightRed(\"false\")\n      }\n\n      else -> {\n        formatValuePlain(value)\n      }\n    }\n  }\n\n  private fun formatTimestamp(entry: ReportEntry): String =\n    entry.timestamp\n      .atZone(ZoneId.systemDefault())\n      .format(timeFormatter)\n\n  private fun extractLastNumber(value: String): Int? =\n    Regex(\"(\\\\d+)(?!.*\\\\d)\")\n      .find(value)\n      ?.groupValues\n      ?.getOrNull(1)\n      ?.toIntOrNull()\n\n  private fun formatValuePlain(value: Any?): String = when (value) {\n    null -> \"none\"\n    is Option<*> -> value.getOrElse { null }?.let(::formatValuePlain) ?: \"none\"\n    is String -> sanitize(value)\n    is Number, is Boolean -> value.toString()\n    is Collection<*> -> renderCollection(value)\n    is Map<*, *> -> renderMap(value)\n    else -> sanitize(value.toString())\n  }\n\n  private fun renderCollection(value: Collection<*>): String {\n    if (value.isEmpty()) return \"[]\"\n\n    val printable = value.take(VALUE_PREVIEW_LIMIT)\n    return printable.joinToString(\", \", prefix = \"[\", postfix = if (value.size > VALUE_PREVIEW_LIMIT) \", ...]\" else \"]\") {\n      formatValuePlain(it)\n    }\n  }\n\n  private fun renderMap(value: Map<*, *>): String {\n    if (value.isEmpty()) return \"{}\"\n\n    val printable = value.entries.take(VALUE_PREVIEW_LIMIT)\n    return printable.joinToString(\", \", prefix = \"{\", postfix = if (value.size > VALUE_PREVIEW_LIMIT) \", ...}\" else \"}\") {\n      \"${it.key}=${formatValuePlain(it.value)}\"\n    }\n  }\n\n  private fun sanitize(value: String): String = value.replace(\"\\r\", \"\")\n\n  private fun visibleLength(value: String): Int = stripAnsi(value).length\n\n  private fun stripAnsi(value: String): String = value.replace(Regex(\"\\u001B\\\\[[0-9;]*m\"), \"\")\n\n  private fun wrapTextBlock(text: String, width: Int): String =\n    text.lines().flatMap { wrapLine(it, width) }.joinToString(\"\\n\")\n\n  private fun wrapLine(line: String, width: Int): List<String> {\n    if (line.isEmpty() || visibleLength(line) <= width) return listOf(line)\n\n    val plain = stripAnsi(line)\n    val continuationIndent = buildContinuationIndent(plain)\n    val wrapped = mutableListOf<String>()\n    var remaining = line\n    var remainingWidth = width\n    var firstLine = true\n\n    while (visibleLength(remaining) > remainingWidth) {\n      val plainRemaining = stripAnsi(remaining)\n      val breakAt = findWrapPosition(plainRemaining, remainingWidth)\n      val rawBreakAt = rawIndexForVisibleIndex(remaining, breakAt)\n      wrapped += remaining.substring(0, rawBreakAt).trimEnd()\n\n      val nextRawStart = rawIndexAfterLeadingWhitespace(remaining, rawBreakAt)\n      remaining = \" \".repeat(continuationIndent) + remaining.substring(nextRawStart)\n      remainingWidth = (width - continuationIndent).coerceAtLeast(MIN_BREAK_SEARCH_WINDOW)\n      firstLine = false\n\n      if (!firstLine && visibleLength(remaining) <= width) {\n        remainingWidth = width\n      }\n    }\n\n    wrapped += remaining\n    return wrapped\n  }\n\n  private fun buildContinuationIndent(line: String): Int {\n    val leadingSpaces = line.takeWhile { it == ' ' }.length\n    val content = line.drop(leadingSpaces)\n    val labelIndex = content.indexOf(\": \")\n\n    return when {\n      labelIndex in 1..LABEL_WRAP_INDENT_LIMIT -> leadingSpaces + labelIndex + 2\n      content.startsWith(\"[\") -> leadingSpaces + DETAIL_INDENT_STEP\n      else -> leadingSpaces + DETAIL_INDENT_STEP\n    }\n  }\n\n  private fun findWrapPosition(line: String, width: Int): Int {\n    val softBreakStart = width.coerceAtLeast(MIN_BREAK_SEARCH_WINDOW)\n\n    for (index in width downTo softBreakStart) {\n      val previous = line.getOrNull(index - 1)\n      val current = line.getOrNull(index)\n\n      if (previous != null && isWrapDelimiter(previous)) return index\n      if (current != null && current.isWhitespace()) return index\n    }\n\n    return width.coerceAtMost(line.length)\n  }\n\n  private fun isWrapDelimiter(char: Char): Boolean =\n    char.isWhitespace() || char in charArrayOf(',', ';', ')', ']', '}', '/', '_')\n\n  private fun rawIndexForVisibleIndex(line: String, visibleIndex: Int): Int {\n    var rawIndex = 0\n    var visibleCount = 0\n\n    while (rawIndex < line.length && visibleCount < visibleIndex) {\n      if (line[rawIndex] == '\\u001B') {\n        rawIndex = advancePastAnsi(line, rawIndex)\n      } else {\n        rawIndex++\n        visibleCount++\n      }\n    }\n\n    return rawIndex\n  }\n\n  private fun rawIndexAfterLeadingWhitespace(line: String, startIndex: Int): Int {\n    var rawIndex = startIndex\n    while (rawIndex < line.length) {\n      if (line[rawIndex] == '\\u001B') {\n        rawIndex = advancePastAnsi(line, rawIndex)\n      } else if (line[rawIndex].isWhitespace()) {\n        rawIndex++\n      } else {\n        break\n      }\n    }\n    return rawIndex\n  }\n\n  private fun advancePastAnsi(line: String, startIndex: Int): Int {\n    var rawIndex = startIndex + 1\n    while (rawIndex < line.length && line[rawIndex] != 'm') rawIndex++\n    return (rawIndex + 1).coerceAtMost(line.length)\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/ReportEntry.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport arrow.core.None\nimport arrow.core.Option\nimport arrow.core.Some\nimport arrow.core.toOption\nimport com.trendyol.stove.tracing.TraceContext\nimport com.trendyol.stove.tracing.TraceVisualization\nimport java.time.Instant\n\n/**\n * Represents an action performed during test execution with its result.\n *\n * Every test operation is an action with an outcome:\n * - If it completes successfully → PASSED\n * - If it throws or fails assertion → FAILED\n *\n * Uses Arrow's Option monad for optional fields - no nullability.\n */\ndata class ReportEntry(\n  val timestamp: Instant,\n  val system: String,\n  val testId: String,\n  val action: String,\n  val result: AssertionResult = AssertionResult.PASSED,\n  val input: Option<Any> = None,\n  val output: Option<Any> = None,\n  val metadata: Map<String, Any> = emptyMap(),\n  val expected: Option<Any> = None,\n  val actual: Option<Any> = None,\n  val error: Option<String> = None,\n  val traceId: Option<String> = None,\n  val executionTrace: Option<TraceVisualization> = None\n) {\n  val summary: String get() = \"[$system] $action\"\n  val isFailed: Boolean get() = result == AssertionResult.FAILED\n  val isPassed: Boolean get() = result == AssertionResult.PASSED\n  val hasTrace: Boolean get() = traceId.isSome()\n\n  companion object {\n    private fun now(): Instant = Instant.now()\n\n    /**\n     * Creates a successful action entry.\n     */\n    fun success(\n      system: String,\n      testId: String,\n      action: String,\n      input: Option<Any> = None,\n      output: Option<Any> = None,\n      metadata: Map<String, Any> = emptyMap(),\n      traceId: Option<String> = TraceContext.current()?.traceId.toOption()\n    ): ReportEntry = ReportEntry(\n      timestamp = now(),\n      system = system,\n      testId = testId,\n      action = action,\n      result = AssertionResult.PASSED,\n      input = input,\n      output = output,\n      metadata = metadata,\n      traceId = traceId\n    )\n\n    /**\n     * Creates an action entry with explicit result.\n     */\n    fun action(\n      system: String,\n      testId: String,\n      action: String,\n      passed: Boolean,\n      input: Option<Any> = None,\n      output: Option<Any> = None,\n      metadata: Map<String, Any> = emptyMap(),\n      expected: Option<Any> = None,\n      actual: Option<Any> = None,\n      error: Option<String> = None,\n      traceId: Option<String> = TraceContext.current()?.traceId.toOption(),\n      executionTrace: Option<TraceVisualization> = None\n    ): ReportEntry = ReportEntry(\n      timestamp = now(),\n      system = system,\n      testId = testId,\n      action = action,\n      result = AssertionResult.of(passed),\n      input = input,\n      output = output,\n      metadata = metadata,\n      expected = expected,\n      actual = actual,\n      error = error,\n      traceId = traceId,\n      executionTrace = executionTrace\n    )\n\n    /**\n     * Creates a failed action entry.\n     */\n    fun failure(\n      system: String,\n      testId: String,\n      action: String,\n      error: String,\n      input: Option<Any> = None,\n      output: Option<Any> = None,\n      metadata: Map<String, Any> = emptyMap(),\n      expected: Option<Any> = None,\n      actual: Option<Any> = None,\n      traceId: Option<String> = TraceContext.current()?.traceId.toOption()\n    ): ReportEntry = ReportEntry(\n      timestamp = now(),\n      system = system,\n      testId = testId,\n      action = action,\n      result = AssertionResult.FAILED,\n      input = input,\n      output = output,\n      metadata = metadata,\n      expected = expected,\n      actual = actual,\n      error = Some(error),\n      traceId = traceId\n    )\n  }\n}\n\n/**\n * Result of an action/assertion.\n */\nenum class AssertionResult {\n  PASSED,\n  FAILED;\n\n  companion object {\n    fun of(passed: Boolean): AssertionResult = if (passed) PASSED else FAILED\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/ReportEventListener.kt",
    "content": "package com.trendyol.stove.reporting\n\n/**\n * Listener for report lifecycle events.\n *\n * Implementors receive callbacks when tests start, end, and when report entries are recorded.\n * All methods have default no-op implementations — override only what you need.\n *\n * Methods are non-suspending. Implementors should dispatch async work internally\n * if needed — the reporter will not wait.\n */\ninterface ReportEventListener {\n  fun onTestStarted(ctx: StoveTestContext) {}\n  fun onTestFailed(testId: String, error: String) {}\n  fun onTestEnded(testId: String) {}\n  fun onEntryRecorded(entry: ReportEntry) {}\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/ReportRenderer.kt",
    "content": "package com.trendyol.stove.reporting\n\n/**\n * Interface for rendering test reports in different formats.\n */\ninterface ReportRenderer {\n  /**\n   * Render a test report with optional system snapshots.\n   */\n  fun render(report: TestReport, snapshots: List<SystemSnapshot>): String\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/Reports.kt",
    "content": "@file:Suppress(\"TooGenericExceptionCaught\")\n\npackage com.trendyol.stove.reporting\n\nimport arrow.core.None\nimport arrow.core.Option\nimport arrow.core.Some\nimport arrow.core.toOption\nimport com.trendyol.stove.system.abstractions.PluggedSystem\nimport com.trendyol.stove.tracing.TraceContext\nimport com.trendyol.stove.tracing.TraceVisualization\n\n/**\n * Interface for systems that participate in test reporting.\n *\n * Provides recording capabilities for actions during test execution.\n * Every action has an implicit or explicit result (PASSED/FAILED).\n *\n * ## Design Principles\n * - **Functional**: Uses Option monad, immutable data, no nullability\n * - **Simple**: Single entry type for all operations\n * - **Composable**: `record` combines action recording with execution\n */\ninterface Reports {\n  /**\n   * System identifier for reports. Defaults to class name without \"System\" suffix.\n   * Override only when custom naming is needed (e.g., \"HTTP\" instead of \"Http\").\n   */\n  val reportSystemName: String\n    get() = this::class.simpleName?.removeSuffix(\"System\") ?: \"Unknown\"\n\n  /**\n   * Access to the reporter. Requires implementing class to be a [PluggedSystem].\n   */\n  val reporter: StoveReporter\n    get() = (this as? PluggedSystem)?.stove?.reporter\n      ?: error(\"Reports must be implemented by a PluggedSystem\")\n\n  /**\n   * Capture current system state for failure reports.\n   * Override to provide system-specific snapshots (e.g., Kafka messages, WireMock stubs).\n   */\n  fun snapshot(): SystemSnapshot = SystemSnapshot(\n    system = reportSystemName,\n    state = emptyMap(),\n    summary = \"No detailed state available\"\n  )\n\n  /**\n   * Execute an action and report the result.\n   *\n   * This is the preferred method for actions that include assertions.\n   * It handles success/failure reporting automatically and re-throws on failure.\n   *\n   * @param action Description of the action being performed\n   * @param input Optional input data for the action\n   * @param output Optional output data (if not provided, block result is used)\n   * @param metadata Additional metadata for the report entry\n   * @param expected Optional expected result description\n   * @param actual Optional actual result description\n   * @param block The block to execute\n   *\n   * Example:\n   * ```kotlin\n   * report(\n   *   action = \"GET /users/123\",\n   *   input = Some(queryParams),\n   *   expected = Some(\"200 OK\")\n   * ) {\n   *   response.status shouldBe 200\n   * }\n   * ```\n   */\n  suspend fun <T> report(\n    action: String,\n    input: Option<Any> = None,\n    output: Option<Any> = None,\n    metadata: Map<String, Any> = emptyMap(),\n    expected: Option<Any> = None,\n    actual: Option<Any> = None,\n    block: suspend () -> T\n  ): T {\n    if (!reporter.isEnabled) return block()\n\n    return try {\n      val result = block()\n      val finalOutput = output.fold({ result.toOption() }, { Some(it) })\n      reporter.record(\n        ReportEntry.action(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = action,\n          passed = true,\n          input = input,\n          output = finalOutput,\n          metadata = metadata,\n          expected = expected,\n          actual = actual,\n          traceId = TraceContext.current()?.traceId.toOption()\n        )\n      )\n      result\n    } catch (e: Throwable) {\n      // Try to attach trace visualization if tracing system is available\n      val executionTrace = tryAttachTraceVisualization()\n\n      reporter.record(\n        ReportEntry.action(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = action,\n          passed = false,\n          input = input,\n          output = output,\n          metadata = metadata,\n          expected = expected,\n          actual = actual,\n          error = e.message.toOption(),\n          traceId = TraceContext.current()?.traceId.toOption(),\n          executionTrace = executionTrace\n        )\n      )\n      throw e\n    }\n  }\n\n  /**\n   * Try to attach trace visualization from the tracing system if available.\n   * No reflection needed - uses TraceProvider interface.\n   *\n   * For failure cases, we wait longer (2 seconds) to ensure spans are exported,\n   * especially when exceptions are thrown immediately.\n   */\n  private fun tryAttachTraceVisualization(): Option<TraceVisualization> {\n    val stove = (this as? PluggedSystem)?.stove ?: return None\n\n    // Find any system that implements TraceProvider\n    val traceProvider = stove.systemsOf<TraceProvider>()\n      .firstOrNull() ?: return None\n\n    // Wait longer for failures (2s) since exceptions might interrupt span export\n    return traceProvider.getTraceVisualizationForCurrentTest(waitTimeMs = 2000)\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/SpanEventListener.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport com.trendyol.stove.tracing.SpanInfo\n\n/**\n * Listener for span recording events.\n *\n * Receives callbacks when spans are recorded by [com.trendyol.stove.tracing.StoveTraceCollector].\n * Default no-op implementation — override only what you need.\n */\ninterface SpanEventListener {\n  fun onSpanRecorded(span: SpanInfo) {}\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/SpanListenerRegistry.kt",
    "content": "package com.trendyol.stove.reporting\n\n/**\n * Interface for systems that accept span event listeners.\n * Lives in the core module so that other modules (e.g. dashboard) can\n * register listeners without depending on the tracing module directly.\n */\ninterface SpanListenerRegistry {\n  fun addSpanListener(listener: SpanEventListener)\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/StoveReporter.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport com.trendyol.stove.system.Stove\nimport java.util.concurrent.*\n\n/**\n * Central reporter that manages test reports and context.\n * Thread-safe for concurrent test execution.\n *\n * ## Design\n * - Each test gets its own [TestReport] container\n * - Test context is resolved from [StoveTestContextHolder] (ThreadLocal) or internal context\n * - Snapshots are collected from all systems implementing [Reports]\n */\nclass StoveReporter(\n  val isEnabled: Boolean = true\n) {\n  private val logger = org.slf4j.LoggerFactory.getLogger(StoveReporter::class.java)\n  private val reports = ConcurrentHashMap<String, TestReport>()\n  private val contextThreadLocal = ThreadLocal<String>()\n  private val listeners = CopyOnWriteArrayList<ReportEventListener>()\n\n  /** Register a listener to receive report events */\n  fun addListener(listener: ReportEventListener) {\n    listeners.add(listener)\n  }\n\n  /** Remove a previously registered listener */\n  fun removeListener(listener: ReportEventListener) {\n    listeners.remove(listener)\n  }\n\n  /** Start tracking a new test */\n  fun startTest(ctx: StoveTestContext) {\n    contextThreadLocal.set(ctx.testId)\n    reports.computeIfAbsent(ctx.testId) { TestReport(ctx.testId, ctx.testName) }\n    listeners.forEach {\n      runCatching { it.onTestStarted(ctx) }.onFailure { e -> logger.warn(\"Listener failed on onTestStarted\", e) }\n    }\n  }\n\n  /** Mark the current test as failed */\n  fun reportFailure(error: String) {\n    val testId = resolveTestId() ?: return\n    listeners.forEach {\n      runCatching { it.onTestFailed(testId, error) }.onFailure { e -> logger.warn(\"Listener failed on onTestFailed\", e) }\n    }\n  }\n\n  /** End tracking the current test */\n  fun endTest() {\n    val testId = resolveTestId()\n    try {\n      if (testId != null) {\n        listeners.forEach {\n          runCatching { it.onTestEnded(testId) }.onFailure { e -> logger.warn(\"Listener failed on onTestEnded\", e) }\n        }\n      }\n    } finally {\n      contextThreadLocal.remove()\n    }\n  }\n\n  /** Record an entry in the current test's report */\n  fun record(entry: ReportEntry) {\n    if (!isEnabled) return\n    currentTest().record(entry)\n    listeners.forEach {\n      runCatching { it.onEntryRecorded(entry) }.onFailure { e -> logger.warn(\"Listener failed on onEntryRecorded\", e) }\n    }\n  }\n\n  /** Get report for current test, creating if needed */\n  fun currentTest(): TestReport =\n    reports.computeIfAbsent(currentTestId()) { TestReport(it, it) }\n\n  /** Get report for current test if it exists */\n  fun currentTestOrNull(): TestReport? =\n    resolveTestId()?.let { reports[it] }\n\n  /** Get current test ID */\n  fun currentTestId(): String =\n    resolveTestId() ?: DEFAULT_TEST_ID\n\n  /** Check if current test has failures */\n  fun hasFailures(): Boolean =\n    currentTestOrNull()?.hasFailures() == true\n\n  /** Clear current test report */\n  fun clear(): Unit = resolveTestId()?.let(::clear) ?: Unit\n\n  /** Clear report for the specified test ID */\n  fun clear(testId: String): Unit = reports.remove(testId)?.clear() ?: Unit\n\n  /** Render report using specified renderer */\n  fun dump(renderer: ReportRenderer): String =\n    currentTestOrNull()?.let { renderer.render(it, collectSnapshots()) } ?: \"\"\n\n  /** Render report only if there are failures */\n  fun dumpIfFailed(renderer: ReportRenderer = PrettyConsoleRenderer): String =\n    currentTestOrNull()\n      ?.takeIf { it.hasFailures() }\n      ?.let { renderer.render(it, collectSnapshots()) }\n      ?: \"\"\n\n  /** Print report to console only if there are failures */\n  fun printIfFailed(renderer: ReportRenderer = PrettyConsoleRenderer): Unit =\n    dumpIfFailed(renderer).takeIf { it.isNotEmpty() }?.let(::println) ?: Unit\n\n  /** Collect snapshots from all reporting systems */\n  fun collectSnapshots(): List<SystemSnapshot> = runCatching {\n    if (Stove.instanceInitialized()) {\n      Stove.instance.systemsOf<Reports>()\n        .map { it.snapshot() }\n    } else {\n      emptyList()\n    }\n  }.getOrDefault(emptyList())\n\n  private fun resolveTestId(): String? = StoveTestContextHolder.get()?.testId ?: contextThreadLocal.get()\n\n  companion object {\n    private const val DEFAULT_TEST_ID = \"default\"\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/StoveTestContext.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlin.coroutines.AbstractCoroutineContextElement\nimport kotlin.coroutines.CoroutineContext\n\n/**\n * Coroutine context element that identifies the current test.\n * Used by Kotest to correlate report entries with the test that generated them.\n */\ndata class StoveTestContext(\n    val testId: String,\n    val testName: String,\n    val specName: String? = null,\n    val testPath: List<String> = emptyList()\n) : AbstractCoroutineContextElement(Key) {\n    companion object Key : CoroutineContext.Key<StoveTestContext>\n}\n\n/**\n * Extension function to get the current test context from the coroutine context.\n */\nsuspend fun currentStoveTestContext(): StoveTestContext? =\n    currentCoroutineContext()[StoveTestContext]\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/StoveTestContextHolder.kt",
    "content": "package com.trendyol.stove.reporting\n\n/**\n * Thread-local holder for test context.\n * Used by JUnit 5 (which uses threads) to correlate report entries with tests.\n */\nobject StoveTestContextHolder {\n  private val threadLocalContext = ThreadLocal<StoveTestContext>()\n\n  /**\n   * Set the current test context for this thread.\n   */\n  fun set(context: StoveTestContext) = threadLocalContext.set(context)\n\n  /**\n   * Get the current test context for this thread, if any.\n   */\n  fun get(): StoveTestContext? = threadLocalContext.get()\n\n  /**\n   * Clear the test context for this thread.\n   */\n  fun clear() = threadLocalContext.remove()\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/StoveTestExceptions.kt",
    "content": "package com.trendyol.stove.reporting\n\n/**\n * Exception that wraps test assertion failures with Stove's execution report.\n * The report is included in the exception message for display by test engines.\n *\n * Preserves the original exception's stack trace so test frameworks show the actual failure location.\n */\nclass StoveTestFailureException(\n  originalMessage: String,\n  stoveReport: String,\n  cause: Throwable? = null\n) : AssertionError(buildStoveReportMessage(originalMessage, stoveReport), cause) {\n  init {\n    // Copy the original stack trace to show the actual failure location\n    cause?.let { stackTrace = it.stackTrace }\n  }\n}\n\n/**\n * Exception that wraps test errors with Stove's execution report.\n * The report is included in the exception message for display by test engines.\n *\n * Preserves the original exception's stack trace so test frameworks show the actual failure location.\n */\nclass StoveTestErrorException(\n  originalMessage: String,\n  stoveReport: String,\n  cause: Throwable? = null\n) : Exception(buildStoveReportMessage(originalMessage, stoveReport), cause) {\n  init {\n    // Copy the original stack trace to show the actual failure location\n    cause?.let { stackTrace = it.stackTrace }\n  }\n}\n\nprivate fun buildStoveReportMessage(\n  originalMessage: String,\n  stoveReport: String\n): String = \"\"\"\n  |$originalMessage\n  |\n  |${formatStoveReport(stoveReport)}\n\"\"\".trimMargin()\n\nprivate fun formatStoveReport(stoveReport: String): String {\n  if (stoveReport.isBlank()) return \"\"\n\n  return if (hasReportHeader(stoveReport)) {\n    stoveReport\n  } else {\n    \"\"\"\n    |═══════════════════════════════════════════════════════════════════════════════\n    |                         STOVE EXECUTION REPORT\n    |═══════════════════════════════════════════════════════════════════════════════\n    |\n    |$stoveReport\n    \"\"\".trimMargin()\n  }\n}\n\nprivate fun hasReportHeader(stoveReport: String): Boolean {\n  val plain = stoveReport.replace(Regex(\"\\u001B\\\\[[0-9;]*m\"), \"\")\n  return plain.contains(\"STOVE EXECUTION REPORT\") || plain.contains(\"STOVE TEST EXECUTION REPORT\")\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/SystemSnapshot.kt",
    "content": "package com.trendyol.stove.reporting\n\n/**\n * Snapshot of a system's state at a point in time.\n * Used for debugging test failures by providing context about what the system was doing.\n *\n * Systems with rich internal state (like Kafka's MessageStore or WireMock's stubs)\n * should override [Reports.snapshot] to provide detailed state information.\n */\ndata class SystemSnapshot(\n  val system: String,\n  val state: Map<String, Any>,\n  val summary: String\n)\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/TestReport.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport java.util.concurrent.ConcurrentLinkedQueue\n\n/**\n * Container for report entries belonging to a single test.\n * Thread-safe for concurrent recording during test execution.\n *\n * Exposes only immutable views of data through public APIs.\n */\nclass TestReport(\n  val testId: String,\n  val testName: String\n) {\n  private val queue: ConcurrentLinkedQueue<ReportEntry> = ConcurrentLinkedQueue()\n\n  /** Record a new entry. Thread-safe. */\n  fun record(entry: ReportEntry): Unit = queue.add(entry).let { }\n\n  /** All entries as immutable list */\n  fun entries(): List<ReportEntry> = queue.toList()\n\n  /** Entries for this test only */\n  fun entriesForThisTest(): List<ReportEntry> = queue.filter { it.testId == testId }\n\n  /** All failed entries */\n  fun failures(): List<ReportEntry> = queue.filter { it.isFailed }\n\n  /** Failed entries for this test */\n  fun failuresForThisTest(): List<ReportEntry> = entriesForThisTest().filter { it.isFailed }\n\n  /** True if any failures exist */\n  fun hasFailures(): Boolean = queue.any { it.isFailed }\n\n  /** Clear all entries */\n  fun clear(): Unit = queue.clear()\n}\n\n// ============================================================================\n// Extension Functions for List<ReportEntry>\n// Functional-style filtering operations\n// ============================================================================\n\n/** Filter entries by system name */\nfun List<ReportEntry>.forSystem(system: String): List<ReportEntry> =\n  filter { it.system == system }\n\n/** Filter entries by test ID */\nfun List<ReportEntry>.forTest(testId: String): List<ReportEntry> =\n  filter { it.testId == testId }\n\n/** Get only failed entries */\nfun List<ReportEntry>.failures(): List<ReportEntry> =\n  filter { it.isFailed }\n\n/** Get only passed entries */\nfun List<ReportEntry>.passed(): List<ReportEntry> =\n  filter { it.isPassed }\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/reporting/TraceProvider.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport arrow.core.Option\nimport com.trendyol.stove.tracing.TraceVisualization\n\n/**\n * Interface for systems that can provide execution trace information.\n * Implemented by the tracing system to avoid circular dependencies.\n */\ninterface TraceProvider {\n  /**\n   * Gets trace visualization for the current test context.\n   * Returns None if no traces are available.\n   *\n   * @param waitTimeMs How long to wait for spans to be exported (default 300ms)\n   */\n  fun getTraceVisualizationForCurrentTest(waitTimeMs: Long = 300): Option<TraceVisualization>\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/serialization/gson.kt",
    "content": "package com.trendyol.stove.serialization\n\nimport com.google.gson.Gson\n\nobject StoveGson {\n  val default: Gson = com.google.gson\n    .GsonBuilder()\n    .create()\n\n  fun byConfiguring(\n    configurer: com.google.gson.GsonBuilder.() -> com.google.gson.GsonBuilder\n  ): Gson = configurer(com.google.gson.GsonBuilder()).create()\n\n  fun anyJsonStringSerde(gson: Gson = default): StoveSerde<Any, String> = StoveGsonStringSerializer(gson)\n\n  fun anyByteArraySerde(gson: Gson = default): StoveSerde<Any, ByteArray> = StoveGsonByteArraySerializer(gson)\n}\n\nclass StoveGsonStringSerializer<TIn : Any>(\n  private val gson: Gson\n) : StoveSerde<TIn, String> {\n  override fun serialize(value: TIn): String = gson.toJson(value)\n\n  override fun <T : TIn> deserialize(value: String, clazz: Class<T>): T = gson.fromJson(value, clazz)\n}\n\nclass StoveGsonByteArraySerializer<TIn : Any>(\n  private val gson: Gson\n) : StoveSerde<TIn, ByteArray> {\n  override fun serialize(value: TIn): ByteArray = gson.toJson(value).toByteArray()\n\n  override fun <T : TIn> deserialize(value: ByteArray, clazz: Class<T>): T = gson.fromJson(value.toString(Charsets.UTF_8), clazz)\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/serialization/jackson.kt",
    "content": "package com.trendyol.stove.serialization\n\nimport com.fasterxml.jackson.annotation.JsonInclude\nimport com.fasterxml.jackson.core.*\nimport com.fasterxml.jackson.databind.*\nimport com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS\nimport com.fasterxml.jackson.databind.json.JsonMapper\nimport com.fasterxml.jackson.databind.module.SimpleModule\nimport com.fasterxml.jackson.module.kotlin.*\nimport com.trendyol.stove.functional.*\nimport java.time.Instant\nimport java.time.format.DateTimeFormatter\nimport java.time.temporal.TemporalAccessor\n\nobject StoveJackson {\n  val default: ObjectMapper = jacksonObjectMapper().disable(FAIL_ON_EMPTY_BEANS).apply {\n    findAndRegisterModules()\n  }\n\n  fun byConfiguring(\n    configurer: JsonMapper.Builder.() -> Unit\n  ): ObjectMapper = JsonMapper.builder(default.factory).apply(configurer).build()\n\n  fun anyByteArraySerde(objectMapper: ObjectMapper = default): StoveSerde<Any, ByteArray> = StoveJacksonByteArraySerializer(objectMapper)\n\n  fun anyJsonStringSerde(objectMapper: ObjectMapper = default): StoveSerde<Any, String> = StoveJacksonStringSerializer(objectMapper)\n}\n\nclass StoveJacksonStringSerializer<TIn : Any>(\n  private val objectMapper: ObjectMapper\n) : StoveSerde<TIn, String> {\n  override fun serialize(value: TIn): String = objectMapper.writeValueAsString(value) as String\n\n  override fun <T : TIn> deserialize(value: String, clazz: Class<T>): T = objectMapper.readValue(value, clazz)\n}\n\nclass StoveJacksonByteArraySerializer<TIn : Any>(\n  private val objectMapper: ObjectMapper\n) : StoveSerde<TIn, ByteArray> {\n  override fun serialize(value: TIn): ByteArray = objectMapper.writeValueAsBytes(value)\n\n  override fun <T : TIn> deserialize(value: ByteArray, clazz: Class<T>): T = objectMapper.readValue(value, clazz)\n}\n\n/**\n * This class is used to create an object mapper with default configurations.\n * This object mapper is used to serialize and deserialize request and response bodies.\n */\nobject E2eObjectMapperConfig {\n  /**\n   * Creates an object mapper with default configurations.\n   * This object mapper is used to serialize and deserialize request and response bodies.\n   */\n  fun createObjectMapperWithDefaults(): ObjectMapper {\n    val isoInstantModule = SimpleModule()\n      .addSerializer(Instant::class.java, IsoInstantSerializer())\n      .addDeserializer(Instant::class.java, IsoInstantDeserializer())\n\n    return JsonMapper\n      .builder()\n      .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)\n      .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)\n      .defaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL))\n      .build()\n      .registerKotlinModule()\n      .registerModule(isoInstantModule)\n  }\n}\n\n/**\n * Instant serializer deserializer for jackson\n */\nclass IsoInstantDeserializer : JsonDeserializer<Instant>() {\n  override fun deserialize(\n    parser: JsonParser,\n    context: DeserializationContext\n  ): Instant {\n    val string: String = parser.text.trim()\n    return Try {\n      DateTimeFormatter.ISO_INSTANT.parse(string) { temporal: TemporalAccessor ->\n        Instant.from(temporal)\n      } as Instant\n    }.recover { Instant.ofEpochSecond(string.toLong()) }.get()\n  }\n}\n\n/**\n * Instant serializer for jackson\n */\nclass IsoInstantSerializer : JsonSerializer<Instant>() {\n  override fun serialize(\n    value: Instant,\n    gen: JsonGenerator,\n    serializers: SerializerProvider?\n  ) {\n    gen.writeString(value.toString())\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/serialization/kotlinx.kt",
    "content": "package com.trendyol.stove.serialization\n\nimport kotlinx.serialization.*\nimport kotlinx.serialization.json.*\nimport java.io.ByteArrayOutputStream\n\nobject StoveKotlinx {\n  val default: Json = Json {\n    ignoreUnknownKeys = true\n    encodeDefaults = true\n    isLenient = true\n    explicitNulls = false\n  }\n\n  fun byConfiguring(configurer: JsonBuilder.() -> Unit): Json = Json(default) { configurer() }\n\n  fun anyJsonStringSerde(json: Json = default): StoveSerde<Any, String> = StoveKotlinxStringSerializer(json)\n\n  fun anyByteArraySerde(json: Json = default): StoveSerde<Any, ByteArray> = StoveKotlinxByteArraySerializer(json)\n}\n\n@Suppress(\"UNCHECKED_CAST\")\nclass StoveKotlinxStringSerializer<TIn : Any>(\n  private val json: Json\n) : StoveSerde<TIn, String> {\n  override fun serialize(value: TIn): String {\n    value as Any\n    return json.encodeToString(serializer(value::class.java), value)\n  }\n\n  override fun <T : TIn> deserialize(value: String, clazz: Class<T>): T = json.decodeFromString(serializer(clazz), value) as T\n}\n\nclass StoveKotlinxByteArraySerializer(\n  private val json: Json\n) : StoveSerde<Any, ByteArray> {\n  @OptIn(ExperimentalSerializationApi::class)\n  override fun serialize(value: Any): ByteArray = ByteArrayOutputStream().use { stream ->\n    json.encodeToStream(serializer(value::class.java), value, stream)\n    stream.toByteArray()\n  }\n\n  @OptIn(ExperimentalSerializationApi::class)\n  @Suppress(\"UNCHECKED_CAST\")\n  override fun <T : Any> deserialize(value: ByteArray, clazz: Class<T>): T =\n    json.decodeFromStream(serializer(clazz), value.inputStream()) as T\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/serialization/serialization.kt",
    "content": "package com.trendyol.stove.serialization\n\nimport arrow.core.*\n\n/**\n * Unified serialization/deserialization interface for Stove's test infrastructure.\n *\n * Stove uses this interface internally for JSON handling in HTTP responses, Kafka messages,\n * document databases, and more. You can configure which implementation to use (Jackson, Gson,\n * or Kotlinx Serialization) to match your application's serialization setup.\n *\n * ## Available Implementations\n *\n * - [StoveSerde.jackson] - Jackson ObjectMapper (default)\n * - [StoveSerde.gson] - Google Gson\n * - [StoveSerde.kotlinx] - Kotlinx Serialization\n *\n * ## Configuration Example\n *\n * ```kotlin\n * // Configure Kafka to use the same ObjectMapper as your application\n * kafka {\n *     stoveKafkaObjectMapperRef = myApplicationObjectMapper\n *     KafkaSystemOptions { cfg ->\n *         listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n *     }\n * }\n *\n * // Configure HTTP client with custom content converter\n * httpClient {\n *     HttpClientSystemOptions(\n *         baseUrl = \"http://localhost:8080\",\n *         contentConverter = JacksonConverter(myObjectMapper)\n *     )\n * }\n * ```\n *\n * ## Custom Serde Implementation\n *\n * ```kotlin\n * object MyCustomSerde : StoveSerde<Any, String> {\n *     override fun serialize(value: Any): String = mySerialize(value)\n *\n *     override fun <T : Any> deserialize(value: String, clazz: Class<T>): T =\n *         myDeserialize(value, clazz)\n * }\n * ```\n *\n * @param TIn The base type of objects that can be serialized (typically `Any`).\n * @param TOut The serialized format type (`String` for JSON, `ByteArray` for binary).\n */\ninterface StoveSerde<TIn : Any, TOut : Any> {\n  /**\n   * Serializes an object to the target format.\n   *\n   * @param value The object to serialize.\n   * @return The serialized representation.\n   * @throws StoveSerdeProblem.BecauseOfSerialization if serialization fails.\n   */\n  fun serialize(value: TIn): TOut\n\n  /**\n   * Deserializes data into the specified type.\n   *\n   * @param value The serialized data.\n   * @param clazz The target class to deserialize into.\n   * @return The deserialized object.\n   * @throws StoveSerdeProblem.BecauseOfDeserialization if deserialization fails.\n   */\n  fun <T : TIn> deserialize(value: TOut, clazz: Class<T>): T\n\n  /**\n   * Deserializes data with error handling via [Either].\n   *\n   * Use this when you want to handle deserialization failures gracefully\n   * without exceptions.\n   *\n   * ```kotlin\n   * val result = serde.deserializeEither(json, User::class.java)\n   * result.fold(\n   *     ifLeft = { error -> println(\"Failed: ${error.message}\") },\n   *     ifRight = { user -> println(\"Success: ${user.name}\") }\n   * )\n   * ```\n   *\n   * @param value The serialized data.\n   * @param clazz The target class.\n   * @return Either a [StoveSerdeProblem] or the deserialized object.\n   */\n  fun <T : TIn> deserializeEither(value: TOut, clazz: Class<T>): Either<StoveSerdeProblem, T> = Either\n    .catch { deserialize(value, clazz) }\n    .mapLeft { StoveSerdeProblem.BecauseOfDeserialization(it.message ?: \"Deserialization failed\", it) }\n\n  /**\n   * Companion object providing default serde implementations and utility functions.\n   */\n  companion object {\n    /**\n     * Jackson-based serialization using [com.fasterxml.jackson.databind.ObjectMapper].\n     *\n     * This is the default and most commonly used implementation.\n     *\n     * ```kotlin\n     * val mapper = StoveSerde.jackson.default\n     * val json = mapper.serialize(myObject)\n     * val obj = mapper.deserialize<MyClass>(json)\n     * ```\n     */\n    val jackson = StoveJackson\n\n    /**\n     * Gson-based serialization using [com.google.gson.Gson].\n     *\n     * ```kotlin\n     * val gson = StoveSerde.gson.default\n     * val json = gson.serialize(myObject)\n     * ```\n     */\n    val gson = StoveGson\n\n    /**\n     * Kotlinx Serialization-based implementation.\n     *\n     * Requires classes to be annotated with `@Serializable`.\n     *\n     * ```kotlin\n     * @Serializable\n     * data class User(val name: String)\n     *\n     * val json = StoveSerde.kotlinx.default.serialize(user)\n     * ```\n     */\n    val kotlinx = StoveKotlinx\n\n    /**\n     * Deserializes [ByteArray] data using reified type parameter.\n     *\n     * ```kotlin\n     * val user: User = serde.deserialize(bytes)\n     * ```\n     */\n    inline fun <reified T : Any> StoveSerde<Any, ByteArray>.deserialize(\n      value: ByteArray\n    ): T = deserialize(value, T::class.java)\n\n    /**\n     * Deserializes [ByteArray] data, returning [None] on failure.\n     *\n     * ```kotlin\n     * val userOption: Option<User> = serde.deserializeOption(bytes)\n     * userOption.onSome { user -> println(user.name) }\n     * ```\n     */\n    inline fun <reified T : Any> StoveSerde<Any, ByteArray>.deserializeOption(\n      value: ByteArray\n    ): Option<T> = deserializeEither(value, T::class.java).getOrNone()\n\n    /**\n     * Deserializes [String] data using reified type parameter.\n     *\n     * ```kotlin\n     * val user: User = serde.deserialize(jsonString)\n     * ```\n     */\n    inline fun <reified T : Any> StoveSerde<Any, String>.deserialize(\n      value: String\n    ): T = deserialize(value, T::class.java)\n\n    /**\n     * Deserializes [String] data, returning [None] on failure.\n     *\n     * ```kotlin\n     * val userOption: Option<User> = serde.deserializeOption(jsonString)\n     * ```\n     */\n    inline fun <reified T : Any> StoveSerde<Any, String>.deserializeOption(value: String): Option<T> =\n      deserializeEither(value, T::class.java).getOrNone()\n  }\n\n  /**\n   * Sealed class hierarchy for serialization/deserialization errors.\n   *\n   * These exceptions provide structured error information when JSON operations fail.\n   */\n  sealed class StoveSerdeProblem(\n    message: String,\n    cause: Throwable? = null\n  ) : RuntimeException(message, cause) {\n    /**\n     * Error during serialization (object to JSON/bytes).\n     */\n    class BecauseOfSerialization(\n      message: String,\n      cause: Throwable? = null\n    ) : StoveSerdeProblem(message, cause)\n\n    /**\n     * Error during deserialization (JSON/bytes to object).\n     */\n    class BecauseOfDeserialization(\n      message: String,\n      cause: Throwable? = null\n    ) : StoveSerdeProblem(message, cause)\n\n    /**\n     * Deserialization failed but a specific type was expected.\n     *\n     * Used when asserting message types in Kafka or document types in databases.\n     */\n    class BecauseOfDeserializationButExpected(\n      message: String,\n      cause: Throwable? = null\n    ) : StoveSerdeProblem(message, cause)\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/BridgeSystem.kt",
    "content": "package com.trendyol.stove.system\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport kotlin.reflect.*\n\n/**\n * A system that provides a bridge between the test system and the application context.\n *\n * @property stove the test system to bridge.\n */\n@StoveDsl\nabstract class BridgeSystem<T : Any>(\n  override val stove: Stove\n) : PluggedSystem,\n  AfterRunAwareWithContext<T>,\n  Reports {\n  /**\n   * The application context used to resolve dependencies.\n   */\n  protected lateinit var ctx: T\n\n  /**\n   * Closes the bridge system.\n   */\n  override fun close(): Unit = Unit\n\n  /**\n   * Initializes the bridge system after the test run.\n   *\n   * @param context the application context.\n   */\n  override suspend fun afterRun(context: T) {\n    ctx = context\n  }\n\n  /**\n   * Resolves a dependency by KClass.\n   * Override this for basic type resolution without generic support.\n   */\n  abstract fun <D : Any> get(klass: KClass<D>): D\n\n  /**\n   * Resolves a dependency by KType, preserving generic type information.\n   * Override this to support generic types like List<T>, Map<K,V>, etc.\n   * Default implementation falls back to KClass-based resolution.\n   *\n   * @param type the full KType including generic parameters\n   * @return the resolved dependency\n   */\n  @Suppress(\"UNCHECKED_CAST\")\n  open fun <D : Any> getByType(type: KType): D {\n    val klass = type.classifier as? KClass<D>\n      ?: throw IllegalArgumentException(\"Cannot resolve type: $type\")\n    return get(klass)\n  }\n\n  /**\n   * Checks that the application context has been initialized.\n   * Throws with a clear error message if it hasn't (e.g., when using providedApplication()).\n   */\n  @PublishedApi\n  internal fun ensureContextInitialized() {\n    check(::ctx.isInitialized) {\n      \"BridgeSystem context is not initialized. \" +\n        \"Ensure a JVM starter (springBoot/ktor/micronaut) is configured and Stove.run() has been called. \" +\n        \"Note: providedApplication() does not support Bridge because the remote application's DI container is not accessible.\"\n    }\n  }\n\n  /**\n   * Resolves a bean of the specified type from the application context.\n   * Uses KType to preserve generic type information (e.g., List<PaymentService>).\n   *\n   * @param T the type of bean to resolve.\n   * @return the resolved bean.\n   */\n  @PublishedApi\n  internal inline fun <reified D : Any> resolve(): D = getByType(typeOf<D>())\n\n  /**\n   * Executes the specified block using the resolved bean.\n   * If you need to capture values, declare variables outside the block and assign inside.\n   *\n   * @param D the type of bean to resolve.\n   * @param block the block to execute with the resolved bean as receiver.\n   */\n  @Suppress(\"TooGenericExceptionCaught\")\n  suspend inline fun <reified D : Any> using(block: suspend D.() -> Unit) {\n    ensureContextInitialized()\n    val beanName = D::class.simpleName ?: \"Unknown\"\n    val metadata = mapOf(\"type\" to (D::class.qualifiedName ?: \"\"))\n\n    try {\n      block(resolve())\n      reporter.record(\n        ReportEntry.success(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = \"Bean usage: $beanName\",\n          metadata = metadata\n        )\n      )\n    } catch (e: Throwable) {\n      reporter.record(\n        ReportEntry.failure(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = \"Bean usage: $beanName\",\n          error = e.message ?: \"Unknown error\",\n          metadata = metadata\n        )\n      )\n      throw e\n    }\n  }\n}\n\n/**\n * Adds a bridge system to Stove and returns the modified Stove instance.\n *\n * @receiver Stove instance to modify.\n * @return the modified Stove instance.\n */\nfun <T : Any> Stove.withBridgeSystem(bridge: BridgeSystem<T>): Stove = getOrRegister(bridge).let { this }\n\n/**\n * Returns the bridge system associated with Stove.\n * This function is only available in the validation DSL.\n *\n * @receiver Stove instance.\n * @return the bridge system.\n * @throws SystemNotRegisteredException if the bridge system is not registered.\n */\n@PublishedApi\ninternal fun Stove.bridge(): BridgeSystem<*> = getOrNone<BridgeSystem<*>>().getOrElse {\n  throw SystemNotRegisteredException(BridgeSystem::class)\n}\n\n/**\n * Returns the bridge system associated with Stove.\n *\n * @receiver Stove instance.\n * @return the bridge system.\n * @throws SystemNotRegisteredException if the bridge system is not registered.\n */\nfun <T : Any> WithDsl.bridge(of: BridgeSystem<T>): Stove = this.stove.withBridgeSystem(of)\n\n/**\n * Executes the specified block using the resolved bean from the bridge system.\n * Resolved beans are using physical components of the application.\n *\n * Suggested usage: validating or preparing the application state without accessing the physical components directly.\n * If you need to capture values from inside the block, declare variables outside and assign inside:\n *\n * ```kotlin\n *  stove {\n *      // Simple assertion\n *      using<PersonService> {\n *          serviceName shouldBe \"personService\"\n *          find(userId = 123) shouldBe Person(id = 123, name = \"John Doe\")\n *      }\n *\n *      // Capturing a value for later use\n *      var userId: Long = 0\n *      using<UserRepository> {\n *          userId = save(User(name = \"John\")).id\n *      }\n *      // Use userId in subsequent operations\n *  }\n * ```\n *\n * @receiver the validation DSL.\n * @param T the type of bean to resolve.\n * @param block the block to execute with the resolved bean as receiver.\n */\nsuspend inline fun <reified T : Any> ValidationDsl.using(\n  block: @StoveDsl suspend T.() -> Unit\n): Unit = this.stove.bridge().using(block)\n\n/**\n * Executes the specified block using two resolved beans.\n *\n * @param T1 the type of the first bean to resolve.\n * @param T2 the type of the second bean to resolve.\n * @param validation the block to execute with the resolved beans.\n */\n@Suppress(\"TooGenericExceptionCaught\")\nsuspend inline fun <\n  reified T1 : Any,\n  reified T2 : Any\n  > ValidationDsl.using(\n  crossinline validation: suspend (T1, T2) -> Unit\n): Unit = stove.bridge().let { bridge ->\n  bridge.ensureContextInitialized()\n  val name1 = T1::class.simpleName ?: \"Unknown\"\n  val name2 = T2::class.simpleName ?: \"Unknown\"\n  val beanNames = \"$name1, $name2\"\n  val metadata = mapOf(\n    \"types\" to listOf(T1::class.qualifiedName, T2::class.qualifiedName)\n  )\n\n  try {\n    val t1: T1 = bridge.resolve()\n    val t2: T2 = bridge.resolve()\n    validation(t1, t2)\n    bridge.reporter.record(\n      ReportEntry.success(\n        system = bridge.reportSystemName,\n        testId = bridge.reporter.currentTestId(),\n        action = \"Bean usage: $beanNames\",\n        metadata = metadata\n      )\n    )\n  } catch (e: Throwable) {\n    bridge.reporter.record(\n      ReportEntry.failure(\n        system = bridge.reportSystemName,\n        testId = bridge.reporter.currentTestId(),\n        action = \"Bean usage: $beanNames\",\n        error = e.message ?: \"Unknown error\",\n        metadata = metadata\n      )\n    )\n    throw e\n  }\n}\n\n/**\n * Executes the specified block using three resolved beans.\n *\n * @param T1 the type of the first bean to resolve.\n * @param T2 the type of the second bean to resolve.\n * @param T3 the type of the third bean to resolve.\n * @param validation the block to execute with the resolved beans.\n */\n@Suppress(\"TooGenericExceptionCaught\")\nsuspend inline fun <\n  reified T1 : Any,\n  reified T2 : Any,\n  reified T3 : Any\n  > ValidationDsl.using(\n  crossinline validation: suspend (T1, T2, T3) -> Unit\n): Unit = stove.bridge().let { bridge ->\n  bridge.ensureContextInitialized()\n  val name1 = T1::class.simpleName ?: \"Unknown\"\n  val name2 = T2::class.simpleName ?: \"Unknown\"\n  val name3 = T3::class.simpleName ?: \"Unknown\"\n  val beanNames = \"$name1, $name2, $name3\"\n  val metadata = mapOf(\n    \"types\" to listOf(T1::class.qualifiedName, T2::class.qualifiedName, T3::class.qualifiedName)\n  )\n\n  try {\n    val t1: T1 = bridge.resolve()\n    val t2: T2 = bridge.resolve()\n    val t3: T3 = bridge.resolve()\n    validation(t1, t2, t3)\n    bridge.reporter.record(\n      ReportEntry.success(\n        system = bridge.reportSystemName,\n        testId = bridge.reporter.currentTestId(),\n        action = \"Bean usage: $beanNames\",\n        metadata = metadata\n      )\n    )\n  } catch (e: Throwable) {\n    bridge.reporter.record(\n      ReportEntry.failure(\n        system = bridge.reportSystemName,\n        testId = bridge.reporter.currentTestId(),\n        action = \"Bean usage: $beanNames\",\n        error = e.message ?: \"Unknown error\",\n        metadata = metadata\n      )\n    )\n    throw e\n  }\n}\n\n/**\n * Executes the specified block using four resolved beans.\n *\n * @param T1 the type of the first bean to resolve.\n * @param T2 the type of the second bean to resolve.\n * @param T3 the type of the third bean to resolve.\n * @param T4 the type of the fourth bean to resolve.\n * @param validation the block to execute with the resolved beans.\n */\n@Suppress(\"TooGenericExceptionCaught\")\nsuspend inline fun <\n  reified T1 : Any,\n  reified T2 : Any,\n  reified T3 : Any,\n  reified T4 : Any\n  > ValidationDsl.using(\n  crossinline validation: suspend (T1, T2, T3, T4) -> Unit\n): Unit = stove.bridge().let { bridge ->\n  bridge.ensureContextInitialized()\n  val name1 = T1::class.simpleName ?: \"Unknown\"\n  val name2 = T2::class.simpleName ?: \"Unknown\"\n  val name3 = T3::class.simpleName ?: \"Unknown\"\n  val name4 = T4::class.simpleName ?: \"Unknown\"\n  val beanNames = \"$name1, $name2, $name3, $name4\"\n  val metadata = mapOf(\n    \"types\" to listOf(T1::class.qualifiedName, T2::class.qualifiedName, T3::class.qualifiedName, T4::class.qualifiedName)\n  )\n\n  try {\n    val t1: T1 = bridge.resolve()\n    val t2: T2 = bridge.resolve()\n    val t3: T3 = bridge.resolve()\n    val t4: T4 = bridge.resolve()\n    validation(t1, t2, t3, t4)\n    bridge.reporter.record(\n      ReportEntry.success(\n        system = bridge.reportSystemName,\n        testId = bridge.reporter.currentTestId(),\n        action = \"Bean usage: $beanNames\",\n        metadata = metadata\n      )\n    )\n  } catch (e: Throwable) {\n    bridge.reporter.record(\n      ReportEntry.failure(\n        system = bridge.reportSystemName,\n        testId = bridge.reporter.currentTestId(),\n        action = \"Bean usage: $beanNames\",\n        error = e.message ?: \"Unknown error\",\n        metadata = metadata\n      )\n    )\n    throw e\n  }\n}\n\n/**\n * Executes the specified block using five resolved beans.\n *\n * @param T1 the type of the first bean to resolve.\n * @param T2 the type of the second bean to resolve.\n * @param T3 the type of the third bean to resolve.\n * @param T4 the type of the fourth bean to resolve.\n * @param T5 the type of the fifth bean to resolve.\n * @param validation the block to execute with the resolved beans.\n */\n@Suppress(\"TooGenericExceptionCaught\")\nsuspend inline fun <\n  reified T1 : Any,\n  reified T2 : Any,\n  reified T3 : Any,\n  reified T4 : Any,\n  reified T5 : Any\n  > ValidationDsl.using(\n  crossinline validation: suspend (T1, T2, T3, T4, T5) -> Unit\n): Unit = stove.bridge().let { bridge ->\n  bridge.ensureContextInitialized()\n  val name1 = T1::class.simpleName ?: \"Unknown\"\n  val name2 = T2::class.simpleName ?: \"Unknown\"\n  val name3 = T3::class.simpleName ?: \"Unknown\"\n  val name4 = T4::class.simpleName ?: \"Unknown\"\n  val name5 = T5::class.simpleName ?: \"Unknown\"\n  val beanNames = \"$name1, $name2, $name3, $name4, $name5\"\n  val metadata = mapOf(\n    \"types\" to listOf(\n      T1::class.qualifiedName,\n      T2::class.qualifiedName,\n      T3::class.qualifiedName,\n      T4::class.qualifiedName,\n      T5::class.qualifiedName\n    )\n  )\n\n  try {\n    val t1: T1 = bridge.resolve()\n    val t2: T2 = bridge.resolve()\n    val t3: T3 = bridge.resolve()\n    val t4: T4 = bridge.resolve()\n    val t5: T5 = bridge.resolve()\n    validation(t1, t2, t3, t4, t5)\n    bridge.reporter.record(\n      ReportEntry.success(\n        system = bridge.reportSystemName,\n        testId = bridge.reporter.currentTestId(),\n        action = \"Bean usage: $beanNames\",\n        metadata = metadata\n      )\n    )\n  } catch (e: Throwable) {\n    bridge.reporter.record(\n      ReportEntry.failure(\n        system = bridge.reportSystemName,\n        testId = bridge.reporter.currentTestId(),\n        action = \"Bean usage: $beanNames\",\n        error = e.message ?: \"Unknown error\",\n        metadata = metadata\n      )\n    )\n    throw e\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/PortFinder.kt",
    "content": "package com.trendyol.stove.system\n\nimport java.net.ServerSocket\n\n/**\n * Utility for finding available ports for test infrastructure.\n *\n * This is useful when running tests in parallel or when default ports\n * might already be in use.\n *\n * Usage:\n * ```kotlin\n * val port = PortFinder.findAvailablePort()\n * // or\n * val port = PortFinder.findAvailablePortFrom(50000)\n * ```\n */\nobject PortFinder {\n  private const val MAX_PORT = 65535\n  private const val MIN_PORT = 1024\n\n  /**\n   * Finds an available port by letting the OS assign one.\n   * This is the most reliable way to find an available port.\n   *\n   * @return An available port number assigned by the OS\n   */\n  @JvmStatic\n  fun findAvailablePort(): Int = ServerSocket(0).use { it.localPort }\n\n  /**\n   * Finds an available port starting from the given port number.\n   * Scans ports sequentially until an available one is found.\n   *\n   * @param startingFrom The port number to start searching from\n   * @return An available port number\n   * @throws IllegalStateException if no available port is found in the range\n   */\n  @JvmStatic\n  fun findAvailablePortFrom(startingFrom: Int): Int {\n    var port = startingFrom\n    while (port <= MAX_PORT) {\n      if (isPortAvailable(port)) {\n        return port\n      }\n      port++\n    }\n    port = MIN_PORT\n    while (port < startingFrom) {\n      if (isPortAvailable(port)) {\n        return port\n      }\n      port++\n    }\n    error(\"No available port found in range 1024-$MAX_PORT\")\n  }\n\n  /**\n   * Finds an available port and returns it as a String.\n   * Uses OS-assigned port for reliability.\n   *\n   * @return An available port number as a String\n   */\n  @JvmStatic\n  fun findAvailablePortAsString(): String = findAvailablePort().toString()\n\n  /**\n   * Finds an available port starting from the given port and returns it as a String.\n   *\n   * @param startingFrom The port number to start searching from\n   * @return An available port number as a String\n   */\n  @JvmStatic\n  fun findAvailablePortFromAsString(startingFrom: Int): String = findAvailablePortFrom(startingFrom).toString()\n\n  /**\n   * Checks if a given port is available for binding.\n   *\n   * @param port The port to check\n   * @return true if the port is available, false otherwise\n   */\n  @JvmStatic\n  fun isPortAvailable(port: Int): Boolean = try {\n    ServerSocket(port).use { true }\n  } catch (_: Exception) {\n    false\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/PropertiesFile.kt",
    "content": "package com.trendyol.stove.system\n\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\nimport java.nio.file.Path\nimport java.nio.file.Paths\nimport kotlin.io.path.*\n\nclass PropertiesFile {\n  companion object {\n    const val REUSE_ENABLED = \"testcontainers.reuse.enable=true\"\n  }\n\n  private val l: Logger = LoggerFactory.getLogger(javaClass)\n  private val propertiesFilePath: Path = Paths.get(System.getProperty(\"user.home\"), \".testcontainers.properties\")\n\n  fun detectAndLogStatus() {\n    if (propertiesFilePath.exists()) {\n      l.info(\"'.testcontainers.properties' file exists\")\n      when {\n        propertiesFilePath\n          .readText()\n          .contains(REUSE_ENABLED) -> {\n          l.info(\"'.testcontainers.properties' looks good and contains reuse feature!\")\n        }\n\n        else -> {\n          l.info(\n            \"\"\"\n                        '.testcontainers.properties' does not contain 'testcontainers.reuse.enable=true'\n You need to create either by yourself or using '${StoveOptionsDsl::enableReuseForTestContainers.name}' method\n            \"\"\".trimIndent()\n          )\n        }\n      }\n    } else {\n      l.info(\n        \"\"\"'.testcontainers.properties' file DOES NOT exist. \n                    |You need to create either by yourself or using '${StoveOptionsDsl::enableReuseForTestContainers.name} method\n        \"\"\".trimMargin()\n      )\n    }\n  }\n\n  fun enable() {\n    l.info(\n      \"\"\"\n            You will see a file `~/.testcontainers.properties', with the setting 'testcontainers.reuse.enable=true'.\n            | If you don't see the file please create by yourself. \n            | Otherwise dependencies won't keep running.\n      \"\"\".trimIndent()\n    )\n    when {\n      !propertiesFilePath.exists() -> {\n        propertiesFilePath.writeText(REUSE_ENABLED)\n      }\n\n      else -> {\n        when {\n          propertiesFilePath\n            .readText()\n            .contains(REUSE_ENABLED) -> {\n            l.info(\n              \"'.testcontainers.properties' looks good and contains reuse feature!\"\n            )\n          }\n\n          else -> {\n            propertiesFilePath.appendText(REUSE_ENABLED)\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/ProvidedApplicationUnderTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n/**\n * Options for [ProvidedApplicationUnderTest].\n *\n * @param readiness Optional readiness strategy. If provided, Stove will verify\n *                  the remote application is reachable before running tests.\n *                  Use [ReadinessStrategy.HttpGet] for HTTP health checks,\n *                  [ReadinessStrategy.TcpPort] for gRPC/TCP, or [ReadinessStrategy.Probe]\n *                  for custom checks.\n */\ndata class ProvidedApplicationOptions(\n  val readiness: ReadinessStrategy? = null\n)\n\n/**\n * A no-op [ApplicationUnderTest] for testing against already-deployed remote applications.\n *\n * Use this when the application under test is already running (e.g., deployed to staging/dev)\n * and you want to write Stove tests against it without starting it locally.\n *\n * The application can be written in **any language** (Go, Python, .NET, Rust, Node.js, etc.)\n * as long as it exposes HTTP/gRPC and uses infrastructure Stove can connect to.\n *\n * ## Example\n *\n * ```kotlin\n * Stove().with {\n *     httpClient {\n *         HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\")\n *     }\n *     providedApplication {\n *         ProvidedApplicationOptions(\n *             readiness = ReadinessStrategy.HttpGet(\n *                 url = \"https://staging.myapp.com/actuator/health\"\n *             )\n *         )\n *     }\n * }.run()\n * ```\n *\n * @see ProvidedApplicationOptions\n * @see ReadinessStrategy\n */\n@StoveDsl\nclass ProvidedApplicationUnderTest(\n  private val options: ProvidedApplicationOptions\n) : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) {\n    options.readiness?.let { ReadinessChecker.check(it) }\n  }\n\n  override suspend fun stop(): Unit = Unit\n}\n\n/**\n * Registers a no-op application under test for testing against a remote/already-deployed application.\n *\n * HTTP and other system configurations are done separately via their own DSL functions\n * (`httpClient { }`, `kafka { }`, etc.). This function only signals that the application\n * is already running and should not be started by Stove.\n *\n * ## Example\n *\n * ```kotlin\n * Stove().with {\n *     httpClient { HttpClientSystemOptions(baseUrl = \"https://staging.myapp.com\") }\n *     postgresql(AppDb) { PostgresqlOptions.provided(jdbcUrl = \"jdbc:...\") }\n *     providedApplication {\n *         ProvidedApplicationOptions(\n *             readiness = ReadinessStrategy.HttpGet(\n *                 url = \"https://staging.myapp.com/health\"\n *             )\n *         )\n *     }\n * }.run()\n * ```\n *\n * @param configure Configuration block for [ProvidedApplicationOptions]. Defaults to no health check.\n * @return [ReadyStove] to chain with `.run()`.\n */\nfun WithDsl.providedApplication(\n  configure: () -> ProvidedApplicationOptions = { ProvidedApplicationOptions() }\n): ReadyStove {\n  this.stove.applicationUnderTest(ProvidedApplicationUnderTest(configure()))\n  return this.stove\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/ReadinessChecker.kt",
    "content": "@file:Suppress(\"TooGenericExceptionThrown\", \"UseCheckOrError\")\n\npackage com.trendyol.stove.system\n\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.future.await\nimport org.slf4j.LoggerFactory\nimport java.net.InetSocketAddress\nimport java.net.Socket\nimport java.net.URI\nimport java.net.http.HttpClient\nimport java.net.http.HttpRequest\nimport java.net.http.HttpResponse\nimport kotlin.time.Duration\n\n/**\n * Executes [ReadinessStrategy] checks to verify that an application is ready\n * before running tests.\n *\n * Used internally by [ProvidedApplicationUnderTest] and `ProcessApplicationUnderTest`.\n *\n * @see ReadinessStrategy\n */\nobject ReadinessChecker {\n  private val logger = LoggerFactory.getLogger(ReadinessChecker::class.java)\n  private const val TCP_CONNECT_TIMEOUT_MS = 1000\n\n  /**\n   * Executes the given [strategy] and blocks until the application is ready\n   * or the strategy's retry limit is exhausted.\n   *\n   * @throws IllegalStateException if readiness cannot be confirmed.\n   */\n  suspend fun check(strategy: ReadinessStrategy) {\n    when (strategy) {\n      is ReadinessStrategy.HttpGet -> checkHttp(strategy)\n\n      is ReadinessStrategy.TcpPort -> checkTcp(strategy)\n\n      is ReadinessStrategy.Probe -> checkProbe(strategy)\n\n      is ReadinessStrategy.FixedDelay -> {\n        logger.info(\"Waiting ${strategy.delay} for process readiness (fixed delay)\")\n        delay(strategy.delay)\n      }\n    }\n  }\n\n  private suspend fun checkHttp(strategy: ReadinessStrategy.HttpGet) {\n    val client = HttpClient.newBuilder()\n      .connectTimeout(java.time.Duration.ofMillis(strategy.timeout.inWholeMilliseconds))\n      .build()\n\n    val request = HttpRequest.newBuilder()\n      .uri(URI.create(strategy.url))\n      .GET()\n      .timeout(java.time.Duration.ofMillis(strategy.timeout.inWholeMilliseconds))\n      .build()\n\n    retryUntilReady(strategy.retries, strategy.retryDelay, \"Health check failed after ${strategy.retries} attempts for ${strategy.url}\") {\n        attempt,\n        total\n      ->\n      val response = runCatching {\n        client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).await()\n      }.onFailure {\n        logger.warn(\"Health check attempt ${attempt + 1}/$total failed: ${it.message}\")\n      }.getOrThrow()\n\n      if (response.statusCode() !in strategy.expectedStatusCodes) {\n        logger.warn(\"Health check attempt ${attempt + 1}/$total failed: status ${response.statusCode()}\")\n        throw IllegalStateException(\"Health check returned unexpected status ${response.statusCode()} from ${strategy.url}\")\n      }\n      logger.info(\"Health check passed for ${strategy.url} (status: ${response.statusCode()})\")\n    }\n  }\n\n  private suspend fun checkTcp(strategy: ReadinessStrategy.TcpPort) {\n    retryUntilReady(strategy.retries, strategy.retryDelay, \"TCP port ${strategy.port} did not open after ${strategy.retries} attempts\") {\n        attempt,\n        total\n      ->\n      runCatching {\n        Socket().use { socket ->\n          socket.connect(InetSocketAddress(\"localhost\", strategy.port), TCP_CONNECT_TIMEOUT_MS)\n        }\n      }.onFailure {\n        logger.debug(\"TCP check attempt ${attempt + 1}/$total on port ${strategy.port} failed: ${it.message}\")\n      }.getOrThrow()\n\n      logger.info(\"TCP port ${strategy.port} is open after ${attempt + 1} attempts\")\n    }\n  }\n\n  private suspend fun checkProbe(strategy: ReadinessStrategy.Probe) {\n    retryUntilReady(strategy.retries, strategy.retryDelay, \"Readiness probe did not pass after ${strategy.retries} attempts\") {\n        attempt,\n        total\n      ->\n      val ready = runCatching {\n        strategy.check()\n      }.onFailure {\n        logger.debug(\"Readiness probe attempt ${attempt + 1}/$total threw: ${it.message}\")\n      }.getOrThrow()\n\n      if (!ready) {\n        logger.debug(\"Readiness probe attempt ${attempt + 1}/$total returned false\")\n        error(\"Probe returned false\")\n      }\n      logger.info(\"Readiness probe passed after ${attempt + 1} attempts\")\n    }\n  }\n\n  /**\n   * Retries [attempt] up to [retries] times with [retryDelay] between attempts.\n   * The [attempt] block should return normally on success or throw on failure.\n   */\n  private suspend fun retryUntilReady(\n    retries: Int,\n    retryDelay: Duration,\n    errorMessage: String,\n    attempt: suspend (index: Int, total: Int) -> Unit\n  ) {\n    var lastException: Throwable? = null\n    repeat(retries) { index ->\n      runCatching {\n        attempt(index, retries)\n      }.onSuccess {\n        return\n      }.onFailure {\n        lastException = it\n      }\n      if (index < retries - 1) {\n        delay(retryDelay)\n      }\n    }\n    throw IllegalStateException(errorMessage, lastException as? Exception)\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/ReadinessStrategy.kt",
    "content": "package com.trendyol.stove.system\n\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\nprivate const val DEFAULT_RETRIES = 30\nprivate const val DEFAULT_HEALTH_CHECK_RETRIES = 10\nprivate const val HTTP_OK = 200\n\n/**\n * Protocol-agnostic readiness checking strategy for applications under test.\n *\n * Determines how Stove verifies that an application is ready to accept\n * requests before running tests. Supports HTTP, TCP, custom probes,\n * and fixed delays.\n *\n * ## Usage\n *\n * ```kotlin\n * // HTTP health check (REST APIs)\n * ReadinessStrategy.HttpGet(url = \"http://localhost:8080/health\")\n *\n * // TCP port check (gRPC, raw TCP)\n * ReadinessStrategy.TcpPort(port = 50051)\n *\n * // Custom probe (file existence, DB query, etc.)\n * ReadinessStrategy.Probe { File(\"/tmp/ready\").exists() }\n *\n * // Fixed delay (simple workers)\n * ReadinessStrategy.FixedDelay(3.seconds)\n * ```\n *\n * @see ReadinessChecker\n */\nsealed interface ReadinessStrategy {\n  /**\n   * Poll an HTTP GET endpoint until it returns an expected status code.\n   *\n   * Best for REST APIs and web servers that expose a health endpoint.\n   *\n   * @param url The health check endpoint URL (e.g., \"http://localhost:8080/health\").\n   * @param timeout Maximum time to wait for each HTTP request.\n   * @param retries Number of retry attempts before giving up.\n   * @param retryDelay Delay between retry attempts.\n   * @param expectedStatusCodes HTTP status codes considered healthy.\n   */\n  data class HttpGet(\n    val url: String,\n    val timeout: Duration = 30.seconds,\n    val retries: Int = DEFAULT_HEALTH_CHECK_RETRIES,\n    val retryDelay: Duration = 1.seconds,\n    val expectedStatusCodes: Set<Int> = setOf(HTTP_OK)\n  ) : ReadinessStrategy {\n    init {\n      require(url.isNotBlank()) { \"Health check URL must not be blank\" }\n      require(retries > 0) { \"retries must be positive, got $retries\" }\n      require(timeout.isPositive()) { \"timeout must be positive, got $timeout\" }\n      require(!retryDelay.isNegative()) { \"retryDelay must not be negative, got $retryDelay\" }\n    }\n  }\n\n  /**\n   * Try to open a TCP connection to a port until it succeeds.\n   *\n   * Best for gRPC servers, raw TCP servers, and any process that listens\n   * on a port but doesn't expose an HTTP health endpoint.\n   *\n   * @param port The TCP port to connect to.\n   * @param retries Number of connection attempts before giving up.\n   * @param retryDelay Delay between connection attempts.\n   */\n  data class TcpPort(\n    val port: Int,\n    val retries: Int = DEFAULT_RETRIES,\n    val retryDelay: Duration = 1.seconds\n  ) : ReadinessStrategy\n\n  /**\n   * Execute a user-provided probe function until it returns `true`.\n   *\n   * Best for processes with non-standard readiness signals (file existence,\n   * database state, custom protocol, etc.).\n   *\n   * @param retries Number of probe attempts before giving up.\n   * @param retryDelay Delay between probe attempts.\n   * @param check Suspend function that returns `true` when the process is ready.\n   */\n  data class Probe(\n    val retries: Int = DEFAULT_RETRIES,\n    val retryDelay: Duration = 1.seconds,\n    val check: suspend () -> Boolean\n  ) : ReadinessStrategy\n\n  /**\n   * Wait a fixed duration before considering the process ready.\n   *\n   * Fallback for simple workers that don't expose any readiness signal.\n   *\n   * @param delay Duration to wait.\n   */\n  data class FixedDelay(\n    val delay: Duration = 2.seconds\n  ) : ReadinessStrategy\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/Runner.kt",
    "content": "package com.trendyol.stove.system\n\n/**\n * Alias for runner of system under test\n */\ntypealias Runner<TContext> = (Array<String>) -> TContext\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/Stove.kt",
    "content": "package com.trendyol.stove.system\n\nimport arrow.core.*\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.system.Stove.Companion.instance\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport kotlinx.coroutines.*\nimport org.slf4j.*\nimport kotlin.reflect.KClass\n\n/**\n * Entrance of entire Stove test system.\n * Expects an url and port combination that are available also in the configuration of System Under Test.\n * For example; if your Spring application starts at :8081 then you need to change httpClient.baseUrl to `http://localhost:8081`\n *\n * Stove should be initialized only once for project, because it will start all the dependencies you plugged into it.\n * See also: [PluggedSystem]\n *\n * As a full example of Stove:\n * ```kotlin\n * Stove {\n *     if (this.isRunningLocally()) {\n *       enableReuseForTestContainers()\n *       keepDependenciesRunning()\n *     }\n *   }.with {\n *     httpClient {\n *       HttpClientSystemOptions(\n *         baseUrl = \"http://localhost:8080\",\n *       )\n *     }\n *     bridge()\n *     postgresql {\n *       PostgresqlOptions(configureExposedConfiguration = { cfg ->\n *         listOf(\n *           \"database.jdbcUrl=${cfg.jdbcUrl}\",\n *           \"database.host=${cfg.host}\",\n *           \"database.port=${cfg.port}\",\n *           \"database.name=${cfg.database}\",\n *           \"database.username=${cfg.username}\",\n *           \"database.password=${cfg.password}\"\n *         )\n *       })\n *     }\n *     kafka {\n *       stoveKafkaObjectMapperRef = objectMapperRef\n *       KafkaSystemOptions {\n *         listOf(\n *           \"kafka.bootstrapServers=${it.bootstrapServers}\",\n *           \"kafka.interceptorClasses=${it.interceptorClass}\"\n *         )\n *       }\n *     }\n *     wiremock {\n *       WireMockSystemOptions(\n *         port = 9090,\n *         removeStubAfterRequestMatched = true,\n *         afterRequest = { e, _ ->\n *           logger.info(e.request.toString())\n *         }\n *       )\n *     }\n *     ktor(\n *       withParameters = listOf(\n *         \"port=8080\"\n *       ),\n *       runner = { parameters ->\n *         stove.ktor.example.run(parameters) {\n *           addTestSystemDependencies()\n *         }\n *       }\n *     )\n *   }.run()\n * ```\n */\n@StoveDsl\nclass Stove(\n  configure: @StoveDsl StoveOptionsDsl.() -> Unit = {}\n) : ReadyStove,\n  AutoCloseable {\n  private val optionsDsl: StoveOptionsDsl = StoveOptionsDsl()\n\n  init {\n    configure(optionsDsl)\n  }\n\n  private var cleanup: MutableList<(suspend () -> Unit)> = mutableListOf()\n\n  @PublishedApi\n  internal val activeSystems: MutableMap<KClass<*>, PluggedSystem> = mutableMapOf()\n\n  @PublishedApi\n  internal val keyedSystems: MutableMap<Pair<KClass<*>, SystemKey>, PluggedSystem> = mutableMapOf()\n\n  private lateinit var applicationUnderTest: ApplicationUnderTest<*>\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  @PublishedApi\n  internal val options: StoveOptions = optionsDsl.options\n\n  internal val reporter: StoveReporter = StoveReporter(isEnabled = options.reportingEnabled)\n\n  /**\n   * Returns all registered systems from both default and keyed registrations.\n   * This is the single source of truth used by lifecycle, reporting, and cleanup.\n   */\n  fun allRegisteredSystems(): Collection<PluggedSystem> =\n    activeSystems.values + keyedSystems.values\n\n  /**\n   * Returns all registered systems that implement the given type.\n   * Includes both default and keyed systems.\n   */\n  inline fun <reified T> systemsOf(): List<T> =\n    allRegisteredSystems().filterIsInstance<T>()\n\n  /**\n   * Returns a read-only snapshot of all registered systems.\n   */\n  fun allSystems(): Collection<PluggedSystem> =\n    allRegisteredSystems()\n\n  /**\n   * Whether dependencies (containers) should be kept running after tests complete.\n   */\n  val keepDependenciesRunning: Boolean\n    get() = options.keepDependenciesRunning\n\n  /**\n   * Whether migrations should always run, even when reusing containers.\n   */\n  val runMigrationsAlways: Boolean\n    get() = options.runMigrationsAlways\n\n  /**\n   * Creates a state storage for the given system and configuration types.\n   */\n  inline fun <reified TState : ExposedConfiguration, reified TSystem : PluggedSystem> createStateStorage(): StateStorage<TState> =\n    options.createStateStorage<TState, TSystem>()\n\n  /**\n   * Creates a keyed state storage for the given system and configuration types.\n   * The key name is included in the storage path to prevent collisions.\n   */\n  inline fun <reified TState : ExposedConfiguration, reified TSystem : PluggedSystem> createStateStorage(\n    key: SystemKey\n  ): StateStorage<TState> =\n    options.stateStorageFactory.createWithKey(options, TSystem::class, TState::class, keyDisplayName(key))\n\n  /**\n   * Creates a state storage with an optional key name for disambiguation.\n   * When keyName is null, behaves identically to the no-arg version.\n   */\n  inline fun <reified TState : ExposedConfiguration, reified TSystem : PluggedSystem> createStateStorage(\n    keyName: String?\n  ): StateStorage<TState> =\n    if (keyName != null) {\n      options.stateStorageFactory.createWithKey(options, TSystem::class, TState::class, keyName)\n    } else {\n      options.createStateStorage<TState, TSystem>()\n    }\n\n  /**\n   * Registers a listener to receive report events.\n   */\n  fun addReportListener(listener: ReportEventListener) =\n    reporter.addListener(listener)\n\n  /**\n   * Removes a previously registered report event listener.\n   */\n  fun removeReportListener(listener: ReportEventListener) =\n    reporter.removeListener(listener)\n\n  /**\n   * Starts tracking a new test in the reporter.\n   */\n  fun startTest(ctx: StoveTestContext) =\n    reporter.startTest(ctx)\n\n  /**\n   * Records a report entry in the current test.\n   */\n  fun recordReport(entry: ReportEntry) =\n    reporter.record(entry)\n\n  /**\n   * Ends tracking the current test in the reporter.\n   */\n  fun endTest() =\n    reporter.endTest()\n\n  companion object {\n    /**\n     * [instance] is created only once per project, and it is available throughout the lifetime of the all the tests.\n     * DO NOT access it before [run] completes\n     */\n    internal lateinit var instance: Stove\n\n    /**\n     * Check if Stove instance has been initialized.\n     */\n    fun instanceInitialized(): Boolean = ::instance.isInitialized\n\n    fun reporter(): StoveReporter {\n      check(::instance.isInitialized) { \"Stove is not initialized yet, do not forget to call Stove#run\" }\n      return instance.reporter\n    }\n\n    fun options(): StoveOptions {\n      check(::instance.isInitialized) { \"Stove is not initialized yet, do not forget to call Stove#run\" }\n      return instance.options\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <T : PluggedSystem> getSystem(kClass: KClass<*>): T {\n      check(::instance.isInitialized) { \"Stove is not initialized yet, do not forget to call Stove#run\" }\n      return instance.getSystemOrThrow(kClass) as T\n    }\n\n    /**\n     * Returns the system of the given type as an Option.\n     * Returns None if Stove is not initialized or the system is not registered.\n     */\n    inline fun <reified T : PluggedSystem> getSystemOrNone(): Option<T> =\n      getSystemOrNone(T::class)\n\n    /**\n     * Returns the system of the given type as an Option.\n     * Returns None if Stove is not initialized or the system is not registered.\n     */\n    @Suppress(\"UNCHECKED_CAST\")\n    @PublishedApi\n    internal fun <T : PluggedSystem> getSystemOrNone(kClass: KClass<T>): Option<T> {\n      if (!::instance.isInitialized) return None\n      return instance.activeSystems.getOrNone(kClass).map { it as T }\n    }\n\n    fun stop(): Unit = instance.close()\n  }\n\n  /**\n   * Application under test, the tests run against the application provided.\n   * Usually a spring or generic application that can be hosted\n   */\n  fun applicationUnderTest(applicationUnderTest: ApplicationUnderTest<*>): Stove {\n    this.applicationUnderTest = applicationUnderTest\n    return this\n  }\n\n  internal fun getSystemOrThrow(\n    kClass: KClass<*>\n  ): PluggedSystem = activeSystems[kClass]\n    ?: error(\"System of type ${kClass.simpleName} is not registered in Stove\")\n\n  private lateinit var applicationUnderTestContext: Any\n\n  /**\n   * Runs the entire dependency tree that implements [RunnableSystemWithContext] since only the [RunnableSystemWithContext] can be run.\n   * Note that all the dependencies will run as parallel.\n   * It will invoke the runnable methods of [RunnableSystemWithContext]s with the order:\n   * - [RunnableSystemWithContext.beforeRun]\n   * - [RunnableSystemWithContext.run]\n   * - [RunnableSystemWithContext.afterRun]\n   */\n  override suspend fun run() {\n    coroutineScope {\n      val allSystems = allRegisteredSystems()\n\n      allSystems.filterIsInstance<BeforeRunAware>()\n        .map { async(context = Dispatchers.IO) { it.beforeRun() } }.awaitAll()\n\n      allSystems.filterIsInstance<RunAware>()\n        .map { async(context = Dispatchers.IO) { it.run() } }.awaitAll()\n\n      val dependencyConfigurations =\n        allSystems\n          .filterIsInstance<ExposesConfiguration>()\n          .flatMap { it.configuration() }\n\n      applicationUnderTestContext = applicationUnderTest.start(dependencyConfigurations)\n\n      allSystems.filterIsInstance<AfterRunAware>()\n        .map { async(context = Dispatchers.IO) { it.afterRun() } }.awaitAll()\n\n      // Cleanup is handled by registerForDispose — no duplication here\n      cleanup.add { applicationUnderTest.stop() }\n    }\n\n    instance = this\n  }\n\n  /**\n   * Enables the DSL for constructing the entire system with the [PluggedSystem]s.\n   *\n   * Example:\n   * ```kotlin\n   *  Stove().with {\n   *    httpClient{\n   *      // configure the http client\n   *    }\n   *    kafka{\n   *      // configure kafka\n   *    }\n   *    couchbase {\n   *      // configure couchbase\n   *    }\n   *\n   *    // and so on...\n   *  }\n   * ```\n   */\n  fun with(withDsl: WithDsl.() -> Unit): Stove {\n    withDsl(WithDsl(this))\n    return this\n  }\n\n  /**\n   * Gets or registers a [PluggedSystem] to Stove. Use it when you want to register a new [PluggedSystem] to Stove.\n   * That can be a system that comply your needs, for example; SchedulerSystem, GarbageCollectorSystem etc... These are only the names,\n   * so, you can implement these systems and register to the Test suite. When you register a new system to the test suite, it is wise to\n   * implement [AfterRunAwareWithContext.afterRun] to get the context/container of the system,\n   * so you can create your system methods based on that.\n   *\n   * Example:\n   * ```kotlin\n   * // plug the new system called scheduler\n   * Stove().withScheduler()\n   *\n   * // use it in testing\n   * stove.scheduler().advance()\n   * ```\n   */\n  inline fun <reified T : PluggedSystem> getOrRegister(system: T): T = activeSystems.getOrPut(T::class) {\n    registerForDispose(system)\n  } as T\n\n  /**\n   * Gets or registers a keyed [PluggedSystem]. Multiple instances of the same system type\n   * can coexist when registered with different [SystemKey]s.\n   *\n   * @see SystemKey\n   */\n  inline fun <reified T : PluggedSystem> getOrRegister(\n    key: SystemKey,\n    system: T\n  ): T = keyedSystems.getOrPut(T::class to key) {\n    registerForDispose(system)\n  } as T\n\n  /**\n   * Gets the registered system or returns [None]\n   */\n  inline fun <reified T : PluggedSystem> getOrNone(): Option<T> = activeSystems.getOrNone(T::class).map { it as T }\n\n  /**\n   * Gets the keyed registered system or returns [None]\n   */\n  inline fun <reified T : PluggedSystem> getOrNone(key: SystemKey): Option<T> =\n    keyedSystems.getOrNone(T::class to key).map { it as T }\n\n  fun <T : AutoCloseable> registerForDispose(closeable: T): T {\n    cleanup.add { closeable.close() }\n    return closeable\n  }\n\n  @Suppress(\"UNCHECKED_CAST\", \"unused\")\n  fun <TContext> applicationUnderTestContext(): TContext = applicationUnderTestContext as TContext\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      if (options.dumpReportOnStop && options.reportingEnabled) {\n        // Only dump report if there are failures\n        val report = reporter.dumpIfFailed(options.defaultRenderer)\n        if (report.isNotEmpty()) {\n          logger.info(\"=== Stove Test Report (Failures Detected) ===\")\n          logger.info(report)\n        }\n      }\n      cleanup.forEach { it() }\n    }.recover { logger.warn(\"got an error while stopping Stove: ${it.message}\") }\n  }\n}\n\n/**\n * Main entry point for test validation. Use this DSL to write assertions against your system.\n *\n * This is the primary way to interact with Stove in your tests. The DSL provides\n * access to all registered systems (HTTP, Kafka, databases, etc.) for assertions.\n *\n * ## Example\n *\n * ```kotlin\n * @Test\n * fun `should create user and publish event`() = runTest {\n *     stove {\n *         http {\n *             post<CreateUserRequest, UserResponse>(\"/users\", request) { response ->\n *                 response.id shouldNotBe null\n *             }\n *         }\n *         kafka {\n *             shouldBePublished<UserCreatedEvent> {\n *                 actual.userId == expectedUserId\n *             }\n *         }\n *         couchbase {\n *             shouldGet<User>(\"users\", \"user-123\") { user ->\n *                 user.name shouldBe \"John\"\n *             }\n *         }\n *     }\n * }\n * ```\n *\n * @param validation The DSL block containing test assertions.\n * @throws IllegalStateException if Stove has not been initialized via [Stove.run].\n * @see ValidationDsl\n */\nsuspend fun stove(\n  validation: @StoveDsl suspend ValidationDsl.() -> Unit\n) {\n  check(Stove.instanceInitialized()) { \"Stove is not initialized yet, do not forget to call Stove#run\" }\n  validation(ValidationDsl(instance))\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/StoveOptions.kt",
    "content": "package com.trendyol.stove.system\n\nimport com.trendyol.stove.reporting.JsonReportRenderer\nimport com.trendyol.stove.reporting.PrettyConsoleRenderer\nimport com.trendyol.stove.reporting.ReportRenderer\nimport com.trendyol.stove.system.abstractions.*\n\ndata class StoveOptions(\n  val keepDependenciesRunning: Boolean = false,\n  val stateStorageFactory: StateStorageFactory = StateStorageFactory.Default(),\n  val runMigrationsAlways: Boolean = false,\n  val reportingEnabled: Boolean = true,\n  val dumpReportOnTestFailure: Boolean = true,\n  val dumpReportOnStop: Boolean = false,\n  val defaultRenderer: ReportRenderer = PrettyConsoleRenderer,\n  val failureRenderer: ReportRenderer = PrettyConsoleRenderer,\n  val fileRenderer: ReportRenderer = JsonReportRenderer,\n  val reportToConsole: Boolean = true,\n  val reportToFile: Boolean = false,\n  val reportFilePath: String = \"build/stove-reports\"\n) {\n  inline fun <reified TState : ExposedConfiguration, reified TSystem : PluggedSystem> createStateStorage(): StateStorage<TState> =\n    (this.stateStorageFactory(this, TSystem::class, TState::class))\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/StoveOptionsDsl.kt",
    "content": "package com.trendyol.stove.system\n\nimport com.trendyol.stove.reporting.ReportRenderer\nimport com.trendyol.stove.system.abstractions.StateStorageFactory\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.slf4j.LoggerFactory\n\n/**\n * DSL for configuring [StoveOptions].\n *\n * Example:\n * ```kotlin\n * Stove {\n *   if (isRunningLocally()) {\n *     enableReuseForTestContainers()\n *     keepDependenciesRunning()\n *   }\n *   reporting {\n *     enabled()\n *     dumpOnFailure()\n *   }\n * }\n * ```\n */\n@StoveDsl\nclass StoveOptionsDsl {\n  private val logger = LoggerFactory.getLogger(javaClass)\n  private val propertiesFile = PropertiesFile()\n\n  internal var options = StoveOptions()\n    private set\n\n  // ═══════════════════════════════════════════════════════════════════════════\n  // Container & Environment\n  // ═══════════════════════════════════════════════════════════════════════════\n\n  /**\n   * Keep dependencies (containers) running after tests complete.\n   * Requires `.testcontainers.properties` file - call [enableReuseForTestContainers] first.\n   */\n  fun keepDependenciesRunning(): StoveOptionsDsl = apply {\n    logger.info(\n      \"\"\"\n      |You have chosen to keep dependencies running.\n      |For that Stove needs '.testcontainers.properties' file under your user(~/).\n      |To add that call 'enableReuseForTestContainers()' method\n      \"\"\".trimMargin()\n    )\n    propertiesFile.detectAndLogStatus()\n    options = options.copy(keepDependenciesRunning = true)\n  }\n\n  /**\n   * Check if tests are running locally (not on CI).\n   */\n  fun isRunningLocally(): Boolean = !isRunningOnCI()\n\n  /**\n   * Enable container reuse in TestContainers by creating the required properties file.\n   */\n  fun enableReuseForTestContainers(): Unit = propertiesFile.enable()\n\n  /**\n   * Configure custom state storage factory.\n   */\n  fun stateStorage(factory: StateStorageFactory): StoveOptionsDsl = apply {\n    options = options.copy(stateStorageFactory = factory)\n  }\n\n  /**\n   * Always run migrations, even if the database state hasn't changed.\n   */\n  fun runMigrationsAlways(): StoveOptionsDsl = apply {\n    options = options.copy(runMigrationsAlways = true)\n  }\n\n  // ═══════════════════════════════════════════════════════════════════════════\n  // Reporting Configuration\n  // ═══════════════════════════════════════════════════════════════════════════\n\n  /**\n   * Configure reporting options using the [ReportingDsl].\n   *\n   * Example:\n   * ```kotlin\n   * reporting {\n   *   enabled()\n   *   dumpOnFailure()\n   * }\n   * ```\n   */\n  fun reporting(configure: ReportingDsl.() -> Unit): StoveOptionsDsl = apply {\n    ReportingDsl(this).configure()\n  }\n\n  /** Enable reporting. */\n  fun reportingEnabled(enabled: Boolean = true): StoveOptionsDsl = apply {\n    options = options.copy(reportingEnabled = enabled)\n  }\n\n  /** Dump report on test failure. */\n  fun dumpReportOnTestFailure(enabled: Boolean = true): StoveOptionsDsl = apply {\n    options = options.copy(dumpReportOnTestFailure = enabled)\n  }\n\n  /** Set the renderer used for test failure reports. */\n  fun failureRenderer(renderer: ReportRenderer): StoveOptionsDsl = apply {\n    options = options.copy(failureRenderer = renderer)\n  }\n\n  private fun isRunningOnCI(): Boolean = CI_ENV_VARS.any { System.getenv(it) == \"true\" }\n\n  companion object {\n    private val CI_ENV_VARS = listOf(\"CI\", \"GITLAB_CI\", \"GITHUB_ACTIONS\")\n  }\n}\n\n/**\n * DSL for configuring reporting options in a grouped, fluent manner.\n */\n@StoveDsl\nclass ReportingDsl(\n  private val parent: StoveOptionsDsl\n) {\n  /** Enable reporting. */\n  fun enabled(value: Boolean = true) = parent.reportingEnabled(value)\n\n  /** Disable reporting. */\n  fun disabled() = enabled(false)\n\n  /** Dump report on test failure. */\n  fun dumpOnFailure(value: Boolean = true) = parent.dumpReportOnTestFailure(value)\n\n  /** Set the failure renderer. */\n  fun failureRenderer(renderer: ReportRenderer) = parent.failureRenderer(renderer)\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/ValidationDsl.kt",
    "content": "package com.trendyol.stove.system\n\nimport com.trendyol.stove.system.abstractions.PluggedSystem\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n/**\n * The DSL wrapper for writing test validations against registered [PluggedSystem]s.\n *\n * This class provides the entry point for all test assertions and validations.\n * It wraps [Stove] and exposes extension functions for each registered system\n * (HTTP, Kafka, Couchbase, PostgreSQL, etc.).\n *\n * ## Usage\n *\n * Use [Stove.stove] to access the validation DSL:\n *\n * ```kotlin\n * Stove.stove {\n *     // HTTP assertions\n *     http {\n *         get<UserResponse>(\"/users/123\") { user ->\n *             user.name shouldBe \"John\"\n *         }\n *     }\n *\n *     // Kafka assertions\n *     kafka {\n *         shouldBePublished<UserCreatedEvent> {\n *             actual.userId == \"123\"\n *         }\n *     }\n *\n *     // Database assertions using Bridge\n *     using<UserRepository> {\n *         findById(\"123\").name shouldBe \"John\"\n *     }\n *\n *     // Couchbase assertions\n *     couchbase {\n *         shouldGet<User>(\"users\", \"user-123\") { user ->\n *             user.name shouldBe \"John\"\n *         }\n *     }\n * }\n * ```\n *\n * ## Available System DSLs\n *\n * Each registered system provides its own DSL extension:\n * - `http { }` - HTTP client assertions\n * - `kafka { }` - Kafka publish/consume assertions\n * - `couchbase { }` - Couchbase document assertions\n * - `postgresql { }` / `mssql { }` - Database assertions\n * - `elasticsearch { }` - Elasticsearch document assertions\n * - `mongodb { }` - MongoDB document assertions\n * - `wiremock { }` - WireMock stub setup\n * - `using<T> { }` - Bridge to application's DI container\n *\n * @property stove The underlying Stove instance containing all registered systems.\n * @see stove\n * @see PluggedSystem\n */\n@JvmInline\n@StoveDsl\nvalue class ValidationDsl(\n  val stove: Stove\n)\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/WithDsl.kt",
    "content": "package com.trendyol.stove.system\n\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n/**\n * The DSL wrapper for constructing and configuring the test system with [PluggedSystem]s.\n *\n * This class provides the entry point for registering all components that your tests need:\n * databases, message brokers, HTTP clients, mock servers, and the application under test.\n *\n * ## Usage\n *\n * Use [Stove.with] to access the configuration DSL:\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         // Configure HTTP client\n *         httpClient {\n *             HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n *         }\n *\n *         // Configure Kafka\n *         kafka {\n *             KafkaSystemOptions {\n *                 listOf(\"kafka.bootstrapServers=${it.bootstrapServers}\")\n *             }\n *         }\n *\n *         // Configure PostgreSQL\n *         postgresql {\n *             PostgresqlOptions(configureExposedConfiguration = { cfg ->\n *                 listOf(\n *                     \"spring.datasource.url=${cfg.jdbcUrl}\",\n *                     \"spring.datasource.username=${cfg.username}\",\n *                     \"spring.datasource.password=${cfg.password}\"\n *                 )\n *             })\n *         }\n *\n *         // Configure WireMock for external service mocking\n *         wiremock {\n *             WireMockSystemOptions(port = 9090)\n *         }\n *\n *         // Enable Bridge for DI container access\n *         bridge()\n *\n *         // Configure the application under test\n *         springBoot(\n *             runner = { params -> myApp.run(params) },\n *             withParameters = listOf(\"server.port=8080\")\n *         )\n *     }\n *     .run()\n * ```\n *\n * ## Available System Configurations\n *\n * Each system provides its own configuration function:\n * - `httpClient { }` - HTTP client for API testing\n * - `kafka { }` - Apache Kafka for messaging\n * - `couchbase { }` - Couchbase database\n * - `postgresql { }` - PostgreSQL database\n * - `mssql { }` - Microsoft SQL Server database\n * - `mongodb { }` - MongoDB database\n * - `elasticsearch { }` - Elasticsearch search engine\n * - `redis { }` - Redis cache\n * - `wiremock { }` - WireMock for HTTP mocking\n * - `bridge()` - Bridge to application's DI container\n * - `springBoot { }` / `ktor { }` - Application under test\n *\n * @property stove The underlying Stove instance being configured.\n * @see Stove.with\n * @see PluggedSystem\n */\n@JvmInline\n@StoveDsl\nvalue class WithDsl(\n  val stove: Stove\n) {\n  /**\n   * Registers the application under test with Stove.\n   *\n   * This is typically called by framework-specific functions like `springBoot()` or `ktor()`.\n   * You generally don't need to call this directly.\n   *\n   * @param applicationUnderTest The application to be tested.\n   */\n  fun applicationUnderTest(applicationUnderTest: ApplicationUnderTest<*>) {\n    this.stove.applicationUnderTest(applicationUnderTest)\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/ApplicationUnderTest.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\n/**\n * Interface representing the application being tested by Stove.\n *\n * This is the entry point for your actual application. Stove starts this application\n * after all test infrastructure (databases, message brokers, etc.) is running,\n * passing the exposed configurations so your app can connect to the test infrastructure.\n *\n * ## Framework Implementations\n *\n * Stove provides implementations for popular frameworks:\n * - **Spring Boot**: `SpringApplicationUnderTest`\n * - **Ktor**: `KtorApplicationUnderTest`\n * - **Micronaut**: `MicronautApplicationUnderTest`\n *\n * ## Spring Boot Example\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         postgresql { /* config */ }\n *         kafka { /* config */ }\n *\n *         springBoot(\n *             runner = { params ->\n *                 com.example.MyApplication.run(params) {\n *                     addTestDependencies()  // Optional test-specific beans\n *                 }\n *             },\n *             withParameters = listOf(\n *                 \"server.port=8080\",\n *                 \"logging.level.root=WARN\"\n *             )\n *         )\n *     }\n *     .run()\n * ```\n *\n * ## Ktor Example\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         mongodb { /* config */ }\n *\n *         ktor(\n *             withParameters = listOf(\"port=8080\"),\n *             runner = { params ->\n *                 com.example.main(params) {\n *                     addTestModules()\n *                 }\n *             }\n *         )\n *     }\n *     .run()\n * ```\n *\n * ## Configuration Flow\n *\n * 1. [TestSystem] starts all [PluggedSystem]s\n * 2. Systems expose their configuration via [ExposesConfiguration]\n * 3. Configurations are collected and passed to [start]\n * 4. Your application starts with access to test infrastructure\n * 5. [AfterRunAware.afterRun] is called on all systems\n *\n * @param TContext The application context type (e.g., `ApplicationContext` for Spring,\n *                 `Application` for Ktor).\n * @see TestSystem\n * @see ExposesConfiguration\n * @author Oguzhan Soykan\n */\ninterface ApplicationUnderTest<TContext : Any> {\n  /**\n   * Starts the application with the provided configurations.\n   *\n   * The [configurations] list contains properties from all [ExposesConfiguration]\n   * implementors plus any parameters specified in the test setup. These are typically\n   * passed as system properties or command-line arguments.\n   *\n   * ## Example Configuration List\n   *\n   * ```kotlin\n   * // configurations parameter might contain:\n   * listOf(\n   *     \"spring.datasource.url=jdbc:postgresql://localhost:32789/test\",\n   *     \"spring.datasource.username=test\",\n   *     \"spring.kafka.bootstrap-servers=localhost:32790\",\n   *     \"server.port=8080\"\n   * )\n   * ```\n   *\n   * @param configurations Combined list of infrastructure configs and test parameters.\n   * @return The application context for use by [AfterRunAwareWithContext] systems.\n   */\n  suspend fun start(configurations: List<String>): TContext\n\n  /**\n   * Stops the application gracefully.\n   *\n   * Called during [TestSystem] shutdown to clean up application resources.\n   */\n  suspend fun stop()\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/Exceptions.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\nimport kotlin.reflect.KClass\n\n/**\n * @author Oguzhan Soykan\n */\nclass SystemNotRegisteredException(\n  system: KClass<*>,\n  detail: String? = null\n) : Throwable(\n  \"${system.simpleName} was not registered. \" +\n    (detail ?: \"Make sure that you registered your service on TestSystem\")\n)\n\n/**\n * @author Oguzhan Soykan\n */\nclass SystemConfigurationException(\n  system: KClass<*>,\n  reason: String\n) : Throwable(\n  \"${system.simpleName} configuration got an error: $reason\"\n)\n\nclass SystemNotInitializedException(\n  system: KClass<*>\n) : Throwable(\n  \"${system.simpleName} was not initialized. Make sure that you initialized your service on TestSystem\"\n)\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/ExposesConfiguration.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\n/**\n * Interface for systems that expose configuration to the application under test.\n *\n * When a [PluggedSystem] starts (e.g., a database container), it knows its runtime configuration\n * (ports, credentials, URLs). This interface allows the system to expose that configuration\n * so it can be passed to the application under test during startup.\n *\n * ## How It Works\n *\n * 1. [TestSystem] starts all registered systems via [RunAware.run]\n * 2. After systems are running, [TestSystem] collects configuration from all [ExposesConfiguration] implementors\n * 3. The collected configuration is passed to [ApplicationUnderTest.start]\n * 4. Your application receives these as system properties/environment variables\n *\n * ## Example Implementation\n *\n * ```kotlin\n * class KafkaSystem(\n *     override val stove: Stove,\n *     private val options: KafkaSystemOptions\n * ) : PluggedSystem, RunAware, ExposesConfiguration {\n *\n *     private lateinit var container: KafkaContainer\n *     private lateinit var exposedConfig: KafkaExposedConfiguration\n *\n *     override suspend fun run() {\n *         container = KafkaContainer(DockerImageName.parse(\"confluentinc/cp-kafka:7.4.0\"))\n *         container.start()\n *         exposedConfig = KafkaExposedConfiguration(\n *             bootstrapServers = container.bootstrapServers\n *         )\n *     }\n *\n *     override fun configuration(): List<String> =\n *         options.configureExposedConfiguration(exposedConfig)\n *     // Returns: [\"spring.kafka.bootstrap-servers=localhost:32789\"]\n * }\n * ```\n *\n * ## Configuration Format\n *\n * The returned list typically contains `key=value` strings that match your application's\n * expected configuration format:\n *\n * ```kotlin\n * // Spring Boot style\n * listOf(\n *     \"spring.datasource.url=jdbc:postgresql://localhost:5432/test\",\n *     \"spring.datasource.username=test\"\n * )\n *\n * // Generic style\n * listOf(\n *     \"DATABASE_URL=jdbc:postgresql://localhost:5432/test\",\n *     \"DATABASE_USER=test\"\n * )\n * ```\n *\n * @see PluggedSystem\n * @see RunAware\n * @see ConfiguresExposedConfiguration\n * @see ExposedConfiguration\n * @author Oguzhan Soykan\n */\ninterface ExposesConfiguration {\n  /**\n   * Returns the configuration properties exposed by this system.\n   *\n   * This method is called after [RunAware.run] completes, so containers\n   * are running and their runtime information (ports, hosts) is available.\n   *\n   * @return A list of configuration strings, typically in `key=value` format.\n   */\n  fun configuration(): List<String>\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/PluggedSystem.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\nimport com.trendyol.stove.system.Stove\n\n/**\n * Base interface for all systems that can be plugged into [Stove].\n *\n * A PluggedSystem represents a testable component such as:\n * - **Databases**: PostgreSQL, MongoDB, Couchbase, Elasticsearch, MSSQL, Redis\n * - **Message Brokers**: Kafka\n * - **HTTP**: HTTP client, WireMock\n * - **Bridge**: Access to the application's DI container\n * - **Custom Systems**: Any domain-specific system you implement\n *\n * ## Implementing a Custom PluggedSystem\n *\n * To create a custom system, implement this interface along with the appropriate\n * lifecycle interfaces ([RunAware], [AfterRunAware], [ExposesConfiguration]):\n *\n * ```kotlin\n * class MyCustomSystem(\n *     override val stove: Stove,\n *     private val options: MyCustomSystemOptions\n * ) : PluggedSystem, RunAware, AfterRunAware {\n *\n *     private lateinit var client: MyClient\n *\n *     override suspend fun run() {\n *         // Initialize your system (e.g., start container, connect to service)\n *         client = MyClient(options.connectionString)\n *     }\n *\n *     override suspend fun afterRun() {\n *         // Called after the application under test has started\n *         // Useful for setup that requires the app to be running\n *     }\n *\n *     override fun close() {\n *         // Cleanup resources\n *         client.close()\n *     }\n *\n *     // Chainable method for DSL\n *     override fun then(): Stove = stove\n *\n *     // Custom DSL methods\n *     fun doSomething(): MyCustomSystem {\n *         client.execute()\n *         return this\n *     }\n * }\n * ```\n *\n * ## Registering Your System\n *\n * Create extension functions for easy DSL usage:\n *\n * ```kotlin\n * // Registration function\n * fun WithDsl.myCustomSystem(\n *     configure: () -> MyCustomSystemOptions\n * ): Stove = stove.getOrRegister(\n *     MyCustomSystem(stove, configure())\n * ).let { stove }\n *\n * // Validation DSL function\n * suspend fun ValidationDsl.myCustom(\n *     block: suspend MyCustomSystem.() -> Unit\n * ) = block(stove.getOrNone<MyCustomSystem>().getOrElse {\n *     throw SystemNotRegisteredException(MyCustomSystem::class)\n * })\n * ```\n *\n * @see Stove\n * @see RunAware\n * @see AfterRunAware\n * @see ExposesConfiguration\n * @see ThenSystemContinuation\n * @author Oguzhan Soykan\n */\ninterface PluggedSystem :\n  AutoCloseable,\n  ThenSystemContinuation\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/ReadyStove.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\n/**\n * Marks the [com.trendyol.stove.system.Stove] as ready after it is started.\n * @author Oguzhan Soykan\n */\ninterface ReadyStove {\n  suspend fun run()\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/RunnableSystemWithContext.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\nimport com.trendyol.stove.functional.*\nimport kotlinx.coroutines.runBlocking\nimport org.slf4j.*\n\n/**\n * Lifecycle interface for systems that need to perform setup before starting.\n *\n * Implement this when your system needs to prepare resources before the main\n * [RunAware.run] phase. This is called before any systems are started.\n *\n * ## Example Use Cases\n * - Pulling Docker images ahead of time\n * - Validating configuration\n * - Creating network resources\n *\n * ```kotlin\n * class MySystem(...) : PluggedSystem, BeforeRunAware, RunAware {\n *     override suspend fun beforeRun() {\n *         // Download required files, validate config, etc.\n *         validateConfiguration()\n *     }\n *\n *     override suspend fun run() {\n *         // Start the actual system\n *     }\n * }\n * ```\n *\n * @see RunAware\n * @see AfterRunAware\n * @author Oguzhan Soykan\n */\ninterface BeforeRunAware {\n  /**\n   * Called before any systems are started.\n   *\n   * Use this for early initialization that doesn't depend on other systems.\n   */\n  suspend fun beforeRun()\n}\n\n/**\n * Core lifecycle interface for systems that can be started and stopped.\n *\n * This is the main lifecycle interface for [PluggedSystem]s. Most systems\n * implement this to start containers, establish connections, or initialize resources.\n *\n * ## Lifecycle Order\n *\n * 1. [BeforeRunAware.beforeRun] - All systems (parallel)\n * 2. [RunAware.run] - All systems (parallel)\n * 3. Application under test starts\n * 4. [AfterRunAware.afterRun] - All systems (parallel)\n *\n * ## Example\n *\n * ```kotlin\n * class PostgresqlSystem(...) : PluggedSystem, RunAware {\n *     private lateinit var container: PostgreSQLContainer<*>\n *\n *     override suspend fun run() {\n *         container = PostgreSQLContainer(\"postgres:15\")\n *             .withDatabaseName(\"test\")\n *         container.start()\n *     }\n *\n *     override suspend fun stop() {\n *         container.stop()\n *     }\n * }\n * ```\n *\n * @see BeforeRunAware\n * @see AfterRunAware\n * @author Oguzhan Soykan\n */\ninterface RunAware {\n  /**\n   * Starts the system.\n   *\n   * This is called in parallel for all registered systems.\n   * Start containers, establish connections, or initialize resources here.\n   */\n  suspend fun run()\n\n  /**\n   * Stops the system.\n   *\n   * Called during [TestSystem] shutdown. Clean up resources here.\n   */\n  suspend fun stop()\n}\n\n/**\n * Lifecycle interface for systems that need the application context after startup.\n *\n * This is used by systems like [BridgeSystem] that need access to the\n * application's DI container after the application has started.\n *\n * ## Example\n *\n * ```kotlin\n * class SpringBridgeSystem(stove: Stove) :\n *     BridgeSystem<ApplicationContext>(stove) {\n *\n *     override suspend fun afterRun(context: ApplicationContext) {\n *         // Now we have access to Spring's ApplicationContext\n *         this.ctx = context\n *     }\n *\n *     override fun <D : Any> get(klass: KClass<D>): D =\n *         ctx.getBean(klass.java)\n * }\n * ```\n *\n * @param TContext The type of application context (e.g., `ApplicationContext` for Spring)\n * @see AfterRunAware\n * @see BridgeSystem\n * @author Oguzhan Soykan\n */\ninterface AfterRunAwareWithContext<TContext> {\n  /**\n   * Called after the application under test has started.\n   *\n   * @param context The application context from the started application.\n   */\n  suspend fun afterRun(context: TContext)\n}\n\n/**\n * Lifecycle interface for systems that need to perform actions after the application starts.\n *\n * Implement this when your system needs to do something after the application\n * under test is running, but doesn't need direct access to the application context.\n *\n * ## Example Use Cases\n * - Running database migrations\n * - Seeding test data\n * - Verifying connectivity\n *\n * ```kotlin\n * class PostgresqlSystem(...) : PluggedSystem, RunAware, AfterRunAware {\n *     override suspend fun afterRun() {\n *         // Run migrations after app has started\n *         migrations.forEach { it.execute(connection) }\n *     }\n * }\n * ```\n *\n * @see AfterRunAwareWithContext\n * @see RunAware\n * @author Oguzhan Soykan\n */\ninterface AfterRunAware {\n  /**\n   * Called after the application under test has started.\n   */\n  suspend fun afterRun()\n}\n\n/**\n * Combined lifecycle interface for systems with full lifecycle support and context access.\n *\n * This interface combines [BeforeRunAware], [RunAware], and [AfterRunAwareWithContext]\n * for systems that need complete lifecycle control and access to the application context.\n *\n * Most systems don't need this full interface; use individual interfaces instead.\n *\n * @param TContext The type of application context.\n * @see BeforeRunAware\n * @see RunAware\n * @see AfterRunAwareWithContext\n * @author Oguzhan Soykan\n */\ninterface RunnableSystemWithContext<TContext> :\n  AutoCloseable,\n  BeforeRunAware,\n  RunAware,\n  AfterRunAwareWithContext<TContext> {\n  private val logger: Logger get() = LoggerFactory.getLogger(javaClass)\n\n  override fun close(): Unit = runBlocking { Try { stop() }.recover { logger.warn(\"got an error while stopping\") } }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/StateStorage.kt",
    "content": "@file:Suppress(\"FunctionName\")\n\npackage com.trendyol.stove.system.abstractions\n\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport com.trendyol.stove.serialization.*\nimport com.trendyol.stove.system.*\nimport org.slf4j.*\nimport java.nio.file.*\nimport java.util.*\nimport kotlin.io.path.*\nimport kotlin.reflect.KClass\n\ninterface StateStorage<TState> {\n  suspend fun capture(start: suspend () -> TState): TState\n\n  fun isSubsequentRun(): Boolean\n}\n\ninterface StateStorageFactory {\n  operator fun <T : Any> invoke(options: StoveOptions, system: KClass<*>, state: KClass<T>): StateStorage<T>\n\n  /**\n   * Creates a state storage with an optional key name to prevent collisions\n   * when multiple instances of the same system type are registered.\n   * Default implementation delegates to [invoke], ignoring the key.\n   */\n  fun <T : Any> createWithKey(\n    options: StoveOptions,\n    system: KClass<*>,\n    state: KClass<T>,\n    keyName: String?\n  ): StateStorage<T> = invoke(options, system, state)\n\n  companion object {\n    fun Default(): StateStorageFactory = DefaultStateStorageFactory()\n\n    private fun DefaultStateStorageFactory(): StateStorageFactory = object : StateStorageFactory {\n      override fun <T : Any> invoke(options: StoveOptions, system: KClass<*>, state: KClass<T>): StateStorage<T> =\n        DefaultStateStorage(options, system, state)\n\n      override fun <T : Any> createWithKey(\n        options: StoveOptions,\n        system: KClass<*>,\n        state: KClass<T>,\n        keyName: String?\n      ): StateStorage<T> = FileSystemStorage(options, system, state, keyName)\n    }\n  }\n\n  fun <T : Any> DefaultStateStorage(\n    options: StoveOptions,\n    system: KClass<*>,\n    state: KClass<T>\n  ): StateStorage<T> = FileSystemStorage(options, system, state)\n}\n\n/**\n * Represents the state of [Stove] which is being captured.\n * @param TState the type of the state\n * @param state the state of [Stove]\n * @param processId the process id of [Stove]\n */\ndata class StateWithProcess<TState : Any>(\n  val state: TState,\n  val processId: Long\n)\n\ninternal class FileSystemStorage<TState : Any>(\n  val options: StoveOptions,\n  val system: KClass<*>,\n  private val state: KClass<TState>,\n  private val keyName: String? = null\n) : StateStorage<TState> {\n  private val folderForSystem =\n    Paths.get(\n      System.getProperty(\"java.io.tmpdir\"),\n      \"com.trendyol.stove\"\n    )\n\n  private val pathForSystem: Path = folderForSystem.resolve(\n    \"stove-e2e-${system.simpleName!!.lowercase(Locale.ROOT)}\" +\n      (keyName?.let { \"-${it.replace(UNSAFE_FILENAME_CHARS, \"-\").lowercase(Locale.ROOT)}\" } ?: \"\") +\n      \".lock\"\n  )\n  private val j = StoveSerde.jackson.default\n  private val l: Logger = LoggerFactory.getLogger(javaClass)\n\n  init {\n    if (!folderForSystem.exists()) {\n      folderForSystem.createDirectories()\n    }\n  }\n\n  /**\n   * Captures Stove state into the file system. Basically creates a Json file which contains the state of the [PluggedSystem]\n   * that is run by [Stove].\n   */\n  override suspend fun capture(start: suspend () -> TState): TState = when {\n    !options.keepDependenciesRunning -> {\n      l.info(\"State for ${name()} is being deleted at the path: ${pathForSystem.absolutePathString()}\")\n      pathForSystem.deleteIfExists()\n      start()\n    }\n\n    pathForSystem.exists() && options.keepDependenciesRunning -> {\n      recover(otherwise = start)\n    }\n\n    !pathForSystem.exists() && options.keepDependenciesRunning -> {\n      saveStateForNextRun(start())\n    }\n\n    else -> {\n      pathForSystem.deleteIfExists()\n      start()\n    }\n  }\n\n  /**\n   * Returns true if [Stove] is being run for the first time.\n   */\n  override fun isSubsequentRun(): Boolean = pathForSystem.exists() && options.keepDependenciesRunning && isDifferentProcess()\n\n  /**\n   * Recovers the state of [Stove] from the file system.\n   */\n  private suspend fun recover(otherwise: suspend () -> TState): TState =\n    when {\n      pathForSystem.exists() -> {\n        l.info(\"State exists for ${name()}. System is being recovered from: ${pathForSystem.absolutePathString()}\")\n        val swp = j.readValue<StateWithProcess<TState>>(pathForSystem.readBytes())\n        j.convertValue(swp.state, state.java)\n      }\n\n      else -> {\n        saveStateForNextRun(otherwise())\n      }\n    }\n\n  private fun saveStateForNextRun(state: TState): TState =\n    state.also {\n      l.info(\"State does not exist for ${name()}. System is being saved to: ${pathForSystem.absolutePathString()}\")\n      pathForSystem.writeBytes(j.writeValueAsBytes(StateWithProcess(state, getPid())))\n    }\n\n  private fun isDifferentProcess(): Boolean {\n    val swp: StateWithProcess<TState> = j.readValue(pathForSystem.readBytes())\n    return swp.processId != getPid()\n  }\n\n  private fun name(): String = system.simpleName!!\n\n  private fun getPid(): Long = ProcessHandle.current().pid()\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/SystemKey.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\n/**\n * Marker interface for typed keys used to register and look up multiple instances of the same system type.\n *\n * Define keys as singleton objects:\n * ```kotlin\n * object PaymentService : SystemKey\n * object OrderService : SystemKey\n * object AnalyticsDb : SystemKey\n * ```\n *\n * Use keys in registration and validation DSLs:\n * ```kotlin\n * // Registration\n * httpClient(PaymentService) { HttpClientSystemOptions(baseUrl = \"https://pay.internal\") }\n *\n * // Validation\n * http(PaymentService) { get<Payment>(\"/payments\") { ... } }\n * ```\n *\n * A single key can be shared across protocols:\n * ```kotlin\n * httpClient(PaymentService) { ... }\n * grpc(PaymentService) { ... }\n * ```\n *\n * @see com.trendyol.stove.system.Stove.getOrRegister\n */\ninterface SystemKey\n\ninternal val UNSAFE_FILENAME_CHARS = Regex(\"[^a-zA-Z0-9._-]\")\n\n/**\n * Returns a display-safe, filesystem-safe name for a [SystemKey],\n * with fallbacks for anonymous classes.\n */\nfun keyDisplayName(key: SystemKey): String =\n  (key::class.simpleName ?: key::class.qualifiedName ?: \"anonymous-key-${System.identityHashCode(key)}\")\n    .replace(UNSAFE_FILENAME_CHARS, \"-\")\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/SystemOptions.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\n/**\n * Marker interface for system configuration options.\n *\n * Each [PluggedSystem] has its own options class implementing this interface.\n * Options define how the system should be configured, including container settings,\n * connection parameters, and configuration exposure.\n *\n * ## Example Implementation\n *\n * ```kotlin\n * data class PostgresqlOptions(\n *     val databaseName: String = \"test_db\",\n *     val username: String = \"test\",\n *     val password: String = \"test\",\n *     override val configureExposedConfiguration: (PostgresqlExposedConfiguration) -> List<String>\n * ) : SystemOptions, ConfiguresExposedConfiguration<PostgresqlExposedConfiguration>\n * ```\n *\n * ## Usage in TestSystem\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         postgresql {\n *             PostgresqlOptions(\n *                 databaseName = \"my_app_test\",\n *                 configureExposedConfiguration = { cfg ->\n *                     listOf(\n *                         \"spring.datasource.url=${cfg.jdbcUrl}\",\n *                         \"spring.datasource.username=${cfg.username}\",\n *                         \"spring.datasource.password=${cfg.password}\"\n *                     )\n *                 }\n *             )\n *         }\n *     }\n * ```\n *\n * @see PluggedSystem\n * @see ExposedConfiguration\n * @see ConfiguresExposedConfiguration\n */\ninterface SystemOptions\n\n/**\n * Marker interface for configuration values exposed by a [PluggedSystem] to the application under test.\n *\n * When a system starts (e.g., a PostgreSQL container), it exposes configuration values\n * like connection URLs, ports, and credentials. These values are passed to the application\n * under test so it can connect to the test infrastructure.\n *\n * ## Example Implementation\n *\n * ```kotlin\n * data class PostgresqlExposedConfiguration(\n *     val host: String,\n *     val port: Int,\n *     val database: String,\n *     val username: String,\n *     val password: String\n * ) : ExposedConfiguration {\n *     val jdbcUrl: String\n *         get() = \"jdbc:postgresql://$host:$port/$database\"\n * }\n * ```\n *\n * ## How Configuration Flows\n *\n * 1. System starts (container or provided instance)\n * 2. System creates [ExposedConfiguration] with runtime values\n * 3. [ConfiguresExposedConfiguration.configureExposedConfiguration] transforms it to property strings\n * 4. Properties are passed to the application under test on startup\n *\n * @see SystemOptions\n * @see ConfiguresExposedConfiguration\n * @see ExposesConfiguration\n */\ninterface ExposedConfiguration\n\n/**\n * Interface for system options that can transform [ExposedConfiguration] into application properties.\n *\n * This interface bridges the gap between Stove's test infrastructure and your application's\n * configuration format (Spring properties, environment variables, etc.).\n *\n * ## Example\n *\n * ```kotlin\n * data class KafkaSystemOptions(\n *     override val configureExposedConfiguration: (KafkaExposedConfiguration) -> List<String>\n * ) : SystemOptions, ConfiguresExposedConfiguration<KafkaExposedConfiguration>\n *\n * // Usage\n * kafka {\n *     KafkaSystemOptions { cfg ->\n *         listOf(\n *             \"spring.kafka.bootstrap-servers=${cfg.bootstrapServers}\",\n *             \"spring.kafka.consumer.group-id=test-group\"\n *         )\n *     }\n * }\n * ```\n *\n * @param T The type of exposed configuration this options class works with.\n * @see ExposedConfiguration\n * @see SystemOptions\n */\ninterface ConfiguresExposedConfiguration<T : ExposedConfiguration> {\n  /**\n   * Function that transforms the exposed configuration into a list of property strings.\n   *\n   * The returned strings are typically in the format `key=value` and are passed\n   * to the application under test as configuration properties.\n   */\n  val configureExposedConfiguration: (T) -> List<String>\n}\n\n/**\n * Interface for system options that connect to externally provided instances\n * instead of starting testcontainers.\n *\n * Use this when you want to:\n * - Connect to shared test infrastructure\n * - Use existing databases/services in CI/CD\n * - Debug against local installations\n * - Avoid container startup overhead\n *\n * ## Example\n *\n * ```kotlin\n * // Instead of starting a PostgreSQL container, connect to an existing instance\n * postgresql {\n *     ProvidedPostgresqlOptions(\n *         providedConfig = PostgresqlExposedConfiguration(\n *             host = \"localhost\",\n *             port = 5432,\n *             database = \"test_db\",\n *             username = \"postgres\",\n *             password = \"secret\"\n *         ),\n *         configureExposedConfiguration = { cfg ->\n *             listOf(\"spring.datasource.url=${cfg.jdbcUrl}\")\n *         }\n *     )\n * }\n * ```\n *\n * @param TConfig The type of exposed configuration for this system.\n * @see SystemOptions\n * @see ExposedConfiguration\n */\ninterface ProvidedSystemOptions<TConfig : ExposedConfiguration> {\n  /**\n   * The configuration for the provided (external) instance.\n   *\n   * This contains connection details for the external service.\n   * Unlike container-based options, this is always non-null.\n   */\n  val providedConfig: TConfig\n\n  /**\n   * Whether to run database migrations when using a provided instance.\n   *\n   * Set to `true` if you want Stove to apply migrations to the external database.\n   * Set to `false` if migrations are managed externally or not needed.\n   */\n  val runMigrationsForProvided: Boolean\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/SystemRuntime.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\n/**\n * Represents the runtime environment for a system.\n *\n * Implementations:\n * - [com.trendyol.stove.containers.StoveContainer] - container-based runtime\n * - [ProvidedRuntime] - externally provided instance\n *\n * Use pattern matching (`when`) to handle different runtime types:\n * ```kotlin\n * when (val runtime = context.runtime) {\n *   is StoveContainer -> runtime.start()\n *   is ProvidedRuntime -> // use provided config from options\n * }\n * ```\n */\ninterface SystemRuntime\n\n/**\n * Provided (external) instance runtime that connects to an existing service.\n * Configuration comes from the options class.\n */\ndata object ProvidedRuntime : SystemRuntime\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/ThenSystemContinuation.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\nimport com.trendyol.stove.system.Stove\n\n/**\n * Enables method chaining between different system assertions in the DSL.\n *\n * All [PluggedSystem]s implement this interface, allowing fluent switching\n * between different systems during test assertions.\n *\n * ## Chaining Example\n *\n * ```kotlin\n * stove {\n *     http {\n *         postAndExpectBodilessResponse(\"/orders\", body = order.some()) {\n *             it.status shouldBe 201\n *         }\n *     }\n *         .then()  // Switch back to Stove context\n *         .also {\n *             kafka {\n *                 shouldBePublished<OrderCreatedEvent> {\n *                     actual.orderId == order.id\n *                 }\n *             }\n *         }\n * }\n * ```\n *\n * ## Fluent DSL Style\n *\n * The `then()` method returns [Stove], enabling continued assertions:\n *\n * ```kotlin\n * // All methods return their system, allowing chaining\n * couchbase {\n *     save(documentId, document)\n * }.then()\n *\n * http {\n *     get<Document>(\"/documents/$documentId\") { doc ->\n *         doc shouldBe document\n *     }\n * }\n * ```\n *\n * @property stove The parent Stove instance for continuation.\n * @see PluggedSystem\n * @author Oguzhan Soykan\n */\ninterface ThenSystemContinuation {\n  val stove: Stove\n\n  /**\n   * Returns [Stove] to continue with other system assertions.\n   *\n   * @return The parent Stove instance.\n   */\n  fun then(): Stove = stove\n\n  /**\n   * Executes an action only if dependencies are not set to keep running.\n   *\n   * This is useful for cleanup actions that should be skipped when\n   * containers are reused across test runs (development mode).\n   *\n   * @param action The suspend action to conditionally execute.\n   */\n  suspend fun executeWithReuseCheck(action: suspend () -> Unit) {\n    if (stove.keepDependenciesRunning) {\n      return\n    }\n    action()\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/abstractions/ValidatedSystem.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\n/**\n * An abstraction for a system that can be validated after each test or any given moment.\n * @author Oguzhan Soykan\n */\ninterface ValidatedSystem {\n  /**\n   * System that validates itself at any given moment\n   * Each system needs to implement its validation logic\n   */\n  suspend fun validate()\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/annotations/StoveDsl.kt",
    "content": "package com.trendyol.stove.system.annotations\n\n/**\n * DSL marker annotation for Stove's type-safe builder pattern.\n *\n * This annotation is used to scope DSL functions and prevent accidental access\n * to outer receivers in nested lambdas, ensuring type safety in the test DSL.\n *\n * ## Purpose\n *\n * When writing nested DSL blocks, Kotlin allows implicit access to outer receivers.\n * This can lead to confusing code where it's unclear which receiver a method belongs to.\n * [StoveDsl] prevents this by marking all Stove DSL components.\n *\n * ## Example\n *\n * ```kotlin\n * // Without @DslMarker, this would compile but be confusing:\n * stove {\n *     http {\n *         kafka {  // Accidentally nested - should be at validation level\n *             // ...\n *         }\n *     }\n * }\n *\n * // With @StoveDsl, the above code won't compile, forcing correct structure:\n * stove {\n *     http {\n *         get<User>(\"/users/1\") { /* ... */ }\n *     }\n *     kafka {  // Correctly at validation level\n *         shouldBePublished<Event> { /* ... */ }\n *     }\n * }\n * ```\n *\n * ## When to Use\n *\n * Apply this annotation when creating:\n * - Custom [com.trendyol.stove.system.abstractions.PluggedSystem] implementations\n * - DSL extension functions for systems\n * - Options classes used in configuration DSL\n * - Any class that participates in Stove's builder pattern\n *\n * ```kotlin\n * @StoveDsl\n * class MyCustomSystem(override val stove: Stove) : PluggedSystem {\n *     @StoveDsl\n *     fun myDslMethod(): MyCustomSystem {\n *         // ...\n *         return this\n *     }\n * }\n * ```\n *\n * @see DslMarker\n */\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class StoveDsl\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/application/ApplicationConfigurations.kt",
    "content": "package com.trendyol.stove.system.application\n\n/**\n * Parses Stove `key=value` configuration entries into a map for application launch.\n */\nfun List<String>.toConfigurationMap(): Map<String, String> =\n  associate { line ->\n    val (key, value) = line.split(\"=\", limit = 2)\n    key to value\n  }\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/application/ArgsProvider.kt",
    "content": "package com.trendyol.stove.system.application\n\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n/**\n * Provides CLI arguments for an application under test.\n */\nfun interface ArgsProvider {\n  fun provide(configurations: Map<String, String>): List<String>\n\n  companion object {\n    fun empty(): ArgsProvider = ArgsProvider { emptyList() }\n  }\n}\n\nfun argsMapper(\n  prefix: String = \"--\",\n  separator: String = \"=\",\n  block: ArgsMapperBuilder.() -> Unit\n): ArgsProvider =\n  ArgsMapperBuilder(prefix = prefix, separator = separator).apply(block).build()\n\n@StoveDsl\nclass ArgsMapperBuilder(\n  private val prefix: String,\n  private val separator: String\n) {\n  private val mappings = mutableMapOf<String, String>()\n  private val staticArgs = mutableListOf<() -> List<String>>()\n\n  fun map(configurationKey: String, flagName: String) {\n    mappings[configurationKey] = flagName\n  }\n\n  infix fun String.to(flagName: String) {\n    map(this, flagName)\n  }\n\n  fun arg(flag: String, value: String? = null) {\n    staticArgs.add {\n      if (value != null) {\n        formatArg(flag, value)\n      } else {\n        listOf(\"$prefix$flag\")\n      }\n    }\n  }\n\n  fun arg(flag: String, value: () -> String) {\n    staticArgs.add { formatArg(flag, value()) }\n  }\n\n  private fun formatArg(flag: String, value: String): List<String> =\n    if (separator == \" \") {\n      listOf(\"$prefix$flag\", value)\n    } else {\n      listOf(\"$prefix$flag$separator$value\")\n    }\n\n  fun build(): ArgsProvider = ArgsProvider { configurations ->\n    buildList {\n      for ((configKey, flagName) in mappings) {\n        configurations[configKey]?.let { value ->\n          addAll(formatArg(flagName, value))\n        }\n      }\n      for (provider in staticArgs) {\n        addAll(provider())\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/system/application/EnvProvider.kt",
    "content": "package com.trendyol.stove.system.application\n\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n/**\n * Provides environment variables for an application under test.\n */\nfun interface EnvProvider {\n  fun provide(configurations: Map<String, String>): Map<String, String>\n\n  companion object {\n    fun empty(): EnvProvider = EnvProvider { emptyMap() }\n  }\n}\n\nfun envMapper(block: EnvMapperBuilder.() -> Unit): EnvProvider =\n  EnvMapperBuilder().apply(block).build()\n\n@StoveDsl\nclass EnvMapperBuilder {\n  private val mappings = mutableMapOf<String, String>()\n  private val staticVars = mutableMapOf<String, () -> String>()\n\n  fun map(configurationKey: String, envVarName: String) {\n    mappings[configurationKey] = envVarName\n  }\n\n  infix fun String.to(envVarName: String) {\n    map(this, envVarName)\n  }\n\n  fun env(name: String, value: String) {\n    staticVars[name] = { value }\n  }\n\n  fun env(name: String, value: () -> String) {\n    staticVars[name] = value\n  }\n\n  fun build(): EnvProvider = EnvProvider { configurations ->\n    buildMap {\n      for ((configKey, envVarName) in mappings) {\n        configurations[configKey]?.let { put(envVarName, it) }\n      }\n      for ((name, valueProvider) in staticVars) {\n        put(name, valueProvider())\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/tracing/SpanInfo.kt",
    "content": "package com.trendyol.stove.tracing\n\n/**\n * Information about a single span in a trace.\n */\ndata class SpanInfo(\n  val traceId: String,\n  val spanId: String,\n  val parentSpanId: String?,\n  val operationName: String,\n  val serviceName: String,\n  val startTimeNanos: Long,\n  val endTimeNanos: Long,\n  val status: SpanStatus,\n  val attributes: Map<String, String> = emptyMap(),\n  val exception: ExceptionInfo? = null\n) {\n  val durationMs: Long\n    get() = (endTimeNanos - startTimeNanos) / NANOS_TO_MILLIS\n\n  val durationNanos: Long\n    get() = endTimeNanos - startTimeNanos\n\n  val isFailed: Boolean\n    get() = status == SpanStatus.ERROR\n\n  val isSuccess: Boolean\n    get() = status == SpanStatus.OK\n\n  companion object {\n    internal const val NANOS_TO_MILLIS = 1_000_000L\n  }\n}\n\n/**\n * Exception information captured in a span.\n */\ndata class ExceptionInfo(\n  val type: String,\n  val message: String,\n  val stackTrace: List<String> = emptyList()\n)\n\n/**\n * Status of a span.\n */\nenum class SpanStatus {\n  OK,\n  ERROR,\n  UNSET\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/tracing/SpanTree.kt",
    "content": "package com.trendyol.stove.tracing\n\n/**\n * Represents a node in the span tree.\n * This is an immutable data structure - children cannot be modified after construction.\n */\ndata class SpanNode(\n  val span: SpanInfo,\n  val children: List<SpanNode> = emptyList()\n) {\n  val hasFailedDescendants: Boolean\n    get() = span.isFailed || children.any { it.hasFailedDescendants }\n\n  val totalDurationMs: Long\n    get() = span.durationMs\n\n  val depth: Int\n    get() = 1 + (children.maxOfOrNull { it.depth } ?: 0)\n\n  val spanCount: Int\n    get() = 1 + children.sumOf { it.spanCount }\n\n  fun findFailurePoint(): SpanNode? {\n    if (span.isFailed && children.none { it.hasFailedDescendants }) {\n      return this\n    }\n    return children.firstNotNullOfOrNull { it.findFailurePoint() }\n  }\n\n  fun flatten(): List<SpanInfo> = listOf(span) + children.flatMap { it.flatten() }\n}\n\n/**\n * Utility object for building and querying span trees.\n */\nobject SpanTree {\n  fun build(spans: List<SpanInfo>): SpanNode? {\n    if (spans.isEmpty()) return null\n\n    val spanMap = spans.associateBy { it.spanId }\n    val childrenMap = mutableMapOf<String?, MutableList<SpanInfo>>()\n\n    // Group spans by their parent ID\n    for (span in spans) {\n      val effectiveParentId = if (span.parentSpanId != null && spanMap.containsKey(span.parentSpanId)) {\n        span.parentSpanId\n      } else {\n        null\n      }\n      childrenMap.getOrPut(effectiveParentId) { mutableListOf() }.add(span)\n    }\n\n    // Recursively build nodes bottom-up (immutably)\n    fun buildNode(spanInfo: SpanInfo): SpanNode {\n      val childSpans = childrenMap[spanInfo.spanId] ?: emptyList()\n      val sortedChildren = childSpans\n        .sortedBy { it.startTimeNanos }\n        .map { buildNode(it) }\n      return SpanNode(spanInfo, sortedChildren)\n    }\n\n    val roots = childrenMap[null] ?: return null\n    if (roots.isEmpty()) return null\n\n    val sortedRoots = roots.sortedBy { it.startTimeNanos }\n\n    return if (sortedRoots.size == 1) {\n      buildNode(sortedRoots.first())\n    } else {\n      // Create a virtual root containing multiple roots\n      val rootNodes = sortedRoots.map { buildNode(it) }\n      SpanNode(\n        span = sortedRoots.first().copy(\n          operationName = \"trace-root\",\n          parentSpanId = null\n        ),\n        children = rootNodes\n      )\n    }\n  }\n\n  fun findSpan(root: SpanNode, predicate: (SpanInfo) -> Boolean): SpanNode? {\n    if (predicate(root.span)) return root\n    return root.children.firstNotNullOfOrNull { findSpan(it, predicate) }\n  }\n\n  fun filterSpans(root: SpanNode, predicate: (SpanInfo) -> Boolean): List<SpanNode> {\n    val result = mutableListOf<SpanNode>()\n    if (predicate(root.span)) result.add(root)\n    root.children.forEach { result.addAll(filterSpans(it, predicate)) }\n    return result\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/tracing/TraceContext.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.opentelemetry.api.baggage.Baggage\nimport io.opentelemetry.api.trace.*\nimport io.opentelemetry.context.Context\nimport io.opentelemetry.context.Scope\nimport kotlinx.coroutines.ThreadContextElement\nimport kotlinx.coroutines.withContext\nimport java.text.Normalizer\nimport java.util.UUID\nimport kotlin.coroutines.AbstractCoroutineContextElement\nimport kotlin.coroutines.CoroutineContext\n\ndata class TraceContext(\n  val traceId: String,\n  val testId: String,\n  val rootSpanId: String\n) {\n  fun toTraceparent(): String = \"00-$traceId-$rootSpanId-01\"\n\n  companion object {\n    /** W3C trace context header name */\n    const val TRACEPARENT_HEADER = \"traceparent\"\n\n    /** Stove test ID header name */\n    const val STOVE_TEST_ID_HEADER = \"X-Stove-Test-Id\"\n\n    /** Baggage key for propagating the Stove test ID through OTel's baggage propagator. */\n    const val BAGGAGE_TEST_ID_KEY = \"stove.test.id\"\n\n    /** Span ID length in W3C trace context */\n    private const val SPAN_ID_LENGTH = 16\n\n    /** Minimum parts required in a W3C traceparent header */\n    private const val TRACEPARENT_MIN_PARTS = 3\n\n    /**\n     * Uses InheritableThreadLocal to propagate trace context to child threads.\n     * IMPORTANT: Always call [clear] when done with the trace to avoid memory leaks.\n     * Prefer using [use] for automatic cleanup.\n     */\n    private val current = InheritableThreadLocal<TraceContext>()\n\n    /**\n     * Stores the OTel [Scope] so it can be closed in [clear].\n     * Not inheritable -- only the creating thread should close it.\n     */\n    private val otelScope = ThreadLocal<Scope>()\n\n    fun start(testId: String): TraceContext {\n      val ctx = TraceContext(\n        traceId = generateTraceId(),\n        testId = testId,\n        rootSpanId = generateSpanId()\n      )\n      current.set(ctx)\n      activateOtelContext(ctx)\n      return ctx\n    }\n\n    fun current(): TraceContext? = current.get()\n\n    /**\n     * Runs [block] while propagating the current [TraceContext] across coroutine thread switches.\n     *\n     * This keeps both Stove's thread-local context and OTel scope active when coroutines hop\n     * between worker threads.\n     */\n    suspend fun <T> withCurrentPropagation(block: suspend () -> T): T {\n      val ctx = current() ?: return block()\n      return withPropagation(ctx, block)\n    }\n\n    /**\n     * Runs [block] with the given [ctx] propagated across coroutine thread switches.\n     */\n    suspend fun <T> withPropagation(ctx: TraceContext, block: suspend () -> T): T =\n      withContext(TraceContextPropagationElement(ctx)) {\n        block()\n      }\n\n    /**\n     * Clears the current trace context from the thread-local storage.\n     * IMPORTANT: Always call this method when done with a trace to prevent memory leaks.\n     */\n    fun clear() {\n      deactivateOtelContext()\n      current.remove()\n    }\n\n    /**\n     * Executes the given block with a new trace context, ensuring cleanup afterward.\n     * This is the preferred way to use trace contexts to avoid memory leaks.\n     *\n     * @param testId The test identifier for the trace\n     * @param block The code block to execute within the trace context\n     * @return The result of the block execution\n     */\n    inline fun <T> use(testId: String, block: (TraceContext) -> T): T {\n      val ctx = start(testId)\n      return try {\n        block(ctx)\n      } finally {\n        clear()\n      }\n    }\n\n    fun generateTraceId(): String =\n      UUID.randomUUID().toString().replace(\"-\", \"\")\n\n    fun generateSpanId(): String =\n      UUID\n        .randomUUID()\n        .toString()\n        .replace(\"-\", \"\")\n        .take(SPAN_ID_LENGTH)\n\n    fun parseTraceparent(traceparent: String): Pair<String, String>? {\n      val parts = traceparent.split(\"-\")\n      return if (parts.size >= TRACEPARENT_MIN_PARTS) {\n        parts[1] to parts[2]\n      } else {\n        null\n      }\n    }\n\n    /**\n     * Sanitizes a string for use in HTTP headers and as a consistent identifier.\n     * Replaces non-ASCII characters with their closest ASCII equivalents or underscores.\n     *\n     * Uses Java's Normalizer to decompose characters (e.g., \"ü\" → \"u\" + combining diaeresis)\n     * then strips combining marks, leaving only base ASCII characters.\n     *\n     * For scripts that don't decompose to ASCII (e.g., Japanese, Chinese, Korean),\n     * a hash suffix is appended to ensure uniqueness.\n     *\n     * This should be used when creating testId to ensure consistency between\n     * what's stored internally and what's sent in HTTP/gRPC/Kafka headers.\n     */\n    fun sanitizeToAscii(value: String): String {\n      val sanitized = Normalizer\n        .normalize(value, Normalizer.Form.NFD)\n        .replace(COMBINING_MARKS_REGEX, \"\")\n        .replace(NON_ASCII_REGEX, \"_\")\n\n      // If we replaced any characters with underscores (lost information),\n      // append a hash to ensure uniqueness for non-decomposable scripts like Japanese\n      val hasReplacements = sanitized.contains(\"_\") && !value.all { it.code in ASCII_PRINTABLE_START..ASCII_PRINTABLE_END }\n      return if (hasReplacements) {\n        val hash = Integer.toHexString(value.hashCode() and POSITIVE_INT_MASK).takeLast(HASH_SUFFIX_LENGTH)\n        \"${sanitized}_$hash\"\n      } else {\n        sanitized\n      }\n    }\n\n    /** Length of hash suffix for uniqueness */\n    private const val HASH_SUFFIX_LENGTH = 6\n\n    /** Start of printable ASCII range (space character) */\n    private const val ASCII_PRINTABLE_START = 0x20\n\n    /** End of printable ASCII range (tilde character) */\n    private const val ASCII_PRINTABLE_END = 0x7E\n\n    /** Mask to convert hash to positive integer */\n    private const val POSITIVE_INT_MASK = 0x7FFFFFFF\n\n    /** Regex to match Unicode combining marks (diacritics) */\n    private val COMBINING_MARKS_REGEX = Regex(\"\\\\p{M}\")\n\n    /** Regex to match any remaining non-ASCII characters */\n    private val NON_ASCII_REGEX = Regex(\"[^\\\\x20-\\\\x7E]\")\n\n    /**\n     * Activates an OTel context with Stove's trace ID and test ID baggage.\n     *\n     * When the OTel Java Agent is present, this makes the agent treat subsequent\n     * outgoing calls (HTTP, Kafka, gRPC) as children of Stove's trace:\n     * - **SpanContext**: The agent sees an active trace and creates child spans\n     *   with Stove's trace ID instead of generating a new root trace.\n     * - **Baggage**: The test ID is propagated automatically via the W3C `baggage`\n     *   header across all transports, without manual header injection.\n     *\n     * When no agent is present, the OTel API is a no-op.\n     */\n    @Suppress(\"TooGenericExceptionCaught\")\n    private fun activateOtelContext(ctx: TraceContext) {\n      try {\n        val spanContext = SpanContext.create(\n          ctx.traceId,\n          ctx.rootSpanId,\n          TraceFlags.getSampled(),\n          TraceState.getDefault()\n        )\n        val span = Span.wrap(spanContext)\n        val baggage = Baggage\n          .fromContext(Context.current())\n          .toBuilder()\n          .put(BAGGAGE_TEST_ID_KEY, ctx.testId)\n          .build()\n        val otelCtx = Context\n          .current()\n          .with(span)\n          .with(baggage)\n        otelScope.set(otelCtx.makeCurrent())\n      } catch (_: Exception) {\n        // OTel API initialization failed -- trace headers still work as fallback\n      }\n    }\n\n    @Suppress(\"TooGenericExceptionCaught\")\n    private fun deactivateOtelContext() {\n      try {\n        otelScope.get()?.close()\n      } catch (_: Exception) {\n        // Scope.close() may fail if called from a different thread than start()\n      }\n      otelScope.remove()\n    }\n\n    private data class PreviousThreadState(\n      val traceContext: TraceContext?\n    )\n\n    /**\n     * Propagates TraceContext and OTel scope across coroutine dispatcher thread switches.\n     */\n    private class TraceContextPropagationElement(\n      private val ctx: TraceContext\n    ) : AbstractCoroutineContextElement(Key),\n      ThreadContextElement<PreviousThreadState> {\n      companion object Key : CoroutineContext.Key<TraceContextPropagationElement>\n\n      override fun updateThreadContext(context: CoroutineContext): PreviousThreadState {\n        val previous = PreviousThreadState(current.get())\n        deactivateOtelContext()\n        current.set(ctx)\n        activateOtelContext(ctx)\n        return previous\n      }\n\n      override fun restoreThreadContext(context: CoroutineContext, oldState: PreviousThreadState) {\n        deactivateOtelContext()\n        oldState.traceContext?.let { previous ->\n          current.set(previous)\n          activateOtelContext(previous)\n        } ?: current.remove()\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/tracing/TraceTreeRenderer.kt",
    "content": "package com.trendyol.stove.tracing\n\n/**\n * Renders span trees as human-readable text with optional ANSI colors.\n */\n@Suppress(\"TooManyFunctions\")\nobject TraceTreeRenderer {\n  private const val INDENT = \"│  \"\n  private const val BRANCH = \"├─ \"\n  private const val LAST_BRANCH = \"└─ \"\n  private const val SPACE = \"   \"\n  private const val MAX_STACK_TRACE_LINES = 3\n\n  // ANSI color codes\n  private object Colors {\n    const val RESET = \"\\u001B[0m\"\n    const val BOLD = \"\\u001B[1m\"\n    const val DIM = \"\\u001B[2m\"\n    const val RED = \"\\u001B[31m\"\n    const val GREEN = \"\\u001B[32m\"\n    const val YELLOW = \"\\u001B[33m\"\n    const val CYAN = \"\\u001B[36m\"\n    const val WHITE = \"\\u001B[37m\"\n    const val BRIGHT_RED = \"\\u001B[91m\"\n    const val BRIGHT_GREEN = \"\\u001B[92m\"\n    const val BRIGHT_YELLOW = \"\\u001B[93m\"\n  }\n\n  /**\n   * Renders the span tree with ANSI colors for terminal display.\n   */\n  fun renderColored(\n    root: SpanNode,\n    includeAttributes: Boolean = true,\n    attributePrefixes: List<String> = listOf(\"db.\", \"http.\", \"rpc.\", \"messaging.\")\n  ): String {\n    val sb = StringBuilder()\n    renderNodeColored(sb, root, \"\", true, includeAttributes, attributePrefixes)\n    return sb.toString()\n  }\n\n  /**\n   * Renders the span tree as plain text (no colors).\n   */\n  fun render(\n    root: SpanNode,\n    includeAttributes: Boolean = true,\n    attributePrefixes: List<String> = listOf(\"db.\", \"http.\", \"rpc.\", \"messaging.\")\n  ): String {\n    val sb = StringBuilder()\n    renderNode(sb, root, \"\", true, includeAttributes, attributePrefixes)\n    return sb.toString()\n  }\n\n  @Suppress(\"CyclomaticComplexMethod\")\n  private fun renderNodeColored(\n    sb: StringBuilder,\n    node: SpanNode,\n    prefix: String,\n    isLast: Boolean,\n    includeAttributes: Boolean,\n    attributePrefixes: List<String>\n  ) {\n    val connector = getConnector(prefix, isLast)\n    val childPrefix = getChildPrefix(prefix, isLast)\n\n    appendColoredSpanLine(sb, node, prefix, connector)\n    appendExceptionIfFailed(sb, node, childPrefix, colored = true)\n    appendAttributesIfEnabled(sb, includeAttributes, childPrefix, node.span.attributes, attributePrefixes, colored = true)\n\n    node.children.forEachIndexed { index, child ->\n      renderNodeColored(sb, child, childPrefix, index == node.children.lastIndex, includeAttributes, attributePrefixes)\n    }\n  }\n\n  private fun appendColoredSpanLine(sb: StringBuilder, node: SpanNode, prefix: String, connector: String) {\n    val isFailed = node.span.isFailed\n    val statusIcon = if (isFailed) \"${Colors.BRIGHT_RED}✗${Colors.RESET}\" else \"${Colors.BRIGHT_GREEN}✓${Colors.RESET}\"\n    val durationColor = if (isFailed) Colors.BRIGHT_RED else Colors.DIM\n    val nameColor = if (isFailed) Colors.BRIGHT_RED else Colors.WHITE\n    val failureMarker = getFailureMarker(node, colored = true)\n\n    sb.appendLine(\n      \"$prefix$connector$nameColor${node.span.operationName}${Colors.RESET} \" +\n        \"$durationColor[${node.span.durationMs}ms]${Colors.RESET} $statusIcon$failureMarker\"\n    )\n  }\n\n  private fun getFailureMarker(node: SpanNode, colored: Boolean): String {\n    val isFailurePoint = node.span.isFailed && node.children.none { it.hasFailedDescendants }\n    return when {\n      !isFailurePoint -> \"\"\n      colored -> \" ${Colors.BOLD}${Colors.BRIGHT_YELLOW}◄── FAILURE POINT${Colors.RESET}\"\n      else -> \" ◄── FAILURE POINT\"\n    }\n  }\n\n  private fun getConnector(prefix: String, isLast: Boolean): String = when {\n    prefix.isEmpty() -> \"\"\n    isLast -> LAST_BRANCH\n    else -> BRANCH\n  }\n\n  private fun getChildPrefix(prefix: String, isLast: Boolean): String = prefix + when {\n    prefix.isEmpty() -> \"\"\n    isLast -> SPACE\n    else -> INDENT\n  }\n\n  private fun appendExceptionIfFailed(sb: StringBuilder, node: SpanNode, childPrefix: String, colored: Boolean) {\n    if (node.span.exception != null && node.span.isFailed) {\n      if (colored) {\n        renderExceptionColored(sb, childPrefix, node.span.exception)\n      } else {\n        renderException(sb, childPrefix, node.span.exception)\n      }\n    }\n  }\n\n  private fun appendAttributesIfEnabled(\n    sb: StringBuilder,\n    includeAttributes: Boolean,\n    childPrefix: String,\n    attributes: Map<String, String>,\n    attributePrefixes: List<String>,\n    colored: Boolean\n  ) {\n    if (includeAttributes) {\n      if (colored) {\n        renderRelevantAttributesColored(sb, childPrefix, attributes, attributePrefixes)\n      } else {\n        renderRelevantAttributes(sb, childPrefix, attributes, attributePrefixes)\n      }\n    }\n  }\n\n  private fun renderExceptionColored(sb: StringBuilder, prefix: String, exception: ExceptionInfo) {\n    sb.appendLine(\n      \"$prefix${Colors.DIM}│${Colors.RESET}  ${Colors.BRIGHT_RED}Error:${Colors.RESET} \" +\n        \"${Colors.YELLOW}${exception.type}${Colors.RESET}: ${exception.message}\"\n    )\n    exception.stackTrace\n      .take(MAX_STACK_TRACE_LINES)\n      .forEach { line ->\n        sb.appendLine(\"$prefix${Colors.DIM}│${Colors.RESET}    ${Colors.DIM}$line${Colors.RESET}\")\n      }\n  }\n\n  private fun renderRelevantAttributesColored(\n    sb: StringBuilder,\n    prefix: String,\n    attributes: Map<String, String>,\n    attributePrefixes: List<String>\n  ) {\n    val relevantAttrs = attributes.filter { (key, _) ->\n      attributePrefixes.any { key.startsWith(it) }\n    }\n    relevantAttrs.forEach { (key, value) ->\n      sb.appendLine(\"$prefix${Colors.DIM}│${Colors.RESET}  ${Colors.CYAN}$key${Colors.RESET}: $value\")\n    }\n  }\n\n  private fun renderNode(\n    sb: StringBuilder,\n    node: SpanNode,\n    prefix: String,\n    isLast: Boolean,\n    includeAttributes: Boolean,\n    attributePrefixes: List<String>\n  ) {\n    val connector = getConnector(prefix, isLast)\n    val childPrefix = getChildPrefix(prefix, isLast)\n    val status = if (node.span.isFailed) \"✗\" else \"✓\"\n    val failureMarker = getFailureMarker(node, colored = false)\n\n    sb.appendLine(\"$prefix$connector${node.span.operationName} [${node.span.durationMs}ms] $status$failureMarker\")\n\n    appendExceptionIfFailed(sb, node, childPrefix, colored = false)\n    appendAttributesIfEnabled(sb, includeAttributes, childPrefix, node.span.attributes, attributePrefixes, colored = false)\n\n    node.children.forEachIndexed { index, child ->\n      renderNode(sb, child, childPrefix, index == node.children.lastIndex, includeAttributes, attributePrefixes)\n    }\n  }\n\n  private fun renderException(sb: StringBuilder, prefix: String, exception: ExceptionInfo) {\n    sb.appendLine(\"$prefix${INDENT}Error: ${exception.type}: ${exception.message}\")\n    exception.stackTrace\n      .take(MAX_STACK_TRACE_LINES)\n      .forEach { line ->\n        sb.appendLine(\"$prefix$INDENT  $line\")\n      }\n  }\n\n  private fun renderRelevantAttributes(\n    sb: StringBuilder,\n    prefix: String,\n    attributes: Map<String, String>,\n    attributePrefixes: List<String>\n  ) {\n    val relevantAttrs = attributes.filter { (key, _) ->\n      attributePrefixes.any { key.startsWith(it) }\n    }\n    relevantAttrs.forEach { (key, value) ->\n      sb.appendLine(\"$prefix${INDENT}$key: $value\")\n    }\n  }\n\n  fun renderCompact(root: SpanNode): String {\n    val sb = StringBuilder()\n    renderCompactNode(sb, root, 0)\n    return sb.toString()\n  }\n\n  private fun renderCompactNode(\n    sb: StringBuilder,\n    node: SpanNode,\n    depth: Int\n  ) {\n    val indent = \"  \".repeat(depth)\n    val status = if (node.span.isFailed) \"✗\" else \"✓\"\n    sb.appendLine(\"$indent$status ${node.span.operationName} (${node.span.durationMs}ms)\")\n    node.children.forEach { child ->\n      renderCompactNode(sb, child, depth + 1)\n    }\n  }\n\n  fun renderSummary(root: SpanNode): String {\n    val totalSpans = root.spanCount\n    val failedSpans = root.flatten().count { it.isFailed }\n    val totalDuration = root.span.durationMs\n    val maxDepth = root.depth\n\n    return buildString {\n      appendLine(\"Trace Summary:\")\n      appendLine(\"  Total spans: $totalSpans\")\n      appendLine(\"  Failed spans: $failedSpans\")\n      appendLine(\"  Total duration: ${totalDuration}ms\")\n      appendLine(\"  Max depth: $maxDepth\")\n\n      if (failedSpans > 0) {\n        val failurePoint = root.findFailurePoint()\n        if (failurePoint != null) {\n          appendLine(\"  Failure point: ${failurePoint.span.operationName}\")\n          failurePoint.span.exception?.let { ex ->\n            appendLine(\"  Error: ${ex.type}: ${ex.message}\")\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/main/kotlin/com/trendyol/stove/tracing/TraceVisualization.kt",
    "content": "package com.trendyol.stove.tracing\n\n/**\n * Data structure for trace visualization in reports.\n * Designed to be serializable and easy to render in different formats.\n */\ndata class TraceVisualization(\n  val traceId: String,\n  val testId: String,\n  val totalSpans: Int,\n  val failedSpans: Int,\n  val spans: List<VisualSpan>,\n  val tree: String,\n  val coloredTree: String\n) {\n  companion object {\n    fun from(traceId: String, testId: String, spans: List<SpanInfo>): TraceVisualization {\n      val visualSpans = spans.map { VisualSpan.from(it) }\n      val (tree, coloredTree) = buildTraceTrees(spans)\n      return TraceVisualization(\n        traceId = traceId,\n        testId = testId,\n        totalSpans = spans.size,\n        failedSpans = spans.count { it.status == SpanStatus.ERROR },\n        spans = visualSpans,\n        tree = tree,\n        coloredTree = coloredTree\n      )\n    }\n\n    /**\n     * Build tree visualizations of spans using SpanTree and TraceTreeRenderer.\n     * Returns both plain and colored versions for different display contexts.\n     */\n    private fun buildTraceTrees(spans: List<SpanInfo>): Pair<String, String> {\n      if (spans.isEmpty()) return \"No spans in trace\" to \"No spans in trace\"\n\n      val root = SpanTree.build(spans) ?: return \"No spans in trace\" to \"No spans in trace\"\n      return TraceTreeRenderer.render(root) to TraceTreeRenderer.renderColored(root)\n    }\n  }\n}\n\n/**\n * Simplified span representation for visualization\n */\ndata class VisualSpan(\n  val spanId: String,\n  val parentSpanId: String?,\n  val operationName: String,\n  val serviceName: String,\n  val durationMs: Double,\n  val status: String,\n  val attributes: Map<String, String>\n) {\n  companion object {\n    private const val NANOS_TO_MILLIS = 1_000_000L\n\n    fun from(span: SpanInfo): VisualSpan = VisualSpan(\n      spanId = span.spanId,\n      parentSpanId = span.parentSpanId,\n      operationName = span.operationName,\n      serviceName = span.serviceName,\n      durationMs = calculateDurationMs(span),\n      status = span.status.name,\n      attributes = span.attributes\n    )\n\n    private fun calculateDurationMs(span: SpanInfo): Double =\n      if (span.endTimeNanos > 0) {\n        (span.endTimeNanos - span.startTimeNanos).toDouble() / NANOS_TO_MILLIS\n      } else {\n        0.0\n      }\n  }\n}\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/containers/ContainerOptionsTest.kt",
    "content": "package com.trendyol.stove.containers\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport org.testcontainers.utility.DockerImageName\n\nclass ContainerOptionsTest :\n  FunSpec({\n    test(\"imageWithTag should combine image and tag\") {\n      val options = object : ContainerOptions<StoveContainer> {\n        override val registry: String = \"docker.io\"\n        override val image: String = \"alpine\"\n        override val tag: String = \"3.19\"\n        override val compatibleSubstitute: String? = null\n        override val useContainerFn: UseContainerFn<StoveContainer> = { _ -> error(\"unused\") }\n        override val containerFn: ContainerFn<StoveContainer> = { }\n      }\n\n      options.imageWithTag shouldBe \"alpine:3.19\"\n    }\n\n    test(\"useContainerFn should receive docker image name\") {\n      var received: DockerImageName? = null\n      val options = object : ContainerOptions<StoveContainer> {\n        override val registry: String = \"docker.io\"\n        override val image: String = \"alpine\"\n        override val tag: String = \"3.19\"\n        override val compatibleSubstitute: String? = null\n        override val useContainerFn: UseContainerFn<StoveContainer> = { imageName ->\n          received = imageName\n          error(\"stop\")\n        }\n        override val containerFn: ContainerFn<StoveContainer> = { }\n      }\n\n      try {\n        options.useContainerFn(DockerImageName.parse(options.imageWithTag))\n      } catch (_: Throwable) {\n      }\n\n      received?.asCanonicalNameString() shouldBe \"alpine:3.19\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/containers/ProvidedRegistryTest.kt",
    "content": "package com.trendyol.stove.containers\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport org.testcontainers.utility.DockerImageName\n\nclass ProvidedRegistryTest :\n  FunSpec({\n\n    beforeEach {\n      // Reset to default before each test\n      DEFAULT_REGISTRY = \"docker.io\"\n    }\n\n    context(\"DEFAULT_REGISTRY\") {\n      test(\"should have docker.io as default\") {\n        DEFAULT_REGISTRY shouldBe \"docker.io\"\n      }\n\n      test(\"should be settable globally\") {\n        DEFAULT_REGISTRY = \"my-registry.example.com\"\n\n        DEFAULT_REGISTRY shouldBe \"my-registry.example.com\"\n      }\n    }\n\n    context(\"withProvidedRegistry\") {\n      test(\"should prepend registry to image name\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\"postgres:15\") { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldContain \"docker.io/postgres:15\"\n      }\n\n      test(\"should use custom registry when provided\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\n          imageName = \"myapp:latest\",\n          registry = \"gcr.io/my-project\"\n        ) { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldContain \"gcr.io/my-project/myapp:latest\"\n      }\n\n      test(\"should trim leading slashes from registry\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\n          imageName = \"nginx:latest\",\n          registry = \"/my-registry.com/\"\n        ) { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldContain \"my-registry.com/nginx:latest\"\n      }\n\n      test(\"should trim leading slashes from image name\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\n          imageName = \"/library/redis:7\",\n          registry = \"docker.io\"\n        ) { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldContain \"docker.io/library/redis:7\"\n      }\n\n      test(\"should use image name as compatible substitute when not provided\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\"couchbase/server:7.0\") { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        // The image should be a substitute for the original image name\n        capturedImageName shouldBe capturedImageName\n      }\n\n      test(\"should use custom compatible substitute when provided\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\n          imageName = \"my-custom-postgres:15\",\n          registry = \"my-registry.com\",\n          compatibleSubstitute = \"postgres\"\n        ) { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldContain \"my-registry.com/my-custom-postgres:15\"\n      }\n\n      test(\"should return result from containerBuilder\") {\n        data class TestContainer(\n          val name: String\n        )\n\n        val result = withProvidedRegistry(\"test:latest\") { imageName ->\n          TestContainer(imageName.toString())\n        }\n\n        result.name shouldContain \"docker.io/test:latest\"\n      }\n\n      test(\"should use DEFAULT_REGISTRY when registry not specified\") {\n        DEFAULT_REGISTRY = \"custom-default.io\"\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\"myimage:v1\") { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldContain \"custom-default.io/myimage:v1\"\n      }\n\n      test(\"should handle image name with organization\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\n          imageName = \"confluentinc/cp-kafka:7.0.0\",\n          registry = \"docker.io\"\n        ) { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldContain \"docker.io/confluentinc/cp-kafka:7.0.0\"\n      }\n\n      test(\"should not prepend registry when image already contains a registry with a dot\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\n          imageName = \"mcr.microsoft.com/mssql/server:2022-CU16-ubuntu-22.04\",\n          registry = \"docker.io\"\n        ) { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldBe \"mcr.microsoft.com/mssql/server:2022-CU16-ubuntu-22.04\"\n      }\n\n      test(\"should not prepend registry when image contains ghcr.io\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\n          imageName = \"ghcr.io/my-org/my-image:latest\",\n          registry = \"docker.io\"\n        ) { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldBe \"ghcr.io/my-org/my-image:latest\"\n      }\n\n      test(\"should not prepend registry when image contains localhost with port\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\n          imageName = \"localhost:5000/my-image:latest\",\n          registry = \"docker.io\"\n        ) { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldBe \"localhost:5000/my-image:latest\"\n      }\n\n      test(\"should not prepend registry when registry is blank\") {\n        var capturedImageName: DockerImageName? = null\n\n        withProvidedRegistry(\n          imageName = \"postgres:15\",\n          registry = \"\"\n        ) { imageName ->\n          capturedImageName = imageName\n          \"container\"\n        }\n\n        capturedImageName?.toString() shouldBe \"postgres:15\"\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/containers/StoveContainerTest.kt",
    "content": "package com.trendyol.stove.containers\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass StoveContainerTest :\n  FunSpec({\n\n    context(\"ExecResult\") {\n      test(\"should store exit code, stdout, and stderr\") {\n        val result = ExecResult(\n          exitCode = 0,\n          stdout = \"command output\",\n          stderr = \"\"\n        )\n\n        result.exitCode shouldBe 0\n        result.stdout shouldBe \"command output\"\n        result.stderr shouldBe \"\"\n      }\n\n      test(\"should handle non-zero exit code\") {\n        val result = ExecResult(\n          exitCode = 1,\n          stdout = \"\",\n          stderr = \"Error: command not found\"\n        )\n\n        result.exitCode shouldBe 1\n        result.stderr shouldBe \"Error: command not found\"\n      }\n\n      test(\"should handle timeout with negative exit code\") {\n        val result = ExecResult(\n          exitCode = -1,\n          stdout = \"partial output\",\n          stderr = \"Command timed out after 60 seconds\"\n        )\n\n        result.exitCode shouldBe -1\n      }\n\n      test(\"should handle multiline output\") {\n        val result = ExecResult(\n          exitCode = 0,\n          stdout = \"\"\"\n            |line 1\n            |line 2\n            |line 3\n          \"\"\".trimMargin(),\n          stderr = \"\"\n        )\n\n        result.stdout.lines().size shouldBe 3\n      }\n    }\n\n    context(\"StoveContainerInspectInformation\") {\n      test(\"should store all container information\") {\n        val info = StoveContainerInspectInformation(\n          id = \"abc123def456\",\n          labels = mapOf(\"app\" to \"test\", \"version\" to \"1.0\"),\n          name = \"/test-container\",\n          state = \"running\",\n          running = true,\n          paused = false,\n          restarting = false,\n          startedAt = \"2024-01-15T10:30:00Z\",\n          finishedAt = \"0001-01-01T00:00:00Z\",\n          exitCode = 0,\n          error = \"\"\n        )\n\n        info.id shouldBe \"abc123def456\"\n        info.labels shouldBe mapOf(\"app\" to \"test\", \"version\" to \"1.0\")\n        info.name shouldBe \"/test-container\"\n        info.state shouldBe \"running\"\n        info.running shouldBe true\n        info.paused shouldBe false\n        info.restarting shouldBe false\n        info.exitCode shouldBe 0\n        info.error shouldBe \"\"\n      }\n\n      test(\"should represent paused container\") {\n        val info = StoveContainerInspectInformation(\n          id = \"container-id\",\n          labels = emptyMap(),\n          name = \"/paused-container\",\n          state = \"paused\",\n          running = true,\n          paused = true,\n          restarting = false,\n          startedAt = \"2024-01-15T10:30:00Z\",\n          finishedAt = \"0001-01-01T00:00:00Z\",\n          exitCode = 0,\n          error = \"\"\n        )\n\n        info.running shouldBe true\n        info.paused shouldBe true\n      }\n\n      test(\"should represent stopped container with error\") {\n        val info = StoveContainerInspectInformation(\n          id = \"failed-container\",\n          labels = emptyMap(),\n          name = \"/failed-container\",\n          state = \"exited\",\n          running = false,\n          paused = false,\n          restarting = false,\n          startedAt = \"2024-01-15T10:30:00Z\",\n          finishedAt = \"2024-01-15T10:35:00Z\",\n          exitCode = 137,\n          error = \"OOM killed\"\n        )\n\n        info.running shouldBe false\n        info.exitCode shouldBe 137\n        info.error shouldBe \"OOM killed\"\n      }\n\n      test(\"should represent restarting container\") {\n        val info = StoveContainerInspectInformation(\n          id = \"restarting-container\",\n          labels = emptyMap(),\n          name = \"/restarting\",\n          state = \"restarting\",\n          running = false,\n          paused = false,\n          restarting = true,\n          startedAt = \"2024-01-15T10:30:00Z\",\n          finishedAt = \"2024-01-15T10:35:00Z\",\n          exitCode = 1,\n          error = \"\"\n        )\n\n        info.restarting shouldBe true\n        info.running shouldBe false\n      }\n\n      test(\"should handle empty labels\") {\n        val info = StoveContainerInspectInformation(\n          id = \"id\",\n          labels = emptyMap(),\n          name = \"/container\",\n          state = \"running\",\n          running = true,\n          paused = false,\n          restarting = false,\n          startedAt = \"\",\n          finishedAt = \"\",\n          exitCode = 0,\n          error = \"\"\n        )\n\n        info.labels shouldBe emptyMap()\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/database/migrations/MigrationCollectionTest.kt",
    "content": "package com.trendyol.stove.database.migrations\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldContainExactly\nimport io.kotest.matchers.shouldBe\n\nclass MigrationCollectionTest :\n  FunSpec({\n\n    data class TestConnection(\n      val name: String\n    )\n\n    class SimpleMigration : DatabaseMigration<TestConnection> {\n      override val order: Int = 1\n      var executed = false\n\n      override suspend fun execute(connection: TestConnection) {\n        executed = true\n      }\n    }\n\n    class AnotherMigration : DatabaseMigration<TestConnection> {\n      override val order: Int = 2\n      var executed = false\n\n      override suspend fun execute(connection: TestConnection) {\n        executed = true\n      }\n    }\n\n    class HighPriorityMigration : DatabaseMigration<TestConnection> {\n      override val order: Int = MigrationPriority.HIGHEST.value\n      var executed = false\n\n      override suspend fun execute(connection: TestConnection) {\n        executed = true\n      }\n    }\n\n    class LowPriorityMigration : DatabaseMigration<TestConnection> {\n      override val order: Int = MigrationPriority.LOWEST.value\n      var executed = false\n\n      override suspend fun execute(connection: TestConnection) {\n        executed = true\n      }\n    }\n\n    class ConfigurableMigration(\n      val config: String\n    ) : DatabaseMigration<TestConnection> {\n      override val order: Int = 5\n      var executed = false\n      var executedConfig: String? = null\n\n      override suspend fun execute(connection: TestConnection) {\n        executed = true\n        executedConfig = config\n      }\n    }\n\n    test(\"should register migration by class\") {\n      val collection = MigrationCollection<TestConnection>()\n\n      collection.register<SimpleMigration>()\n\n      collection.run(TestConnection(\"test\"))\n      // If no exception, registration worked\n    }\n\n    test(\"should register migration with instance\") {\n      val collection = MigrationCollection<TestConnection>()\n      val migration = SimpleMigration()\n\n      collection.register(SimpleMigration::class, migration)\n      collection.run(TestConnection(\"test\"))\n\n      migration.executed shouldBe true\n    }\n\n    test(\"should register migration with factory function\") {\n      val collection = MigrationCollection<TestConnection>()\n\n      collection.register<ConfigurableMigration> {\n        ConfigurableMigration(\"custom-config\")\n      }\n      collection.run(TestConnection(\"test\"))\n    }\n\n    test(\"should not replace existing migration with register by class\") {\n      val collection = MigrationCollection<TestConnection>()\n\n      // First register creates the instance\n      collection.register<SimpleMigration>()\n      // Second register should not replace (uses putIfAbsent)\n      collection.register<SimpleMigration>()\n\n      collection.run(TestConnection(\"test\"))\n      // Test passes if no exception - only one migration executed\n    }\n\n    test(\"should replace migration with replace method\") {\n      val collection = MigrationCollection<TestConnection>()\n      val first = SimpleMigration()\n      val replacement = SimpleMigration()\n\n      collection.register(SimpleMigration::class, first)\n      collection.replace(SimpleMigration::class, replacement)\n      collection.run(TestConnection(\"test\"))\n\n      first.executed shouldBe false\n      replacement.executed shouldBe true\n    }\n\n    test(\"should replace migration using factory function\") {\n      val collection = MigrationCollection<TestConnection>()\n      val original = ConfigurableMigration(\"original\")\n\n      collection.register(ConfigurableMigration::class, original)\n      collection.replace<ConfigurableMigration> {\n        ConfigurableMigration(\"replaced\")\n      }\n      collection.run(TestConnection(\"test\"))\n\n      original.executed shouldBe false\n    }\n\n    test(\"should replace one migration type with another\") {\n      val collection = MigrationCollection<TestConnection>()\n\n      collection.register<SimpleMigration>()\n      collection.replace<SimpleMigration, AnotherMigration>()\n      collection.run(TestConnection(\"test\"))\n    }\n\n    test(\"should execute migrations in order\") {\n      val collection = MigrationCollection<TestConnection>()\n      val executionOrder = mutableListOf<Int>()\n\n      val migration1 = object : DatabaseMigration<TestConnection> {\n        override val order: Int = 10\n\n        override suspend fun execute(connection: TestConnection) {\n          executionOrder.add(10)\n        }\n      }\n\n      val migration2 = object : DatabaseMigration<TestConnection> {\n        override val order: Int = 5\n\n        override suspend fun execute(connection: TestConnection) {\n          executionOrder.add(5)\n        }\n      }\n\n      val migration3 = object : DatabaseMigration<TestConnection> {\n        override val order: Int = 15\n\n        override suspend fun execute(connection: TestConnection) {\n          executionOrder.add(15)\n        }\n      }\n\n      collection.register(SimpleMigration::class, migration1)\n      collection.register(AnotherMigration::class, migration2)\n      collection.register(HighPriorityMigration::class, migration3)\n      collection.run(TestConnection(\"test\"))\n\n      executionOrder shouldContainExactly listOf(5, 10, 15)\n    }\n\n    test(\"should execute high priority migrations first\") {\n      val collection = MigrationCollection<TestConnection>()\n      val executionOrder = mutableListOf<String>()\n\n      val highPriority = object : DatabaseMigration<TestConnection> {\n        override val order: Int = MigrationPriority.HIGHEST.value\n\n        override suspend fun execute(connection: TestConnection) {\n          executionOrder.add(\"high\")\n        }\n      }\n\n      val normalPriority = object : DatabaseMigration<TestConnection> {\n        override val order: Int = 0\n\n        override suspend fun execute(connection: TestConnection) {\n          executionOrder.add(\"normal\")\n        }\n      }\n\n      val lowPriority = object : DatabaseMigration<TestConnection> {\n        override val order: Int = MigrationPriority.LOWEST.value\n\n        override suspend fun execute(connection: TestConnection) {\n          executionOrder.add(\"low\")\n        }\n      }\n\n      collection.register(LowPriorityMigration::class, lowPriority)\n      collection.register(SimpleMigration::class, normalPriority)\n      collection.register(HighPriorityMigration::class, highPriority)\n      collection.run(TestConnection(\"test\"))\n\n      executionOrder shouldContainExactly listOf(\"high\", \"normal\", \"low\")\n    }\n\n    test(\"should pass connection to migrations\") {\n      val collection = MigrationCollection<TestConnection>()\n      var receivedConnection: TestConnection? = null\n\n      val capturingMigration = object : DatabaseMigration<TestConnection> {\n        override val order: Int = 1\n\n        override suspend fun execute(connection: TestConnection) {\n          receivedConnection = connection\n        }\n      }\n\n      collection.register(SimpleMigration::class, capturingMigration)\n      val testConnection = TestConnection(\"my-connection\")\n      collection.run(testConnection)\n\n      receivedConnection shouldBe testConnection\n    }\n\n    test(\"should handle empty collection\") {\n      val collection = MigrationCollection<TestConnection>()\n\n      collection.run(TestConnection(\"test\"))\n      // No exception means success\n    }\n\n    test(\"should support fluent chaining\") {\n      val collection = MigrationCollection<TestConnection>()\n\n      val result = collection\n        .register<SimpleMigration>()\n        .register<AnotherMigration>()\n\n      result shouldBe collection\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/database/migrations/SupportsMigrationsTest.kt",
    "content": "package com.trendyol.stove.database.migrations\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.types.shouldBeSameInstanceAs\n\n/**\n * Unit tests for the SupportsMigrations interface.\n */\nclass SupportsMigrationsTest :\n  FunSpec({\n\n    /**\n     * Simple migration context for testing.\n     */\n    data class TestMigrationContext(\n      val connectionString: String\n    )\n\n    /**\n     * Test options class implementing SupportsMigrations.\n     */\n    class TestSystemOptions(\n      val name: String\n    ) : SupportsMigrations<TestMigrationContext, TestSystemOptions> {\n      override val migrationCollection: MigrationCollection<TestMigrationContext> = MigrationCollection()\n    }\n\n    /**\n     * Test migration that records execution.\n     */\n    class TestMigration : DatabaseMigration<TestMigrationContext> {\n      override val order: Int = 1\n      var executed = false\n      var executedContext: TestMigrationContext? = null\n\n      override suspend fun execute(connection: TestMigrationContext) {\n        executed = true\n        executedContext = connection\n      }\n    }\n\n    /**\n     * Another test migration with higher order.\n     */\n    class TestMigration2 : DatabaseMigration<TestMigrationContext> {\n      override val order: Int = 2\n      var executed = false\n\n      override suspend fun execute(connection: TestMigrationContext) {\n        executed = true\n      }\n    }\n\n    test(\"migrations() should return the same instance for fluent chaining\") {\n      val options = TestSystemOptions(\"test\")\n      val result = options.migrations { }\n\n      result shouldBeSameInstanceAs options\n    }\n\n    test(\"migrations() should allow registering migrations\") {\n      val options = TestSystemOptions(\"test\")\n      val migration = TestMigration()\n\n      options.migrations {\n        register(TestMigration::class, migration)\n      }\n\n      // Run migrations and verify\n      val context = TestMigrationContext(\"test-connection\")\n      options.migrationCollection.run(context)\n\n      migration.executed shouldBe true\n      migration.executedContext shouldBe context\n    }\n\n    test(\"migrations() should allow multiple migrations to be registered\") {\n      val options = TestSystemOptions(\"test\")\n      val migration1 = TestMigration()\n      val migration2 = TestMigration2()\n\n      options.migrations {\n        register(TestMigration::class, migration1)\n        register(TestMigration2::class, migration2)\n      }\n\n      // Run migrations\n      val context = TestMigrationContext(\"test-connection\")\n      options.migrationCollection.run(context)\n\n      migration1.executed shouldBe true\n      migration2.executed shouldBe true\n    }\n\n    test(\"migrationCollection should be unique per instance\") {\n      val options1 = TestSystemOptions(\"test1\")\n      val options2 = TestSystemOptions(\"test2\")\n\n      options1.migrationCollection shouldBe options1.migrationCollection\n      options1.migrationCollection.hashCode() != options2.migrationCollection.hashCode()\n    }\n\n    test(\"migrations can be chained with other builder methods\") {\n      class ChainableOptions(\n        val name: String,\n        var configured: Boolean = false\n      ) : SupportsMigrations<TestMigrationContext, ChainableOptions> {\n        override val migrationCollection: MigrationCollection<TestMigrationContext> = MigrationCollection()\n\n        fun configure(): ChainableOptions {\n          configured = true\n          return this\n        }\n      }\n\n      val options = ChainableOptions(\"test\")\n        .configure()\n        .migrations {\n          register<TestMigration>()\n        }\n\n      options.configured shouldBe true\n      options.name shouldBe \"test\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/http/StoveHttpResponseTest.kt",
    "content": "package com.trendyol.stove.http\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.types.shouldBeInstanceOf\n\nclass StoveHttpResponseTest :\n  FunSpec({\n\n    context(\"Bodiless\") {\n      test(\"should store status and headers\") {\n        val response = StoveHttpResponse.Bodiless(\n          status = 200,\n          headers = mapOf(\"Content-Type\" to \"application/json\")\n        )\n\n        response.status shouldBe 200\n        response.headers shouldBe mapOf(\"Content-Type\" to \"application/json\")\n      }\n\n      test(\"should handle empty headers\") {\n        val response = StoveHttpResponse.Bodiless(\n          status = 404,\n          headers = emptyMap()\n        )\n\n        response.status shouldBe 404\n        response.headers shouldBe emptyMap()\n      }\n\n      test(\"should be instance of StoveHttpResponse\") {\n        val response = StoveHttpResponse.Bodiless(status = 200, headers = emptyMap())\n\n        response.shouldBeInstanceOf<StoveHttpResponse>()\n      }\n\n      test(\"data class equality should work\") {\n        val response1 = StoveHttpResponse.Bodiless(status = 200, headers = mapOf(\"key\" to \"value\"))\n        val response2 = StoveHttpResponse.Bodiless(status = 200, headers = mapOf(\"key\" to \"value\"))\n\n        response1 shouldBe response2\n      }\n\n      test(\"copy should work\") {\n        val original = StoveHttpResponse.Bodiless(status = 200, headers = emptyMap())\n        val copied = original.copy(status = 201)\n\n        copied.status shouldBe 201\n        copied.headers shouldBe emptyMap()\n      }\n    }\n\n    context(\"WithBody\") {\n      test(\"should store status, headers, and body\") {\n        val response = StoveHttpResponse.WithBody(\n          status = 200,\n          headers = mapOf(\"Content-Type\" to \"application/json\"),\n          body = { \"test body\" }\n        )\n\n        response.status shouldBe 200\n        response.headers shouldBe mapOf(\"Content-Type\" to \"application/json\")\n      }\n\n      test(\"body should execute suspend function\") {\n        var executed = false\n        val response = StoveHttpResponse.WithBody(\n          status = 200,\n          headers = emptyMap(),\n          body = {\n            executed = true\n            \"result\"\n          }\n        )\n\n        val result = response.body()\n\n        executed shouldBe true\n        result shouldBe \"result\"\n      }\n\n      test(\"should handle different body types\") {\n        data class User(\n          val id: Int,\n          val name: String\n        )\n\n        val response = StoveHttpResponse.WithBody(\n          status = 200,\n          headers = emptyMap(),\n          body = { User(1, \"John\") }\n        )\n\n        val user = response.body()\n\n        user.id shouldBe 1\n        user.name shouldBe \"John\"\n      }\n\n      test(\"should be instance of StoveHttpResponse\") {\n        val response = StoveHttpResponse.WithBody(\n          status = 200,\n          headers = emptyMap(),\n          body = { \"body\" }\n        )\n\n        response.shouldBeInstanceOf<StoveHttpResponse>()\n      }\n\n      test(\"should handle error status codes\") {\n        val response = StoveHttpResponse.WithBody(\n          status = 500,\n          headers = mapOf(\"X-Error\" to \"Internal Server Error\"),\n          body = { mapOf(\"error\" to \"Something went wrong\") }\n        )\n\n        response.status shouldBe 500\n        response.body() shouldBe mapOf(\"error\" to \"Something went wrong\")\n      }\n    }\n\n    context(\"sealed class behavior\") {\n      test(\"should pattern match on response type\") {\n        val bodiless: StoveHttpResponse = StoveHttpResponse.Bodiless(204, emptyMap())\n        val withBody: StoveHttpResponse = StoveHttpResponse.WithBody(200, emptyMap()) { \"body\" }\n\n        val bodilessResult = when (bodiless) {\n          is StoveHttpResponse.Bodiless -> \"no body\"\n          is StoveHttpResponse.WithBody<*> -> \"has body\"\n        }\n\n        val withBodyResult = when (withBody) {\n          is StoveHttpResponse.Bodiless -> \"no body\"\n          is StoveHttpResponse.WithBody<*> -> \"has body\"\n        }\n\n        bodilessResult shouldBe \"no body\"\n        withBodyResult shouldBe \"has body\"\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/messaging/ObservationTest.kt",
    "content": "package com.trendyol.stove.messaging\n\nimport arrow.core.None\nimport arrow.core.Some\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.types.shouldBeInstanceOf\n\nclass ObservationTest :\n  FunSpec({\n\n    val testMetadata = MessageMetadata(\n      topic = \"test-topic\",\n      key = \"test-key\",\n      headers = mapOf(\"header1\" to \"value1\")\n    )\n\n    context(\"MessageMetadata\") {\n      test(\"should store topic, key, and headers\") {\n        val metadata = MessageMetadata(\n          topic = \"orders\",\n          key = \"order-123\",\n          headers = mapOf(\"traceId\" to \"abc123\", \"version\" to 1)\n        )\n\n        metadata.topic shouldBe \"orders\"\n        metadata.key shouldBe \"order-123\"\n        metadata.headers[\"traceId\"] shouldBe \"abc123\"\n        metadata.headers[\"version\"] shouldBe 1\n      }\n\n      test(\"should support empty headers\") {\n        val metadata = MessageMetadata(\n          topic = \"events\",\n          key = \"event-1\",\n          headers = emptyMap()\n        )\n\n        metadata.headers shouldBe emptyMap()\n      }\n    }\n\n    context(\"SuccessfulParsedMessage\") {\n      test(\"should implement ParsedMessage\") {\n        val message = SuccessfulParsedMessage(\n          message = Some(\"test-content\"),\n          metadata = testMetadata\n        )\n\n        message.shouldBeInstanceOf<ParsedMessage<String>>()\n      }\n\n      test(\"should store message and metadata\") {\n        val message = SuccessfulParsedMessage(\n          message = Some(mapOf(\"id\" to 123)),\n          metadata = testMetadata\n        )\n\n        message.message shouldBe Some(mapOf(\"id\" to 123))\n        message.metadata shouldBe testMetadata\n      }\n\n      test(\"should handle None message\") {\n        val message = SuccessfulParsedMessage<String>(\n          message = None,\n          metadata = testMetadata\n        )\n\n        message.message shouldBe None\n      }\n    }\n\n    context(\"FailedParsedMessage\") {\n      test(\"should implement ParsedMessage\") {\n        val exception = RuntimeException(\"Parse error\")\n        val message = FailedParsedMessage(\n          message = None,\n          metadata = testMetadata,\n          reason = exception\n        )\n\n        message.shouldBeInstanceOf<ParsedMessage<String>>()\n      }\n\n      test(\"should store reason for failure\") {\n        val exception = IllegalArgumentException(\"Invalid JSON\")\n        val message = FailedParsedMessage(\n          message = None,\n          metadata = testMetadata,\n          reason = exception\n        )\n\n        message.reason shouldBe exception\n        message.reason.message shouldBe \"Invalid JSON\"\n      }\n\n      test(\"should preserve partial message on failure\") {\n        val exception = RuntimeException(\"Validation failed\")\n        val message = FailedParsedMessage(\n          message = Some(\"partial-data\"),\n          metadata = testMetadata,\n          reason = exception\n        )\n\n        message.message shouldBe Some(\"partial-data\")\n      }\n    }\n\n    context(\"ObservedMessage\") {\n      test(\"should store actual message and metadata\") {\n        data class OrderEvent(\n          val orderId: String,\n          val amount: Double\n        )\n\n        val event = OrderEvent(\"order-123\", 99.99)\n        val observed = ObservedMessage(\n          actual = event,\n          metadata = testMetadata\n        )\n\n        observed.actual shouldBe event\n        observed.metadata shouldBe testMetadata\n      }\n\n      test(\"should work with primitive types\") {\n        val observed = ObservedMessage(\n          actual = \"simple-string\",\n          metadata = testMetadata\n        )\n\n        observed.actual shouldBe \"simple-string\"\n      }\n    }\n\n    context(\"FailedObservedMessage\") {\n      test(\"should extend ObservedMessage\") {\n        val exception = RuntimeException(\"Processing failed\")\n        val failed = FailedObservedMessage(\n          actual = \"message-content\",\n          metadata = testMetadata,\n          reason = exception\n        )\n\n        failed.shouldBeInstanceOf<ObservedMessage<String>>()\n      }\n\n      test(\"should store failure reason\") {\n        val exception = IllegalStateException(\"Connection lost\")\n        val failed = FailedObservedMessage(\n          actual = 42,\n          metadata = testMetadata,\n          reason = exception\n        )\n\n        failed.actual shouldBe 42\n        failed.metadata shouldBe testMetadata\n        failed.reason shouldBe exception\n      }\n    }\n\n    context(\"Failure\") {\n      test(\"should wrap observed message with failure reason\") {\n        val observed = ObservedMessage(\n          actual = \"test-data\",\n          metadata = testMetadata\n        )\n        val exception = RuntimeException(\"Assertion failed\")\n\n        val failure = Failure(\n          message = observed,\n          reason = exception\n        )\n\n        failure.message shouldBe observed\n        failure.reason shouldBe exception\n      }\n\n      test(\"should work with FailedObservedMessage\") {\n        val innerException = IllegalArgumentException(\"Parse error\")\n        val outerException = RuntimeException(\"Retry exhausted\")\n\n        val failedObserved = FailedObservedMessage(\n          actual = \"data\",\n          metadata = testMetadata,\n          reason = innerException\n        )\n\n        val failure = Failure(\n          message = failedObserved,\n          reason = outerException\n        )\n\n        (failure.message as FailedObservedMessage).reason shouldBe innerException\n        failure.reason shouldBe outerException\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/JsonReportRendererTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass JsonReportRendererTest :\n  FunSpec({\n\n    test(\"generates valid JSON with entries and summary\") {\n      val report = TestReport(\"test-1\", \"should process order\")\n      report.record(ReportEntry.success(\"HTTP\", \"test-1\", \"POST /api\"))\n      report.record(ReportEntry.action(\"HTTP\", \"test-1\", \"status check\", passed = true))\n\n      val json = JsonReportRenderer.render(report, emptyList())\n      val parsed = ObjectMapper().readTree(json)\n\n      parsed[\"testId\"].asText() shouldBe \"test-1\"\n      parsed[\"testName\"].asText() shouldBe \"should process order\"\n      parsed[\"entries\"].size() shouldBe 2\n      parsed[\"summary\"][\"total\"].asInt() shouldBe 2\n      parsed[\"summary\"][\"passed\"].asInt() shouldBe 2\n      parsed[\"summary\"][\"failed\"].asInt() shouldBe 0\n    }\n\n    test(\"includes system snapshots\") {\n      val report = TestReport(\"test-1\", \"test\")\n      val snapshot = SystemSnapshot(\n        system = \"Kafka\",\n        state = mapOf(\"consumed\" to listOf(mapOf(\"topic\" to \"orders\"))),\n        summary = \"1 message\"\n      )\n\n      val json = JsonReportRenderer.render(report, listOf(snapshot))\n      val parsed = ObjectMapper().readTree(json)\n\n      parsed[\"systemSnapshots\"][\"Kafka\"][\"consumed\"].size() shouldBe 1\n      parsed[\"systemSnapshots\"][\"Kafka\"][\"consumed\"][0][\"topic\"].asText() shouldBe \"orders\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/PrettyConsoleRendererTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport arrow.core.Some\nimport com.trendyol.stove.tracing.TraceVisualization\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport io.kotest.matchers.string.shouldNotContain\n\nclass PrettyConsoleRendererTest :\n  FunSpec({\n    fun String.stripAnsi(): String = replace(Regex(\"\\u001B\\\\[[0-9;]*m\"), \"\")\n\n    test(\"renders summary and timeline for successful entries\") {\n      val report = TestReport(\"test-1\", \"should process order\")\n      report.record(\n        ReportEntry.success(\n          system = \"HTTP\",\n          testId = \"test-1\",\n          action = \"POST /api/orders\",\n          input = Some(\"{\" + \"\\\"id\\\":123}\"),\n          output = Some(\"201 Created\")\n        )\n      )\n\n      val rendered = PrettyConsoleRenderer.render(report, emptyList()).stripAnsi()\n\n      rendered shouldContain \"STOVE TEST EXECUTION REPORT\"\n      rendered shouldContain \"should process order\"\n      rendered shouldContain \"IN PROGRESS\"\n      rendered shouldContain \"TIMELINE\"\n      rendered shouldContain \"✓ PASSED\"\n      rendered shouldContain \"POST /api/orders\"\n      rendered shouldContain \"Input: {\\\"id\\\":123}\"\n      rendered shouldContain \"Output: 201 Created\"\n    }\n\n    test(\"renders failed assertions with expected actual and error details\") {\n      val report = TestReport(\"test-2\", \"should fail\")\n      report.record(\n        ReportEntry.failure(\n          system = \"Kafka\",\n          testId = \"test-2\",\n          action = \"shouldBePublished<OrderEvent>\",\n          error = \"expected:<2> but was:<1>\",\n          expected = Some(2),\n          actual = Some(1)\n        )\n      )\n\n      val rendered = PrettyConsoleRenderer.render(report, emptyList()).stripAnsi()\n\n      rendered shouldContain \"FAILED\"\n      rendered shouldContain \"Expected: 2\"\n      rendered shouldContain \"Actual: 1\"\n      rendered shouldContain \"Error: expected:<2> but was:<1>\"\n    }\n\n    test(\"groups sequential timeline entries by system\") {\n      val report = TestReport(\"test-2b\", \"grouped timeline\")\n      report.record(ReportEntry.success(system = \"WireMock\", testId = \"test-2b\", action = \"Register stub A\"))\n      report.record(ReportEntry.success(system = \"WireMock\", testId = \"test-2b\", action = \"Register stub B\"))\n      report.record(ReportEntry.success(system = \"HTTP\", testId = \"test-2b\", action = \"GET /api/a\"))\n      report.record(ReportEntry.success(system = \"HTTP\", testId = \"test-2b\", action = \"POST /api/b\"))\n      report.record(ReportEntry.success(system = \"Kafka\", testId = \"test-2b\", action = \"Produce event\"))\n\n      val rendered = PrettyConsoleRenderer.render(report, emptyList()).stripAnsi()\n\n      rendered shouldContain \"WIREMOCK · 2 step(s)\"\n      rendered shouldContain \"HTTP · 2 step(s)\"\n      rendered shouldContain \"KAFKA · 1 step(s)\"\n      rendered shouldContain \"#1 ✓ PASSED Register stub A\"\n      rendered shouldContain \"#5 ✓ PASSED Produce event\"\n    }\n\n    test(\"renders snapshots section with summary and state details\") {\n      val report = TestReport(\"test-3\", \"snapshot test\")\n      val snapshots = listOf(\n        SystemSnapshot(\n          system = \"Kafka\",\n          summary = \"Consumed: 1\\nPublished: 0\",\n          state = mapOf(\n            \"consumed\" to listOf(mapOf(\"topic\" to \"orders\", \"offset\" to 42)),\n            \"failed\" to emptyList<Any>()\n          )\n        )\n      )\n\n      val rendered = PrettyConsoleRenderer.render(report, snapshots).stripAnsi()\n\n      rendered shouldContain \"SYSTEM SNAPSHOTS\"\n      rendered shouldContain \"KAFKA\"\n      rendered shouldContain \"Summary\"\n      rendered shouldContain \"Consumed: 1\"\n      rendered shouldContain \"State\"\n      rendered shouldContain \"consumed: 1 item(s)\"\n      rendered shouldContain \"topic: orders\"\n      rendered shouldContain \"offset: 42\"\n    }\n\n    test(\"renders execution trace details when trace data exists\") {\n      val report = TestReport(\"test-4\", \"trace test\")\n      val trace = TraceVisualization(\n        traceId = \"trace-123\",\n        testId = \"test-4\",\n        totalSpans = 2,\n        failedSpans = 1,\n        spans = emptyList(),\n        tree = \"root span\\n└─ child span ✗\",\n        coloredTree = \"\"\n      )\n\n      report.record(\n        ReportEntry.action(\n          system = \"HTTP\",\n          testId = \"test-4\",\n          action = \"POST /api/orders\",\n          passed = false,\n          error = Some(\"500 Internal Server Error\"),\n          executionTrace = Some(trace)\n        )\n      )\n\n      val rendered = PrettyConsoleRenderer.render(report, emptyList()).stripAnsi()\n\n      rendered shouldContain \"Execution Trace\"\n      rendered shouldContain \"TraceId: trace-123\"\n      rendered shouldContain \"Spans: 2 total / 1 failed\"\n      rendered shouldContain \"root span\"\n      rendered shouldContain \"child span\"\n    }\n\n    test(\"renders empty timeline message for reports without entries\") {\n      val report = TestReport(\"test-5\", \"empty report\")\n\n      val rendered = PrettyConsoleRenderer.render(report, emptyList()).stripAnsi()\n\n      rendered shouldContain \"No actions recorded yet.\"\n      rendered shouldNotContain \"SYSTEM SNAPSHOTS\"\n    }\n\n    test(\"does not truncate very long values\") {\n      val report = TestReport(\"test-6\", \"long value\")\n      val longWord = \"x\".repeat(220)\n\n      report.record(\n        ReportEntry.success(\n          system = \"HTTP\",\n          testId = \"test-6\",\n          action = \"POST /api/long\",\n          input = Some(longWord)\n        )\n      )\n\n      val rendered = PrettyConsoleRenderer.render(report, emptyList()).stripAnsi()\n\n      rendered shouldContain \"Input:\"\n      rendered shouldContain longWord.take(40)\n      rendered shouldContain longWord.takeLast(40)\n      rendered shouldNotContain \"...\"\n    }\n\n    test(\"wraps long detail lines with hanging indentation\") {\n      val report = TestReport(\"test-6a\", \"wrapped value\")\n      val longInput = buildString {\n        append(\"CreateProductRequest(\")\n        append(\"storefrontId=1, brandId=1092122801123744494, businessUnitId=2496482862758973002, \")\n        append(\"categoryId=3583527936634204334, code=TEST_03eacf0a-6c1d-4f34-a, \")\n        append(\"requestedBarcode=BARCODE_12345678901234567890, supplierId=99)\")\n      }\n      val longOutput =\n        \"\"\"{\"productId\":3,\"code\":\"TEST_03eacf0a-6c1d-4f34-a\",\"contents\":[{\"id\":5,\"variants\":[{\"id\":5,\"barcode\":\"TYBC4ZRD0TK70YBI05\",\"requestedBarcode\":\"BARCODE_12345678901234567890\"}]}]}\"\"\"\n\n      report.record(\n        ReportEntry.success(\n          system = \"HTTP\",\n          testId = \"test-6a\",\n          action = \"POST /products\",\n          input = Some(longInput),\n          output = Some(longOutput)\n        )\n      )\n\n      val rendered = PrettyConsoleRenderer.render(report, emptyList()).stripAnsi()\n\n      rendered shouldContain \"Input: CreateProductRequest(\"\n      rendered shouldContain \"Output: {\\\"productId\\\":3\"\n      (rendered.lines().maxOf { it.length } <= 160) shouldBe true\n      rendered shouldContain \"\\n│             ST_03eacf0a-6c1d-4f34-a, requestedBarcode=BARCODE_12345678901234567890, supplierId=99)\"\n      rendered shouldContain \"\\n│              \\\"BARCODE_12345678901234567890\\\"}]}]}\"\n    }\n\n    test(\"uses a compact width for small reports\") {\n      val report = TestReport(\"test-6b\", \"small report\")\n      report.record(ReportEntry.success(system = \"HTTP\", testId = \"test-6b\", action = \"GET /health\"))\n\n      val rendered = PrettyConsoleRenderer.render(report, emptyList()).stripAnsi()\n      val widths = rendered\n        .lines()\n        .filter { it.isNotBlank() }\n        .map { it.length }\n        .toSet()\n      val width = widths.first()\n\n      widths.size shouldBe 1\n      (width < 120) shouldBe true\n    }\n\n    test(\"caps width for large reports\") {\n      val report = TestReport(\"test-6c\", \"large report\")\n      report.record(\n        ReportEntry.failure(\n          system = \"HTTP\",\n          testId = \"test-6c\",\n          action = \"GET /very/long/endpoint/that/should/not/make/the/report/unreasonably/wide\",\n          error = \"x\".repeat(300)\n        )\n      )\n\n      val rendered = PrettyConsoleRenderer.render(report, emptyList()).stripAnsi()\n      val width = rendered.lines().first { it.isNotBlank() }.length\n\n      (width <= 160) shouldBe true\n    }\n\n    test(\"renders real-world like report fixture for visual iteration\") {\n      val testId = \"ExampleTest::should create new product when send product create request from api\"\n      val report = TestReport(testId, \"should create new product when send product create request from api\")\n\n      report.record(\n        ReportEntry.success(\n          system = \"WireMock\",\n          testId = testId,\n          action = \"Register stub: GET /api/suppliers/99\",\n          metadata = mapOf(\"priority\" to 1, \"responseStatus\" to 200)\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"WireMock\",\n          testId = testId,\n          action = \"Register stub: GET /inventory/products/1\",\n          metadata = mapOf(\"priority\" to 1, \"responseStatus\" to 200)\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"WireMock\",\n          testId = testId,\n          action = \"Register stub: POST /payments/charge\",\n          metadata = mapOf(\"priority\" to 2, \"responseStatus\" to 200)\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"WireMock\",\n          testId = testId,\n          action = \"Register stub: POST /inventory/sync\",\n          metadata = mapOf(\"priority\" to 1, \"responseStatus\" to 200)\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"HTTP\",\n          testId = testId,\n          action = \"GET /api/suppliers/99\",\n          output = Some(\"\"\"{\"status\":200,\"response\":{\"id\":99,\"name\":\"supplier name\"}}\"\"\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"Kafka\",\n          testId = testId,\n          action = \"KafkaProducer.send product command\",\n          output = Some(\"topic=trendyol.stove.service.productCommand.1 offset=41\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"PostgreSQL\",\n          testId = testId,\n          action = \"INSERT INTO outbox(product_id, status)\",\n          metadata = mapOf(\"rowsAffected\" to 1, \"table\" to \"outbox\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"Kafka\",\n          testId = testId,\n          action = \"KafkaConsumer.consume product command\",\n          output = Some(\"topic=trendyol.stove.service.productCommand.1 offset=41\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"HTTP\",\n          testId = testId,\n          action = \"POST /api/product/create\",\n          input = Some(\"ProductCreateRequest(id=1, name=product name, supplierId=99)\"),\n          output = Some(\"\"\"{\"status\":201,\"response\":{\"id\":1,\"status\":\"DRAFT\"}}\"\"\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"Kafka\",\n          testId = testId,\n          action = \"KafkaProducer.send product created event\",\n          output = Some(\"topic=trendyol.stove.service.productCreated.1 offset=0\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"PostgreSQL\",\n          testId = testId,\n          action = \"UPDATE outbox SET sent=true WHERE id=91\",\n          metadata = mapOf(\"rowsAffected\" to 1, \"table\" to \"outbox\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"WireMock\",\n          testId = testId,\n          action = \"Verify downstream call: POST /inventory/sync\",\n          metadata = mapOf(\"called\" to true, \"times\" to 1)\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"PostgreSQL\",\n          testId = testId,\n          action = \"SELECT status FROM products WHERE id=1\",\n          metadata = mapOf(\"rowsReturned\" to 1, \"table\" to \"products\")\n        )\n      )\n\n      val trace = TraceVisualization(\n        traceId = \"00-49242638d15b4e29ba49750d2089633f-87ab5cab1dd5b41d-01\",\n        testId = testId,\n        totalSpans = 15,\n        failedSpans = 1,\n        spans = emptyList(),\n        tree = \"\"\"\n        GET /api/products/1 [412ms] ✗\n        | http.response.status_code: 200\n        | http.route: /api/products/{id}\n        | http.request.method: GET\n        ProductQueryController.get [109ms] ✓\n        ProductQueryService.findById [78ms] ✓\n        PostgreSQL.queryProductById [44ms] ✓\n        WireMock.inventory.getById [31ms] ✓\n        KafkaProducer.send inventory-check [27ms] ✓\n        HTTP.inventory.sync [29ms] ✓\n        PostgreSQL.updateInventoryProjection [41ms] ✓\n        InventorySyncHandler.handle [34ms] ✗\n        | messaging.kafka.topic: trendyol.stove.service.inventorySync.1\n        | error.type: INVENTORY_STATE_MISMATCH\n        \"\"\".trimIndent(),\n        coloredTree = \"\"\n      )\n\n      report.record(\n        ReportEntry.action(\n          system = \"HTTP\",\n          testId = testId,\n          action = \"GET /api/products/1\",\n          passed = false,\n          input = Some(\"ProductQueryRequest(id=1)\"),\n          output = Some(\"\"\"{\"status\":200,\"response\":{\"id\":1,\"status\":\"DRAFT\"}}\"\"\"),\n          metadata = mapOf(\"status\" to 200, \"headers\" to emptyMap<String, String>()),\n          expected = Some(\"Product status ACTIVE\"),\n          actual = Some(\"Product status DRAFT\"),\n          error = Some(\"expected:<ACTIVE> but was:<DRAFT>\"),\n          executionTrace = Some(trace)\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"Kafka\",\n          testId = testId,\n          action = \"KafkaProducer.send compensation event\",\n          output = Some(\"topic=trendyol.stove.service.productCompensation.1 offset=2\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"HTTP\",\n          testId = testId,\n          action = \"POST /api/product/compensate\",\n          input = Some(\"\"\"{\"id\":1,\"reason\":\"STATUS_MISMATCH\"}\"\"\"),\n          output = Some(\"\"\"{\"status\":202,\"response\":{\"queued\":true}}\"\"\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"Kafka\",\n          testId = testId,\n          action = \"KafkaConsumer.consume compensation event\",\n          output = Some(\"topic=trendyol.stove.service.productCompensation.1 offset=2\")\n        )\n      )\n\n      report.record(\n        ReportEntry.success(\n          system = \"WireMock\",\n          testId = testId,\n          action = \"Verify downstream call: GET /inventory/products/1\",\n          metadata = mapOf(\"called\" to true, \"times\" to 2)\n        )\n      )\n\n      val snapshots = listOf(\n        SystemSnapshot(\n          system = \"HTTP\",\n          state = mapOf(\n            \"requests\" to listOf(\n              mapOf(\"method\" to \"GET\", \"path\" to \"/api/suppliers/99\", \"status\" to 200),\n              mapOf(\"method\" to \"POST\", \"path\" to \"/api/product/create\", \"status\" to 201),\n              mapOf(\"method\" to \"GET\", \"path\" to \"/api/products/1\", \"status\" to 200),\n              mapOf(\"method\" to \"POST\", \"path\" to \"/api/product/compensate\", \"status\" to 202)\n            ),\n            \"lastRequest\" to mapOf(\"method\" to \"GET\", \"path\" to \"/api/products/1\"),\n            \"lastResponse\" to mapOf(\"status\" to 200, \"body\" to mapOf(\"id\" to 1, \"status\" to \"DRAFT\"))\n          ),\n          summary = \"Requests (this test): 4\\nLast response status: 200\"\n        ),\n        SystemSnapshot(\n          system = \"Kafka\",\n          summary = \"\"\"\n            Consumed (this test): 3\n            Produced (this test): 4\n            Failed (this test): 1\n          \"\"\".trimIndent(),\n          state = mapOf(\n            \"consumed\" to listOf(\n              mapOf(\n                \"messageId\" to \"consumed-1\",\n                \"topic\" to \"trendyol.stove.service.productCreated.1\",\n                \"key\" to 1,\n                \"offset\" to 0,\n                \"headers\" to mapOf(\n                  \"traceparent\" to \"00-49242638d15b4e29ba49750d2089633f-87ab5cab1dd5b41d-01\",\n                  \"baggage\" to \"stove.test.id=$testId\",\n                  \"__TypeId__\" to \"stove.spring.example4x.application.handlers.ProductCreatedEvent\"\n                ),\n                \"value\" to mapOf(\"id\" to 1, \"name\" to \"product name\", \"status\" to \"DRAFT\")\n              ),\n              mapOf(\n                \"messageId\" to \"consumed-2\",\n                \"topic\" to \"trendyol.stove.service.productCommand.1\",\n                \"key\" to 1,\n                \"offset\" to 41,\n                \"value\" to mapOf(\"id\" to 1, \"command\" to \"CREATE\", \"tags\" to listOf(\"new\", \"campaign\"))\n              ),\n              mapOf(\n                \"messageId\" to \"consumed-3\",\n                \"topic\" to \"trendyol.stove.service.productCompensation.1\",\n                \"key\" to 1,\n                \"offset\" to 2,\n                \"value\" to mapOf(\"id\" to 1, \"reason\" to \"STATUS_MISMATCH\")\n              )\n            ),\n            \"produced\" to listOf(\n              mapOf(\n                \"topic\" to \"trendyol.stove.service.productCommand.1\",\n                \"key\" to 1,\n                \"value\" to mapOf(\"id\" to 1, \"command\" to \"CREATE\")\n              ),\n              mapOf(\n                \"topic\" to \"trendyol.stove.service.productCreated.1\",\n                \"key\" to 1,\n                \"value\" to mapOf(\"id\" to 1, \"name\" to \"product name\", \"status\" to \"DRAFT\")\n              ),\n              mapOf(\n                \"topic\" to \"trendyol.stove.service.productCompensation.1\",\n                \"key\" to 1,\n                \"value\" to mapOf(\"id\" to 1, \"reason\" to \"STATUS_MISMATCH\")\n              ),\n              mapOf(\n                \"topic\" to \"trendyol.stove.service.inventorySync.1\",\n                \"key\" to 1,\n                \"value\" to mapOf(\"id\" to 1, \"expectedStatus\" to \"ACTIVE\", \"actualStatus\" to \"DRAFT\")\n              )\n            ),\n            \"failed\" to listOf(\n              mapOf(\n                \"topic\" to \"trendyol.stove.service.inventorySync.1\",\n                \"key\" to 1,\n                \"reason\" to \"INVENTORY_STATE_MISMATCH\",\n                \"payload\" to mapOf(\"id\" to 1, \"expectedStatus\" to \"ACTIVE\", \"actualStatus\" to \"DRAFT\")\n              )\n            )\n          )\n        ),\n        SystemSnapshot(\n          system = \"PostgreSQL\",\n          summary = \"\"\"\n            Select queries: 4\n            Insert queries: 2\n            Update queries: 2\n            Errors: 0\n          \"\"\".trimIndent(),\n          state = mapOf(\n            \"tables\" to mapOf(\n              \"products\" to listOf(\n                mapOf(\"id\" to 1, \"name\" to \"product name\", \"status\" to \"DRAFT\")\n              ),\n              \"outbox\" to listOf(\n                mapOf(\"id\" to 91, \"type\" to \"ProductCreatedEvent\", \"sent\" to true),\n                mapOf(\"id\" to 92, \"type\" to \"ProductCompensationEvent\", \"sent\" to false)\n              )\n            )\n          )\n        ),\n        SystemSnapshot(\n          system = \"WireMock\",\n          summary = \"\"\"\n            Registered stubs (this test): 5\n            Served requests (this test): 4 (matched: 4)\n            Unmatched requests: 0\n          \"\"\".trimIndent(),\n          state = mapOf(\n            \"registeredStubs\" to listOf(\n              mapOf(\"method\" to \"GET\", \"url\" to \"/api/suppliers/99\", \"status\" to 200),\n              mapOf(\"method\" to \"POST\", \"url\" to \"/api/product/create\", \"status\" to 201),\n              mapOf(\"method\" to \"GET\", \"url\" to \"/inventory/products/1\", \"status\" to 200),\n              mapOf(\"method\" to \"POST\", \"url\" to \"/payments/charge\", \"status\" to 200),\n              mapOf(\"method\" to \"POST\", \"url\" to \"/inventory/sync\", \"status\" to 200)\n            ),\n            \"servedRequests\" to listOf(\n              mapOf(\"method\" to \"GET\", \"url\" to \"/api/suppliers/99\", \"matched\" to true),\n              mapOf(\"method\" to \"POST\", \"url\" to \"/api/product/create\", \"matched\" to true),\n              mapOf(\"method\" to \"GET\", \"url\" to \"/inventory/products/1\", \"matched\" to true),\n              mapOf(\"method\" to \"POST\", \"url\" to \"/inventory/sync\", \"matched\" to true)\n            ),\n            \"unmatchedRequests\" to emptyList<Any>()\n          )\n        )\n      )\n\n      val rendered = PrettyConsoleRenderer.render(report, snapshots)\n      val plainRendered = rendered.stripAnsi()\n\n      println(\"\\n\" + \"=\".repeat(140))\n      println(\"VISUAL ITERATION FIXTURE - REAL WORLD REPORT\")\n      println(\"=\".repeat(140))\n      println(rendered)\n      println(\"=\".repeat(140))\n\n      plainRendered shouldContain \"STOVE TEST EXECUTION REPORT\"\n      plainRendered shouldContain \"TIMELINE\"\n      plainRendered shouldContain \"SYSTEM SNAPSHOTS\"\n      plainRendered shouldContain \"Execution Trace\"\n      plainRendered shouldContain \"InventorySyncHandler.handle [34ms] ✗\"\n      plainRendered shouldContain \"Failed (this test): 1\"\n      plainRendered shouldContain \"expected:<ACTIVE> but was:<DRAFT>\"\n      plainRendered shouldContain \"PostgreSQL\"\n      plainRendered shouldContain \"KafkaProducer.send compensation event\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/ReportEntryTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport arrow.core.Some\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass ReportEntryTest :\n  FunSpec({\n\n    test(\"ReportEntry generates correct summary\") {\n      val entry = ReportEntry.success(\"HTTP\", \"test-1\", \"POST /api/users\")\n\n      entry.summary shouldBe \"[HTTP] POST /api/users\"\n    }\n\n    test(\"ReportEntry with failed result is detected as failure\") {\n      val entry = ReportEntry.failure(\n        system = \"PostgreSQL\",\n        testId = \"test-1\",\n        action = \"Query\",\n        error = \"Row count mismatch\"\n      )\n\n      entry.isFailed shouldBe true\n      entry.isPassed shouldBe false\n    }\n\n    test(\"ReportEntry captures failure details with Option\") {\n      val entry = ReportEntry.action(\n        system = \"HTTP\",\n        testId = \"test-1\",\n        action = \"Response status check\",\n        passed = false,\n        expected = Some(200),\n        actual = Some(500),\n        error = Some(\"Expected 200 but got 500\")\n      )\n\n      entry.isFailed shouldBe true\n      entry.error shouldBe Some(\"Expected 200 but got 500\")\n      entry.summary shouldBe \"[HTTP] Response status check\"\n    }\n\n    test(\"AssertionResult.of converts boolean correctly\") {\n      AssertionResult.of(true) shouldBe AssertionResult.PASSED\n      AssertionResult.of(false) shouldBe AssertionResult.FAILED\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/ReportEventListenerTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.shouldBe\n\nclass ReportEventListenerTest :\n  FunSpec({\n\n    test(\"listener receives all lifecycle events\") {\n      val reporter = StoveReporter()\n      val events = mutableListOf<String>()\n      val listener = object : ReportEventListener {\n        override fun onTestStarted(ctx: StoveTestContext) {\n          events.add(\"started:${ctx.testId}\")\n        }\n\n        override fun onTestEnded(testId: String) {\n          events.add(\"ended:$testId\")\n        }\n\n        override fun onEntryRecorded(entry: ReportEntry) {\n          events.add(\"entry:${entry.action}\")\n        }\n      }\n\n      reporter.addListener(listener)\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n      reporter.record(ReportEntry.success(\"HTTP\", \"test-1\", \"GET /api\"))\n      reporter.endTest()\n\n      events shouldBe listOf(\"started:test-1\", \"entry:GET /api\", \"ended:test-1\")\n    }\n\n    test(\"throwing listener does not break reporter or other listeners\") {\n      val reporter = StoveReporter()\n      val received = mutableListOf<String>()\n\n      val brokenListener = object : ReportEventListener {\n        override fun onTestStarted(ctx: StoveTestContext) {\n          error(\"boom\")\n        }\n\n        override fun onEntryRecorded(entry: ReportEntry) {\n          error(\"boom\")\n        }\n\n        override fun onTestEnded(testId: String) {\n          error(\"boom\")\n        }\n      }\n\n      val goodListener = object : ReportEventListener {\n        override fun onTestStarted(ctx: StoveTestContext) {\n          received.add(\"started\")\n        }\n\n        override fun onEntryRecorded(entry: ReportEntry) {\n          received.add(\"entry\")\n        }\n\n        override fun onTestEnded(testId: String) {\n          received.add(\"ended\")\n        }\n      }\n\n      reporter.addListener(brokenListener)\n      reporter.addListener(goodListener)\n\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n      reporter.record(ReportEntry.success(\"HTTP\", \"test-1\", \"GET /api\"))\n      reporter.endTest()\n\n      received shouldBe listOf(\"started\", \"entry\", \"ended\")\n    }\n\n    test(\"removed listener stops receiving events\") {\n      val reporter = StoveReporter()\n      val events = mutableListOf<String>()\n      val listener = object : ReportEventListener {\n        override fun onEntryRecorded(entry: ReportEntry) {\n          events.add(entry.action)\n        }\n      }\n\n      reporter.addListener(listener)\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n      reporter.record(ReportEntry.success(\"HTTP\", \"test-1\", \"first\"))\n\n      reporter.removeListener(listener)\n      reporter.record(ReportEntry.success(\"HTTP\", \"test-1\", \"second\"))\n\n      events shouldHaveSize 1\n      events[0] shouldBe \"first\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/ReportsTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.PluggedSystem\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\n\nclass ReportsTest :\n  FunSpec({\n\n    context(\"reportSystemName\") {\n      test(\"should return class name without System suffix\") {\n        val stove = Stove()\n        val reports = TestReportsSystem(stove)\n\n        reports.reportSystemName shouldBe \"TestReports\"\n      }\n\n      test(\"should handle class name ending with System\") {\n        val stove = Stove()\n        val reports = AnotherTestSystem(stove)\n\n        reports.reportSystemName shouldBe \"AnotherTest\"\n      }\n    }\n\n    context(\"reporter\") {\n      test(\"should return reporter from PluggedSystem\") {\n        val stove = Stove()\n        val reports = TestReportsSystem(stove)\n\n        reports.reporter shouldBe stove.reporter\n      }\n\n      test(\"should throw when not a PluggedSystem\") {\n        val reports = object : Reports {}\n\n        shouldThrow<IllegalStateException> {\n          reports.reporter\n        }.message shouldContain \"Reports must be implemented by a PluggedSystem\"\n      }\n    }\n\n    context(\"snapshot\") {\n      test(\"should return default snapshot\") {\n        val stove = Stove()\n        val reports = TestReportsSystem(stove)\n\n        val snapshot = reports.snapshot()\n\n        snapshot.system shouldBe \"TestReports\"\n        snapshot.state shouldBe emptyMap()\n        snapshot.summary shouldBe \"No detailed state available\"\n      }\n\n      test(\"can be overridden to provide custom snapshot\") {\n        val stove = Stove()\n        val reports = CustomSnapshotSystem(stove)\n\n        val snapshot = reports.snapshot()\n\n        snapshot.system shouldBe \"CustomSnapshot\"\n        snapshot.state shouldBe mapOf(\"key\" to \"value\")\n        snapshot.summary shouldBe \"Custom snapshot\"\n      }\n    }\n\n    context(\"report\") {\n      test(\"should record success entry and return result\") {\n        val stove = Stove()\n        val reports = TestReportsSystem(stove)\n        val reporter = stove.reporter\n        reporter.startTest(StoveTestContext(\"test-id\", \"test-name\", \"spec\"))\n\n        val result = reports.report(action = \"action\") { \"ok\" }\n\n        result shouldBe \"ok\"\n        reporter.currentTest().entries().size shouldBe 1\n        reporter\n          .currentTest()\n          .entries()\n          .first()\n          .isPassed shouldBe true\n      }\n\n      test(\"should record failure entry and rethrow\") {\n        val stove = Stove()\n        val reports = TestReportsSystem(stove)\n        val reporter = stove.reporter\n        reporter.startTest(StoveTestContext(\"test-id\", \"test-name\", \"spec\"))\n\n        val error = shouldThrow<IllegalStateException> {\n          reports.report(action = \"action\") { error(\"boom\") }\n        }\n\n        error.message shouldBe \"boom\"\n        reporter\n          .currentTest()\n          .entries()\n          .first()\n          .isFailed shouldBe true\n      }\n    }\n  })\n\n/**\n * Test implementation of Reports interface that also implements PluggedSystem.\n */\nprivate class TestReportsSystem(\n  override val stove: Stove\n) : PluggedSystem,\n  Reports {\n  override fun then(): Stove = stove\n\n  override fun close() = Unit\n}\n\n/**\n * Another test system to verify suffix removal.\n */\nprivate class AnotherTestSystem(\n  override val stove: Stove\n) : PluggedSystem,\n  Reports {\n  override fun then(): Stove = stove\n\n  override fun close() = Unit\n}\n\n/**\n * Test system with custom snapshot.\n */\nprivate class CustomSnapshotSystem(\n  override val stove: Stove\n) : PluggedSystem,\n  Reports {\n  override fun then(): Stove = stove\n\n  override fun close() = Unit\n\n  override fun snapshot(): SystemSnapshot = SystemSnapshot(\n    system = reportSystemName,\n    state = mapOf(\"key\" to \"value\"),\n    summary = \"Custom snapshot\"\n  )\n}\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/StoveReporterTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.nulls.shouldNotBeNull\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport io.kotest.matchers.string.shouldNotBeEmpty\n\nclass StoveReporterTest :\n  FunSpec({\n\n    test(\"starts test and creates report\") {\n      val reporter = StoveReporter()\n      val ctx = StoveTestContext(\"TestSpec::test1\", \"test1\", \"TestSpec\")\n\n      reporter.startTest(ctx)\n      val report = reporter.currentTest()\n\n      report.testId shouldBe \"TestSpec::test1\"\n      report.testName shouldBe \"test1\"\n    }\n\n    test(\"records entries when enabled\") {\n      val reporter = StoveReporter()\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n\n      reporter.record(ReportEntry.success(\"HTTP\", \"test-1\", \"GET /api\"))\n\n      reporter.currentTest().entries() shouldHaveSize 1\n    }\n\n    test(\"ignores entries when disabled\") {\n      val reporter = StoveReporter(isEnabled = false)\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n\n      reporter.record(ReportEntry.success(\"HTTP\", \"test-1\", \"GET /api\"))\n\n      reporter.currentTest().entries() shouldHaveSize 0\n    }\n\n    test(\"uses default test ID when no context\") {\n      val reporter = StoveReporter()\n\n      reporter.currentTestId() shouldBe \"default\"\n      reporter.currentTest().testId shouldBe \"default\"\n    }\n\n    test(\"clears context after endTest\") {\n      val reporter = StoveReporter()\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n\n      reporter.endTest()\n\n      reporter.currentTestId() shouldBe \"default\"\n    }\n\n    test(\"endTest emits lifecycle event when only StoveTestContextHolder is set\") {\n      val reporter = StoveReporter()\n      val ended = mutableListOf<String>()\n      val ctx = StoveTestContext(\"test-holder\", \"holder test\")\n      val listener = object : ReportEventListener {\n        override fun onTestEnded(testId: String) {\n          ended.add(testId)\n        }\n      }\n\n      reporter.addListener(listener)\n      StoveTestContextHolder.set(ctx)\n      try {\n        reporter.endTest()\n      } finally {\n        StoveTestContextHolder.clear()\n      }\n\n      ended shouldBe listOf(\"test-holder\")\n    }\n\n    test(\"endTest keeps current test context visible while listeners run\") {\n      val reporter = StoveReporter()\n      val observedTestIds = mutableListOf<String>()\n      val ctx = StoveTestContext(\"test-listener\", \"listener test\")\n      val listener = object : ReportEventListener {\n        override fun onTestEnded(testId: String) {\n          observedTestIds.add(reporter.currentTestId())\n        }\n      }\n\n      reporter.addListener(listener)\n      reporter.startTest(ctx)\n\n      reporter.endTest()\n\n      observedTestIds shouldBe listOf(\"test-listener\")\n      reporter.currentTestId() shouldBe \"default\"\n    }\n\n    test(\"detects failures correctly\") {\n      val reporter = StoveReporter()\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n\n      reporter.hasFailures() shouldBe false\n\n      reporter.record(ReportEntry.failure(\"HTTP\", \"test-1\", \"check\", \"assertion failed\"))\n\n      reporter.hasFailures() shouldBe true\n    }\n\n    test(\"currentTestOrNull returns null when no test started\") {\n      val reporter = StoveReporter()\n\n      reporter.currentTestOrNull() shouldBe null\n    }\n\n    test(\"currentTestOrNull returns test after startTest\") {\n      val reporter = StoveReporter()\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n\n      reporter.currentTestOrNull().shouldNotBeNull()\n      reporter.currentTestOrNull()?.testId shouldBe \"test-1\"\n    }\n\n    test(\"clear removes entries from current test\") {\n      val reporter = StoveReporter()\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n      reporter.record(ReportEntry.success(\"HTTP\", \"test-1\", \"GET /api\"))\n\n      reporter.currentTest().entries() shouldHaveSize 1\n\n      reporter.clear()\n\n      reporter.currentTest().entries() shouldHaveSize 0\n    }\n\n    test(\"dump returns empty string when no test exists\") {\n      val reporter = StoveReporter()\n\n      val result = reporter.dump(PrettyConsoleRenderer)\n\n      result shouldBe \"\"\n    }\n\n    test(\"dumpIfFailed returns empty string when no failures\") {\n      val reporter = StoveReporter()\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n      reporter.record(ReportEntry.success(\"HTTP\", \"test-1\", \"GET /api\"))\n\n      val result = reporter.dumpIfFailed()\n\n      result shouldBe \"\"\n    }\n\n    test(\"dumpIfFailed returns report when there are failures\") {\n      val reporter = StoveReporter()\n      reporter.startTest(StoveTestContext(\"test-1\", \"test1\"))\n      reporter.record(ReportEntry.failure(\"HTTP\", \"test-1\", \"GET /api\", \"Not found\"))\n\n      val result = reporter.dumpIfFailed()\n\n      result.shouldNotBeEmpty()\n      result.replace(Regex(\"\\u001B\\\\[[0-9;]*m\"), \"\") shouldContain \"FAILED\"\n    }\n\n    test(\"collectSnapshots returns empty list when Stove not initialized\") {\n      val reporter = StoveReporter()\n\n      val snapshots = reporter.collectSnapshots()\n\n      snapshots shouldBe emptyList()\n    }\n\n    test(\"hasFailures returns false when no test context\") {\n      val reporter = StoveReporter()\n\n      reporter.hasFailures() shouldBe false\n    }\n\n    test(\"multiple tests can be tracked independently\") {\n      val reporter = StoveReporter()\n\n      reporter.startTest(StoveTestContext(\"test-1\", \"first test\"))\n      reporter.record(ReportEntry.success(\"HTTP\", \"test-1\", \"action1\"))\n      reporter.endTest()\n\n      reporter.startTest(StoveTestContext(\"test-2\", \"second test\"))\n      reporter.record(ReportEntry.failure(\"Kafka\", \"test-2\", \"action2\", \"error\"))\n      reporter.endTest()\n\n      // Start test-1 again to check its state\n      reporter.startTest(StoveTestContext(\"test-1\", \"first test\"))\n      reporter.currentTest().entries() shouldHaveSize 1\n      reporter.hasFailures() shouldBe false\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/StoveTestContextTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.withContext\n\nclass StoveTestContextTest :\n  FunSpec({\n\n    test(\"StoveTestContext is a CoroutineContext element\") {\n      val ctx = StoveTestContext(\"TestSpec::test1\", \"test1\", \"TestSpec\")\n\n      ctx.testId shouldBe \"TestSpec::test1\"\n      ctx.testName shouldBe \"test1\"\n      ctx.specName shouldBe \"TestSpec\"\n      ctx.key shouldBe StoveTestContext.Key\n    }\n\n    test(\"currentStoveTestContext retrieves context from coroutine\") {\n      val ctx = StoveTestContext(\"test-1\", \"test1\")\n      val contextWithStove = currentCoroutineContext() + ctx\n\n      withContext(contextWithStove) {\n        currentStoveTestContext() shouldBe ctx\n      }\n    }\n\n    test(\"StoveTestContextHolder stores context in ThreadLocal\") {\n      val ctx = StoveTestContext(\"test-1\", \"test1\")\n\n      StoveTestContextHolder.set(ctx)\n      StoveTestContextHolder.get() shouldBe ctx\n\n      StoveTestContextHolder.clear()\n      StoveTestContextHolder.get() shouldBe null\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/StoveTestExceptionsTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport io.kotest.matchers.types.shouldBeInstanceOf\n\nclass StoveTestExceptionsTest :\n  FunSpec({\n\n    context(\"StoveTestFailureException\") {\n      test(\"should extend AssertionError\") {\n        val exception = StoveTestFailureException(\n          originalMessage = \"Test failed\",\n          stoveReport = \"Report content\"\n        )\n\n        exception.shouldBeInstanceOf<AssertionError>()\n      }\n\n      test(\"should format message with original message and report\") {\n        val exception = StoveTestFailureException(\n          originalMessage = \"expected:<200> but was:<500>\",\n          stoveReport = \"HTTP GET /api/test - FAILED\"\n        )\n\n        exception.message shouldContain \"expected:<200> but was:<500>\"\n        exception.message shouldContain \"STOVE EXECUTION REPORT\"\n        exception.message shouldContain \"HTTP GET /api/test - FAILED\"\n      }\n\n      test(\"should preserve cause\") {\n        val cause = RuntimeException(\"Original error\")\n        val exception = StoveTestFailureException(\n          originalMessage = \"Test failed\",\n          stoveReport = \"Report\",\n          cause = cause\n        )\n\n        exception.cause shouldBe cause\n      }\n\n      test(\"should copy stack trace from cause\") {\n        val cause = RuntimeException(\"Original error\")\n        val originalStackTrace = cause.stackTrace\n\n        val exception = StoveTestFailureException(\n          originalMessage = \"Test failed\",\n          stoveReport = \"Report\",\n          cause = cause\n        )\n\n        exception.stackTrace shouldBe originalStackTrace\n      }\n\n      test(\"should handle null cause\") {\n        val exception = StoveTestFailureException(\n          originalMessage = \"Test failed\",\n          stoveReport = \"Report\",\n          cause = null\n        )\n\n        exception.cause shouldBe null\n      }\n\n      test(\"should include separator line in message\") {\n        val exception = StoveTestFailureException(\n          originalMessage = \"Test failed\",\n          stoveReport = \"Report\"\n        )\n\n        exception.message shouldContain \"═══════════════════════════════════════════════════════════════════════════════\"\n      }\n    }\n\n    context(\"StoveTestErrorException\") {\n      test(\"should extend Exception\") {\n        val exception = StoveTestErrorException(\n          originalMessage = \"Error occurred\",\n          stoveReport = \"Report content\"\n        )\n\n        exception.shouldBeInstanceOf<Exception>()\n      }\n\n      test(\"should format message with original message and report\") {\n        val exception = StoveTestErrorException(\n          originalMessage = \"Connection refused\",\n          stoveReport = \"Kafka publish - ERROR\"\n        )\n\n        exception.message shouldContain \"Connection refused\"\n        exception.message shouldContain \"STOVE EXECUTION REPORT\"\n        exception.message shouldContain \"Kafka publish - ERROR\"\n      }\n\n      test(\"should preserve cause\") {\n        val cause = IllegalStateException(\"Invalid state\")\n        val exception = StoveTestErrorException(\n          originalMessage = \"Error occurred\",\n          stoveReport = \"Report\",\n          cause = cause\n        )\n\n        exception.cause shouldBe cause\n      }\n\n      test(\"should copy stack trace from cause\") {\n        val cause = IllegalStateException(\"Invalid state\")\n        val originalStackTrace = cause.stackTrace\n\n        val exception = StoveTestErrorException(\n          originalMessage = \"Error occurred\",\n          stoveReport = \"Report\",\n          cause = cause\n        )\n\n        exception.stackTrace shouldBe originalStackTrace\n      }\n\n      test(\"should handle null cause\") {\n        val exception = StoveTestErrorException(\n          originalMessage = \"Error occurred\",\n          stoveReport = \"Report\",\n          cause = null\n        )\n\n        exception.cause shouldBe null\n      }\n    }\n\n    context(\"message formatting\") {\n      test(\"should handle multiline original message\") {\n        val exception = StoveTestFailureException(\n          originalMessage = \"Line 1\\nLine 2\\nLine 3\",\n          stoveReport = \"Report\"\n        )\n\n        exception.message shouldContain \"Line 1\"\n        exception.message shouldContain \"Line 2\"\n        exception.message shouldContain \"Line 3\"\n      }\n\n      test(\"should handle multiline report\") {\n        val exception = StoveTestFailureException(\n          originalMessage = \"Test failed\",\n          stoveReport = \"Step 1: OK\\nStep 2: FAILED\\nStep 3: SKIPPED\"\n        )\n\n        exception.message shouldContain \"Step 1: OK\"\n        exception.message shouldContain \"Step 2: FAILED\"\n        exception.message shouldContain \"Step 3: SKIPPED\"\n      }\n\n      test(\"should handle empty report\") {\n        val exception = StoveTestFailureException(\n          originalMessage = \"Test failed\",\n          stoveReport = \"\"\n        )\n\n        exception.message shouldContain \"Test failed\"\n        exception.message?.trim() shouldBe \"Test failed\"\n      }\n\n      test(\"should handle special characters in message\") {\n        val exception = StoveTestFailureException(\n          originalMessage = \"Expected: {\\\"id\\\": 1} but was: {\\\"id\\\": 2}\",\n          stoveReport = \"JSON comparison failed\"\n        )\n\n        exception.message shouldContain \"{\\\"id\\\": 1}\"\n        exception.message shouldContain \"{\\\"id\\\": 2}\"\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/SystemSnapshotTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass SystemSnapshotTest :\n  FunSpec({\n\n    test(\"should store system name, state, and summary\") {\n      val snapshot = SystemSnapshot(\n        system = \"Kafka\",\n        state = mapOf(\n          \"consumed\" to listOf(\"msg1\", \"msg2\"),\n          \"published\" to emptyList<String>()\n        ),\n        summary = \"Consumed: 2, Published: 0\"\n      )\n\n      snapshot.system shouldBe \"Kafka\"\n      snapshot.state[\"consumed\"] shouldBe listOf(\"msg1\", \"msg2\")\n      snapshot.summary shouldBe \"Consumed: 2, Published: 0\"\n    }\n\n    test(\"should handle empty state\") {\n      val snapshot = SystemSnapshot(\n        system = \"HTTP\",\n        state = emptyMap(),\n        summary = \"No requests recorded\"\n      )\n\n      snapshot.state shouldBe emptyMap()\n    }\n\n    test(\"should handle complex nested state\") {\n      val snapshot = SystemSnapshot(\n        system = \"WireMock\",\n        state = mapOf(\n          \"stubs\" to listOf(\n            mapOf(\"url\" to \"/api/users\", \"method\" to \"GET\"),\n            mapOf(\"url\" to \"/api/orders\", \"method\" to \"POST\")\n          ),\n          \"unmatched\" to listOf(\n            mapOf(\"url\" to \"/api/unknown\", \"count\" to 3)\n          )\n        ),\n        summary = \"Stubs: 2, Unmatched: 1\"\n      )\n\n      val stubs = snapshot.state[\"stubs\"] as List<*>\n      stubs.size shouldBe 2\n    }\n\n    test(\"should handle multiline summary\") {\n      val snapshot = SystemSnapshot(\n        system = \"PostgreSQL\",\n        state = mapOf(\"tables\" to listOf(\"users\", \"orders\")),\n        summary = \"\"\"\n          |Tables: 2\n          |Rows inserted: 150\n          |Last query: SELECT * FROM users\n        \"\"\".trimMargin()\n      )\n\n      snapshot.summary.lines().size shouldBe 3\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/TestReportTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.shouldBe\n\nclass TestReportTest :\n  FunSpec({\n\n    test(\"records entries in chronological order\") {\n      val report = TestReport(\"test-1\", \"test\")\n\n      val entry1 = ReportEntry.success(\"HTTP\", \"test-1\", \"GET /api\")\n      val entry2 = ReportEntry.success(\"Kafka\", \"test-1\", \"Publish\")\n      val entry3 = ReportEntry.action(\"Kafka\", \"test-1\", \"consumed\", passed = true)\n\n      report.record(entry1)\n      report.record(entry2)\n      report.record(entry3)\n\n      report.entries() shouldHaveSize 3\n    }\n\n    test(\"filters failures correctly\") {\n      val report = TestReport(\"test-1\", \"test\")\n\n      report.record(ReportEntry.action(\"HTTP\", \"test-1\", \"check\", passed = true))\n      report.record(ReportEntry.action(\"Kafka\", \"test-1\", \"check\", passed = false))\n      report.record(ReportEntry.failure(\"PostgreSQL\", \"test-1\", \"Query\", \"timeout\"))\n\n      report.failures() shouldHaveSize 2\n      report.hasFailures() shouldBe true\n    }\n\n    test(\"filters entries by testId\") {\n      val report = TestReport(\"test-1\", \"test\")\n\n      report.record(ReportEntry.success(\"HTTP\", \"test-1\", \"action\"))\n      report.record(ReportEntry.success(\"HTTP\", \"test-2\", \"other test\"))\n\n      report.entries() shouldHaveSize 2\n      report.entriesForThisTest() shouldHaveSize 1\n      report.entriesForThisTest().all { it.testId == \"test-1\" } shouldBe true\n    }\n\n    test(\"clear removes all entries\") {\n      val report = TestReport(\"test-1\", \"test\")\n      report.record(ReportEntry.success(\"HTTP\", \"test-1\", \"action\"))\n\n      report.clear()\n\n      report.entries() shouldBe emptyList()\n    }\n\n    test(\"extension functions filter correctly\") {\n      val entries = listOf(\n        ReportEntry.success(\"HTTP\", \"test-1\", \"action\"),\n        ReportEntry.action(\"Kafka\", \"test-1\", \"check\", passed = true),\n        ReportEntry.action(\"HTTP\", \"test-1\", \"check\", passed = false)\n      )\n\n      entries.forSystem(\"HTTP\") shouldHaveSize 2\n      entries.failures() shouldHaveSize 1\n      entries.passed() shouldHaveSize 2\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/reporting/TraceProviderTest.kt",
    "content": "package com.trendyol.stove.reporting\n\nimport arrow.core.None\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass TraceProviderTest :\n  FunSpec({\n    test(\"default wait time should be 300ms\") {\n      val provider = CapturingTraceProvider()\n\n      provider.getTraceVisualizationForCurrentTest()\n\n      provider.lastWaitTime shouldBe 300L\n    }\n\n    test(\"custom wait time should be respected\") {\n      val provider = CapturingTraceProvider()\n\n      provider.getTraceVisualizationForCurrentTest(1234)\n\n      provider.lastWaitTime shouldBe 1234L\n    }\n  })\n\nprivate class CapturingTraceProvider : TraceProvider {\n  var lastWaitTime: Long? = null\n\n  override fun getTraceVisualizationForCurrentTest(waitTimeMs: Long) =\n    None.also { lastWaitTime = waitTimeMs }\n}\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/serialization/SerializationTests.kt",
    "content": "package com.trendyol.stove.serialization\n\nimport arrow.core.None\nimport com.fasterxml.jackson.core.JsonParseException\nimport com.fasterxml.jackson.databind.MapperFeature\nimport com.google.gson.JsonSyntaxException\nimport com.trendyol.stove.serialization.StoveSerde.Companion.deserialize\nimport com.trendyol.stove.serialization.StoveSerde.Companion.deserializeOption\nimport com.trendyol.stove.serialization.StoveSerde.StoveSerdeProblem\nimport io.kotest.assertions.arrow.core.shouldBeLeft\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.*\nimport io.kotest.matchers.types.shouldBeInstanceOf\nimport kotlinx.serialization.*\n\nclass SerializerTest :\n  FunSpec({\n\n    @Serializable\n    data class TestData(\n      val id: Int,\n      val name: String,\n      val tags: List<String> = listOf(),\n      @SerialName(\"created_at\")\n      val createdAt: String? = null\n    )\n\n    val testData = TestData(\n      id = 1,\n      name = \"Test Item\",\n      tags = listOf(\"tag1\", \"tag2\"),\n      createdAt = \"2024-01-01\"\n    )\n\n    context(\"StoveJacksonStringSerializer\") {\n      val serializer = StoveSerde.jackson.anyJsonStringSerde()\n\n      test(\"should serialize and deserialize object correctly\") {\n        val serialized = serializer.serialize(testData)\n        val deserialized = serializer.deserialize(serialized, TestData::class.java)\n\n        deserialized shouldBe testData\n        serialized shouldContain \"\\\"id\\\":1\"\n        serialized shouldContain \"\\\"name\\\":\\\"Test Item\\\"\"\n      }\n\n      test(\"should handle null values\") {\n        val dataWithNull = testData.copy(createdAt = null)\n        val serialized = serializer.serialize(dataWithNull)\n        val deserialized = serializer.deserialize(serialized, TestData::class.java)\n\n        deserialized shouldBe dataWithNull\n        serialized shouldNotContain \"created_at\"\n      }\n\n      test(\"should throw exception for invalid JSON\") {\n        shouldThrow<JsonParseException> {\n          serializer.deserialize(\"invalid json\", TestData::class.java)\n        }\n      }\n\n      test(\"should return None when invalid JSON is deserialized\") {\n        val a = serializer.deserializeOption<TestData>(\"invalid json\")\n        a shouldBe None\n      }\n\n      test(\"should return Left when invalid JSON is deserialized\") {\n        val op = serializer.deserializeEither(\"invalid json\", TestData::class.java)\n        op.shouldBeLeft()\n        op.value.message shouldContain \"Unrecognized token 'invalid': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\"\n        op.value.shouldBeInstanceOf<StoveSerdeProblem.BecauseOfDeserialization>()\n      }\n    }\n\n    context(\"StoveGsonStringSerializer\") {\n      val serializer = StoveSerde.gson.anyJsonStringSerde()\n\n      test(\"should serialize and deserialize object correctly\") {\n        val serialized = serializer.serialize(testData)\n        val deserialized = serializer.deserialize(serialized, TestData::class.java)\n\n        deserialized shouldBe testData\n        serialized shouldContain \"\\\"id\\\":1\"\n        serialized shouldContain \"\\\"name\\\":\\\"Test Item\\\"\"\n      }\n\n      test(\"should handle null values\") {\n        val dataWithNull = testData.copy(createdAt = null)\n        val serialized = serializer.serialize(dataWithNull)\n        val deserialized = serializer.deserialize(serialized, TestData::class.java)\n\n        deserialized shouldBe dataWithNull\n        serialized shouldNotContain \"created_at\"\n      }\n\n      test(\"should throw exception for invalid JSON\") {\n        shouldThrow<JsonSyntaxException> {\n          serializer.deserialize(\"invalid json\", TestData::class.java)\n        }\n      }\n    }\n\n    context(\"StoveKotlinxStringSerializer\") {\n      val serializer = StoveSerde.kotlinx.anyJsonStringSerde()\n\n      test(\"should serialize and deserialize object correctly\") {\n        val serialized = serializer.serialize(testData)\n        val deserialized = serializer.deserialize(serialized, TestData::class.java)\n        val deserializedTyped = serializer.deserialize<TestData>(serialized)\n\n        deserialized shouldBe testData\n        deserializedTyped shouldBe testData\n        serialized shouldContain \"\\\"id\\\":1\"\n        serialized shouldContain \"\\\"name\\\":\\\"Test Item\\\"\"\n      }\n\n      test(\"should handle null values\") {\n        val dataWithNull = testData.copy(createdAt = null)\n        val serialized = serializer.serialize(dataWithNull)\n        val deserialized = serializer.deserialize(serialized, TestData::class.java)\n\n        deserialized shouldBe dataWithNull\n        serialized shouldNotContain \"created_at\"\n      }\n\n      test(\"should throw exception for invalid JSON\") {\n        shouldThrow<SerializationException> {\n          serializer.deserialize(\"invalid json\", TestData::class.java)\n        }\n      }\n    }\n\n    context(\"Edge cases for all serializers\") {\n      val jacksonSerializer = StoveSerde.jackson.anyJsonStringSerde()\n      val gsonSerializer = StoveSerde.gson.anyJsonStringSerde()\n      val kotlinxSerializer = StoveSerde.kotlinx.anyJsonStringSerde()\n\n      test(\"should handle empty lists\") {\n        val dataWithEmptyList = testData.copy(tags = emptyList())\n\n        listOf(jacksonSerializer, gsonSerializer, kotlinxSerializer).forEach { serializer ->\n          val serialized = serializer.serialize(dataWithEmptyList)\n          val deserialized = serializer.deserialize(serialized, TestData::class.java)\n          deserialized shouldBe dataWithEmptyList\n          serialized shouldContain \"\\\"tags\\\":[]\"\n        }\n      }\n\n      test(\"should handle special characters\") {\n        val dataWithSpecialChars = testData.copy(name = \"Test \\\"Item\\\" with \\\\special/ chars\")\n\n        listOf(jacksonSerializer, gsonSerializer, kotlinxSerializer).forEach { serializer ->\n          val serialized = serializer.serialize(dataWithSpecialChars)\n          val deserialized = serializer.deserialize(serialized, TestData::class.java)\n          deserialized shouldBe dataWithSpecialChars\n        }\n      }\n    }\n\n    context(\"configuring tests\") {\n      test(\"should configure StoveGson\") {\n        val gson = StoveGson.byConfiguring {\n          setPrettyPrinting()\n        }\n\n        val serializer = StoveGsonStringSerializer<TestData>(gson)\n        val serialized = serializer.serialize(testData)\n\n        serialized shouldContain \"\\n\"\n      }\n\n      test(\"should configure StoveKotlinx\") {\n        val json = StoveKotlinx.byConfiguring {\n          ignoreUnknownKeys = false\n          prettyPrint = true\n        }\n\n        val serializer = StoveKotlinxStringSerializer<TestData>(json)\n        val serialized = serializer.serialize(testData)\n\n        serialized shouldContain \"\\n\"\n      }\n\n      test(\"should configure StoveJackson\") {\n        val objectMapper = StoveSerde.jackson.byConfiguring {\n          enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)\n          enable(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n          enable(com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT)\n        }\n        val serializer = StoveJacksonStringSerializer<TestData>(objectMapper)\n        val serialized = serializer.serialize(testData)\n\n        serialized shouldContain \"\\n\"\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/BridgeSystemGuardTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport com.trendyol.stove.reporting.StoveTestContext\nimport com.trendyol.stove.system.abstractions.PluggedSystem\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.runBlocking\nimport kotlin.reflect.KClass\n\nclass BridgeSystemGuardTest :\n  FunSpec({\n    test(\"using throws when context is not initialized\") {\n      val stove = Stove()\n      val bridge = UninitializedBridgeSystem(stove)\n      stove.getOrRegister<BridgeSystem<TestCtx>>(bridge)\n      stove.reporter.startTest(StoveTestContext(\"test-id\", \"test-name\", \"spec\"))\n\n      val error = shouldThrow<IllegalStateException> {\n        runBlocking {\n          ValidationDsl(stove).using<TestBean> { }\n        }\n      }\n\n      error.message shouldContain \"BridgeSystem context is not initialized\"\n      error.message shouldContain \"providedApplication()\"\n    }\n\n    test(\"using works after context is initialized\") {\n      val stove = Stove()\n      val bean = TestBean(\"hello\")\n      val bridge = UninitializedBridgeSystem(stove, mapOf(TestBean::class to bean))\n      stove.getOrRegister<BridgeSystem<TestCtx>>(bridge)\n\n      // Initialize context\n      runBlocking { bridge.afterRun(TestCtx()) }\n\n      stove.reporter.startTest(StoveTestContext(\"test-id\", \"test-name\", \"spec\"))\n\n      runBlocking {\n        ValidationDsl(stove).using<TestBean> {\n          value shouldBe \"hello\"\n        }\n      }\n    }\n  })\n\nprivate data class TestBean(val value: String)\n\nprivate class TestCtx\n\nprivate class UninitializedBridgeSystem(\n  override val stove: Stove,\n  private val beans: Map<KClass<*>, Any> = emptyMap()\n) : BridgeSystem<TestCtx>(stove),\n  PluggedSystem {\n  override fun then(): Stove = stove\n\n  @Suppress(\"UNCHECKED_CAST\")\n  override fun <D : Any> get(klass: KClass<D>): D =\n    beans[klass] as? D ?: error(\"Missing bean for ${klass.simpleName}\")\n}\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/BridgeSystemTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport com.trendyol.stove.reporting.StoveTestContext\nimport com.trendyol.stove.system.abstractions.PluggedSystem\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.runBlocking\nimport kotlin.reflect.KClass\n\nclass BridgeSystemTest :\n  FunSpec({\n    test(\"using records success for single bean\") {\n      val stove = Stove()\n      val serviceA = ServiceA(\"initial\")\n      val bridge = TestBridgeSystem(stove, mapOf(ServiceA::class to serviceA))\n      stove.getOrRegister<BridgeSystem<TestContext>>(bridge)\n      runBlocking { bridge.afterRun(TestContext()) }\n\n      stove.reporter.startTest(StoveTestContext(\"test-id\", \"test-name\", \"spec\"))\n\n      runBlocking {\n        ValidationDsl(stove).using<ServiceA> { value = \"updated\" }\n      }\n\n      val entry = stove.reporter\n        .currentTest()\n        .entries()\n        .single()\n      entry.isPassed shouldBe true\n      entry.action shouldContain \"Bean usage: ServiceA\"\n      serviceA.value shouldBe \"updated\"\n    }\n\n    test(\"using records failure and rethrows\") {\n      val stove = Stove()\n      val serviceA = ServiceA(\"initial\")\n      val bridge = TestBridgeSystem(stove, mapOf(ServiceA::class to serviceA))\n      stove.getOrRegister<BridgeSystem<TestContext>>(bridge)\n      runBlocking { bridge.afterRun(TestContext()) }\n\n      stove.reporter.startTest(StoveTestContext(\"test-id\", \"test-name\", \"spec\"))\n\n      val error = shouldThrow<IllegalStateException> {\n        runBlocking {\n          ValidationDsl(stove).using<ServiceA> { error(\"boom\") }\n        }\n      }\n\n      error.message shouldBe \"boom\"\n      val entry = stove.reporter\n        .currentTest()\n        .entries()\n        .single()\n      entry.isFailed shouldBe true\n      entry.action shouldContain \"Bean usage: ServiceA\"\n    }\n\n    test(\"using with two beans records success\") {\n      val stove = Stove()\n      val serviceA = ServiceA(\"a\")\n      val serviceB = ServiceB(42)\n      val bridge = TestBridgeSystem(\n        stove,\n        mapOf(\n          ServiceA::class to serviceA,\n          ServiceB::class to serviceB\n        )\n      )\n      stove.getOrRegister<BridgeSystem<TestContext>>(bridge)\n      runBlocking { bridge.afterRun(TestContext()) }\n\n      stove.reporter.startTest(StoveTestContext(\"test-id\", \"test-name\", \"spec\"))\n\n      runBlocking {\n        ValidationDsl(stove).using<ServiceA, ServiceB> { a, b ->\n          a.value shouldBe \"a\"\n          b.number shouldBe 42\n        }\n      }\n\n      val entry = stove.reporter\n        .currentTest()\n        .entries()\n        .single()\n      entry.isPassed shouldBe true\n      entry.action shouldContain \"Bean usage: ServiceA, ServiceB\"\n    }\n  })\n\nprivate data class ServiceA(\n  var value: String\n)\n\nprivate data class ServiceB(\n  val number: Int\n)\n\nprivate class TestContext\n\nprivate class TestBridgeSystem(\n  override val stove: Stove,\n  private val beans: Map<KClass<*>, Any>\n) : BridgeSystem<TestContext>(stove),\n  PluggedSystem {\n  override fun then(): Stove = stove\n\n  @Suppress(\"UNCHECKED_CAST\")\n  override fun <D : Any> get(klass: KClass<D>): D =\n    beans[klass] as? D ?: error(\"Missing bean for ${klass.simpleName}\")\n}\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/KeyedSystemTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport arrow.core.None\nimport arrow.core.Some\nimport com.trendyol.stove.system.abstractions.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldContainAll\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.types.shouldBeSameInstanceAs\nimport kotlinx.coroutines.runBlocking\n\nprivate object KeyA : SystemKey\n\nprivate object KeyB : SystemKey\n\nclass KeyedSystemTest :\n  FunSpec({\n    test(\"keyed getOrRegister stores and returns system\") {\n      val stove = Stove()\n      val system = KeyedTestSystem(stove)\n\n      val registered = stove.getOrRegister(KeyA, system)\n\n      registered shouldBeSameInstanceAs system\n    }\n\n    test(\"keyed getOrRegister returns existing instance for same key\") {\n      val stove = Stove()\n      val system1 = KeyedTestSystem(stove)\n      val system2 = KeyedTestSystem(stove)\n\n      val first = stove.getOrRegister(KeyA, system1)\n      val second = stove.getOrRegister(KeyA, system2)\n\n      first shouldBeSameInstanceAs second\n      first shouldBeSameInstanceAs system1\n    }\n\n    test(\"different keys for same type store different instances\") {\n      val stove = Stove()\n      val systemA = KeyedTestSystem(stove)\n      val systemB = KeyedTestSystem(stove)\n\n      val registeredA = stove.getOrRegister(KeyA, systemA)\n      val registeredB = stove.getOrRegister(KeyB, systemB)\n\n      registeredA shouldBeSameInstanceAs systemA\n      registeredB shouldBeSameInstanceAs systemB\n      registeredA shouldBe systemA\n      registeredB shouldBe systemB\n    }\n\n    test(\"keyed getOrNone returns None when not registered\") {\n      val stove = Stove()\n\n      stove.getOrNone<KeyedTestSystem>(KeyA) shouldBe None\n    }\n\n    test(\"keyed getOrNone returns Some when registered\") {\n      val stove = Stove()\n      val system = KeyedTestSystem(stove)\n      stove.getOrRegister(KeyA, system)\n\n      val result = stove.getOrNone<KeyedTestSystem>(KeyA)\n\n      result shouldBe Some(system)\n    }\n\n    test(\"keyed and default systems coexist independently\") {\n      val stove = Stove()\n      val defaultSystem = KeyedTestSystem(stove)\n      val keyedSystem = KeyedTestSystem(stove)\n\n      stove.getOrRegister(defaultSystem)\n      stove.getOrRegister(KeyA, keyedSystem)\n\n      stove.getOrNone<KeyedTestSystem>() shouldBe Some(defaultSystem)\n      stove.getOrNone<KeyedTestSystem>(KeyA) shouldBe Some(keyedSystem)\n    }\n\n    test(\"allRegisteredSystems includes both default and keyed systems\") {\n      val stove = Stove()\n      val defaultSystem = KeyedTestSystem(stove)\n      val keyedSystemA = KeyedTestSystem(stove)\n      val keyedSystemB = KeyedTestSystem(stove)\n\n      stove.getOrRegister(defaultSystem)\n      stove.getOrRegister(KeyA, keyedSystemA)\n      stove.getOrRegister(KeyB, keyedSystemB)\n\n      val all = stove.allRegisteredSystems()\n      all shouldHaveSize 3\n      all.shouldContainAll(defaultSystem, keyedSystemA, keyedSystemB)\n    }\n\n    test(\"systemsOf returns matching systems from both maps\") {\n      val stove = Stove()\n      val defaultSystem = KeyedLifecycleSystem(stove)\n      val keyedSystem = KeyedLifecycleSystem(stove)\n\n      stove.getOrRegister(defaultSystem)\n      stove.getOrRegister(KeyA, keyedSystem)\n\n      val runAwareSystems = stove.systemsOf<RunAware>()\n      runAwareSystems shouldHaveSize 2\n    }\n\n    test(\"allSystems returns all registered systems\") {\n      val stove = Stove()\n      val system1 = KeyedTestSystem(stove)\n      val system2 = KeyedTestSystem(stove)\n\n      stove.getOrRegister(system1)\n      stove.getOrRegister(KeyA, system2)\n\n      stove.allSystems() shouldHaveSize 2\n    }\n\n    test(\"keyed systems participate in run lifecycle\") {\n      val stove = Stove()\n      val defaultSystem = KeyedLifecycleSystem(stove)\n      val keyedSystem = KeyedLifecycleSystem(stove)\n      val app = KeyedTestApp()\n\n      stove.getOrRegister(defaultSystem)\n      stove.getOrRegister(KeyA, keyedSystem)\n      stove.applicationUnderTest(app)\n\n      runBlocking { stove.run() }\n\n      defaultSystem.beforeRunCalled shouldBe true\n      defaultSystem.runCalled shouldBe true\n      defaultSystem.afterRunCalled shouldBe true\n\n      keyedSystem.beforeRunCalled shouldBe true\n      keyedSystem.runCalled shouldBe true\n      keyedSystem.afterRunCalled shouldBe true\n\n      app.started shouldBe true\n      app.receivedConfigs shouldHaveSize 2\n    }\n\n    test(\"keyed systems contribute configurations\") {\n      val stove = Stove()\n      val defaultSystem = KeyedLifecycleSystem(stove, configValue = \"default.config=true\")\n      val keyedSystem = KeyedLifecycleSystem(stove, configValue = \"keyed.config=true\")\n      val app = KeyedTestApp()\n\n      stove.getOrRegister(defaultSystem)\n      stove.getOrRegister(KeyA, keyedSystem)\n      stove.applicationUnderTest(app)\n\n      runBlocking { stove.run() }\n\n      app.receivedConfigs.shouldContainAll(\"default.config=true\", \"keyed.config=true\")\n    }\n  })\n\nprivate class KeyedTestSystem(\n  override val stove: Stove\n) : PluggedSystem {\n  override fun then(): Stove = stove\n\n  override fun close() = Unit\n}\n\nprivate class KeyedTestApp : ApplicationUnderTest<String> {\n  var started: Boolean = false\n  var receivedConfigs: List<String> = emptyList()\n\n  override suspend fun start(configurations: List<String>): String {\n    started = true\n    receivedConfigs = configurations\n    return \"context\"\n  }\n\n  override suspend fun stop() = Unit\n}\n\nprivate class KeyedLifecycleSystem(\n  override val stove: Stove,\n  private val configValue: String = \"system.config=true\"\n) : PluggedSystem,\n  BeforeRunAware,\n  RunAware,\n  AfterRunAware,\n  ExposesConfiguration {\n  var beforeRunCalled: Boolean = false\n  var runCalled: Boolean = false\n  var afterRunCalled: Boolean = false\n\n  override suspend fun beforeRun() {\n    beforeRunCalled = true\n  }\n\n  override suspend fun run() {\n    runCalled = true\n  }\n\n  override suspend fun stop() = Unit\n\n  override suspend fun afterRun() {\n    afterRunCalled = true\n  }\n\n  override fun configuration(): List<String> = listOf(configValue)\n\n  override fun then(): Stove = stove\n\n  override fun close() = Unit\n}\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/PortFinderTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.ints.shouldBeGreaterThan\nimport io.kotest.matchers.shouldBe\nimport java.net.ServerSocket\n\nclass PortFinderTest :\n  FunSpec({\n    test(\"findAvailablePort should return a usable port\") {\n      val port = PortFinder.findAvailablePort()\n\n      port shouldBeGreaterThan 0\n      PortFinder.isPortAvailable(port) shouldBe true\n    }\n\n    test(\"findAvailablePortFrom should skip occupied ports\") {\n      ServerSocket(0).use { socket ->\n        val occupied = socket.localPort\n        val found = PortFinder.findAvailablePortFrom(occupied)\n\n        found shouldBeGreaterThan 0\n        found shouldBeGreaterThan occupied\n      }\n    }\n\n    test(\"findAvailablePortAsString should return numeric string\") {\n      val portStr = PortFinder.findAvailablePortAsString()\n\n      portStr.toInt() shouldBeGreaterThan 0\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/ProvidedApplicationUnderTestTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.runBlocking\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass ProvidedApplicationUnderTestTest :\n  FunSpec({\n    test(\"start with no health check is a no-op\") {\n      val aut = ProvidedApplicationUnderTest(ProvidedApplicationOptions())\n\n      runBlocking { aut.start(listOf(\"some.config=true\")) }\n      // No exception — success\n    }\n\n    test(\"stop is a no-op\") {\n      val aut = ProvidedApplicationUnderTest(ProvidedApplicationOptions())\n\n      runBlocking { aut.stop() }\n      // No exception — success\n    }\n\n    test(\"configurations are ignored\") {\n      val aut = ProvidedApplicationUnderTest(ProvidedApplicationOptions())\n\n      runBlocking {\n        aut.start(\n          listOf(\n            \"database.host=localhost\",\n            \"kafka.bootstrap=localhost:9092\"\n          )\n        )\n      }\n      // No exception — configs silently ignored\n    }\n\n    test(\"readiness check fails with unreachable URL\") {\n      val aut = ProvidedApplicationUnderTest(\n        ProvidedApplicationOptions(\n          readiness = ReadinessStrategy.HttpGet(\n            url = \"http://localhost:1/nonexistent-health\",\n            retries = 2,\n            retryDelay = 50.milliseconds,\n            timeout = 500.milliseconds\n          )\n        )\n      )\n\n      val error = shouldThrow<IllegalStateException> {\n        runBlocking { aut.start(emptyList()) }\n      }\n\n      error.message shouldContain \"Health check failed after 2 attempts\"\n      error.message shouldContain \"nonexistent-health\"\n    }\n\n    test(\"provided application integrates with Stove lifecycle\") {\n      val stove = Stove()\n      stove.applicationUnderTest(\n        ProvidedApplicationUnderTest(ProvidedApplicationOptions())\n      )\n\n      runBlocking { stove.run() }\n\n      Stove.instanceInitialized() shouldBe true\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/ReadinessCheckerTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport java.net.ServerSocket\nimport kotlin.time.Duration.Companion.milliseconds\nimport kotlin.time.Duration.Companion.seconds\n\nclass ReadinessCheckerTest :\n  FunSpec({\n    context(\"HttpGet strategy\") {\n      test(\"passes when endpoint returns expected status\") {\n        val port = ServerSocket(0).use { it.localPort }\n        val server = com.sun.net.httpserver.HttpServer.create(java.net.InetSocketAddress(port), 0)\n        server.createContext(\"/health\") { exchange ->\n          exchange.sendResponseHeaders(200, 0)\n          exchange.responseBody.close()\n        }\n        server.start()\n\n        try {\n          ReadinessChecker.check(\n            ReadinessStrategy.HttpGet(\n              url = \"http://localhost:$port/health\",\n              retries = 3,\n              retryDelay = 100.milliseconds,\n              timeout = 2.seconds\n            )\n          )\n        } finally {\n          server.stop(0)\n        }\n      }\n\n      test(\"fails after retries when endpoint is unreachable\") {\n        val error = shouldThrow<IllegalStateException> {\n          ReadinessChecker.check(\n            ReadinessStrategy.HttpGet(\n              url = \"http://localhost:1/nonexistent\",\n              retries = 2,\n              retryDelay = 50.milliseconds,\n              timeout = 500.milliseconds\n            )\n          )\n        }\n        error.message shouldContain \"Health check failed after 2 attempts\"\n      }\n\n      test(\"fails when endpoint returns unexpected status code\") {\n        val port = ServerSocket(0).use { it.localPort }\n        val server = com.sun.net.httpserver.HttpServer.create(java.net.InetSocketAddress(port), 0)\n        server.createContext(\"/health\") { exchange ->\n          exchange.sendResponseHeaders(503, 0)\n          exchange.responseBody.close()\n        }\n        server.start()\n\n        try {\n          val error = shouldThrow<IllegalStateException> {\n            ReadinessChecker.check(\n              ReadinessStrategy.HttpGet(\n                url = \"http://localhost:$port/health\",\n                retries = 2,\n                retryDelay = 50.milliseconds,\n                timeout = 2.seconds\n              )\n            )\n          }\n          error.message shouldContain \"Health check failed after 2 attempts\"\n        } finally {\n          server.stop(0)\n        }\n      }\n\n      test(\"accepts custom expected status codes\") {\n        val port = ServerSocket(0).use { it.localPort }\n        val server = com.sun.net.httpserver.HttpServer.create(java.net.InetSocketAddress(port), 0)\n        server.createContext(\"/health\") { exchange ->\n          exchange.sendResponseHeaders(204, -1)\n          exchange.responseBody.close()\n        }\n        server.start()\n\n        try {\n          ReadinessChecker.check(\n            ReadinessStrategy.HttpGet(\n              url = \"http://localhost:$port/health\",\n              retries = 2,\n              retryDelay = 50.milliseconds,\n              timeout = 2.seconds,\n              expectedStatusCodes = setOf(204)\n            )\n          )\n        } finally {\n          server.stop(0)\n        }\n      }\n    }\n\n    context(\"TcpPort strategy\") {\n      test(\"passes when port is open\") {\n        val server = ServerSocket(0)\n        val port = server.localPort\n\n        try {\n          ReadinessChecker.check(\n            ReadinessStrategy.TcpPort(\n              port = port,\n              retries = 3,\n              retryDelay = 50.milliseconds\n            )\n          )\n        } finally {\n          server.close()\n        }\n      }\n\n      test(\"fails after retries when port is closed\") {\n        // Use port 1 which is almost certainly not open\n        val error = shouldThrow<IllegalStateException> {\n          ReadinessChecker.check(\n            ReadinessStrategy.TcpPort(\n              port = 1,\n              retries = 2,\n              retryDelay = 50.milliseconds\n            )\n          )\n        }\n        error.message shouldContain \"TCP port 1 did not open after 2 attempts\"\n      }\n    }\n\n    context(\"Probe strategy\") {\n      test(\"passes when probe returns true\") {\n        ReadinessChecker.check(\n          ReadinessStrategy.Probe(retries = 3, retryDelay = 50.milliseconds) { true }\n        )\n      }\n\n      test(\"fails after retries when probe returns false\") {\n        val error = shouldThrow<IllegalStateException> {\n          ReadinessChecker.check(\n            ReadinessStrategy.Probe(retries = 2, retryDelay = 50.milliseconds) { false }\n          )\n        }\n        error.message shouldContain \"Readiness probe did not pass after 2 attempts\"\n      }\n\n      test(\"fails after retries when probe throws\") {\n        val error = shouldThrow<IllegalStateException> {\n          ReadinessChecker.check(\n            ReadinessStrategy.Probe(retries = 2, retryDelay = 50.milliseconds) {\n              error(\"Connection refused\")\n            }\n          )\n        }\n        error.message shouldContain \"Readiness probe did not pass after 2 attempts\"\n      }\n\n      test(\"passes after initial failures\") {\n        var attempt = 0\n        ReadinessChecker.check(\n          ReadinessStrategy.Probe(retries = 5, retryDelay = 50.milliseconds) {\n            attempt++\n            attempt >= 3\n          }\n        )\n        attempt shouldBe 3\n      }\n    }\n\n    context(\"FixedDelay strategy\") {\n      test(\"completes after specified delay\") {\n        val start = System.currentTimeMillis()\n        ReadinessChecker.check(ReadinessStrategy.FixedDelay(200.milliseconds))\n        val elapsed = System.currentTimeMillis() - start\n        (elapsed >= 180) shouldBe true\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/StoveOptionsDslTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport com.trendyol.stove.system.abstractions.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlin.reflect.KClass\n\nclass StoveOptionsDslTest :\n  FunSpec({\n    test(\"should keep dependencies running\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n      stoveOptionsDsl.keepDependenciesRunning()\n\n      stoveOptionsDsl.options.keepDependenciesRunning shouldBe true\n    }\n\n    test(\"should check if running locally\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n      stoveOptionsDsl.isRunningLocally() shouldBe (\n        System.getenv(\"CI\") != \"true\" &&\n          System.getenv(\"GITLAB_CI\") != \"true\" &&\n          System.getenv(\"GITHUB_ACTIONS\") != \"true\"\n        )\n    }\n\n    test(\"should enable reuse for test containers\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n      stoveOptionsDsl.enableReuseForTestContainers()\n    }\n\n    test(\"should set state storage factory\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n\n      class AnotherStateStorageFactory : StateStorageFactory {\n        override fun <T : Any> invoke(\n          options: StoveOptions,\n          system: KClass<*>,\n          state: KClass<T>\n        ): StateStorage<T> = object : StateStorage<T> {\n          override suspend fun capture(start: suspend () -> T): T = start()\n\n          override fun isSubsequentRun(): Boolean = false\n        }\n      }\n\n      data class Example1ExposedState(\n        val id: Int = 1\n      ) : ExposedConfiguration\n\n      class Example1System(\n        override val stove: Stove\n      ) : PluggedSystem {\n        override fun close() = Unit\n      }\n\n      val anotherStateStorageFactory = AnotherStateStorageFactory()\n      stoveOptionsDsl.stateStorage(anotherStateStorageFactory)\n\n      stoveOptionsDsl.options.stateStorageFactory shouldBe anotherStateStorageFactory\n      val storage = stoveOptionsDsl.options.createStateStorage<Example1ExposedState, Example1System>()\n      storage.isSubsequentRun() shouldBe false\n      storage.capture { Example1ExposedState() } shouldBe Example1ExposedState()\n    }\n\n    test(\"should run migrations always\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n      stoveOptionsDsl.runMigrationsAlways()\n\n      stoveOptionsDsl.options.runMigrationsAlways shouldBe true\n    }\n\n    test(\"should enable reporting\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n      stoveOptionsDsl.reportingEnabled(true)\n\n      stoveOptionsDsl.options.reportingEnabled shouldBe true\n    }\n\n    test(\"should disable reporting\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n      stoveOptionsDsl.reportingEnabled(false)\n\n      stoveOptionsDsl.options.reportingEnabled shouldBe false\n    }\n\n    test(\"should enable dump report on test failure\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n      stoveOptionsDsl.dumpReportOnTestFailure(true)\n\n      stoveOptionsDsl.options.dumpReportOnTestFailure shouldBe true\n    }\n\n    test(\"should disable dump report on test failure\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n      stoveOptionsDsl.dumpReportOnTestFailure(false)\n\n      stoveOptionsDsl.options.dumpReportOnTestFailure shouldBe false\n    }\n\n    test(\"should configure reporting via DSL block\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n\n      stoveOptionsDsl.reporting {\n        enabled()\n        dumpOnFailure()\n      }\n\n      stoveOptionsDsl.options.reportingEnabled shouldBe true\n      stoveOptionsDsl.options.dumpReportOnTestFailure shouldBe true\n    }\n\n    test(\"should disable reporting via DSL block\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n      stoveOptionsDsl.reportingEnabled(true)\n\n      stoveOptionsDsl.reporting {\n        disabled()\n      }\n\n      stoveOptionsDsl.options.reportingEnabled shouldBe false\n    }\n\n    test(\"should chain multiple options fluently\") {\n      val stoveOptionsDsl = StoveOptionsDsl()\n\n      stoveOptionsDsl\n        .reportingEnabled(true)\n        .dumpReportOnTestFailure(true)\n        .runMigrationsAlways()\n\n      stoveOptionsDsl.options.reportingEnabled shouldBe true\n      stoveOptionsDsl.options.dumpReportOnTestFailure shouldBe true\n      stoveOptionsDsl.options.runMigrationsAlways shouldBe true\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/StoveTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport arrow.core.None\nimport com.trendyol.stove.system.abstractions.*\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.runBlocking\n\nclass StoveTest :\n  FunSpec({\n    test(\"getOrRegister returns existing instance\") {\n      val stove = Stove()\n      val system = TestLifecycleSystem(stove)\n\n      val first = stove.getOrRegister(system)\n      val second = stove.getOrRegister(system)\n\n      first shouldBe second\n    }\n\n    test(\"getOrNone returns None when system missing\") {\n      val stove = Stove()\n\n      stove.getOrNone<TestLifecycleSystem>() shouldBe None\n    }\n\n    test(\"run invokes lifecycle and passes configurations\") {\n      val stove = Stove()\n      val system = TestLifecycleSystem(stove)\n      val app = TestApplicationUnderTest()\n\n      stove.getOrRegister(system)\n      stove.applicationUnderTest(app)\n\n      runBlocking { stove.run() }\n\n      system.beforeRunCalled shouldBe true\n      system.runCalled shouldBe true\n      system.afterRunCalled shouldBe true\n      app.started shouldBe true\n      app.receivedConfigs shouldBe listOf(\"system.config=true\")\n      stove.applicationUnderTestContext<String>() shouldBe \"context\"\n      Stove.instanceInitialized() shouldBe true\n    }\n\n    test(\"stove validation DSL throws when not initialized\") {\n      if (!Stove.instanceInitialized()) {\n        shouldThrow<IllegalStateException> {\n          runBlocking { stove { } }\n        }\n      } else {\n        runBlocking { stove { } }\n      }\n    }\n  })\n\nprivate class TestApplicationUnderTest : ApplicationUnderTest<String> {\n  var started: Boolean = false\n  var receivedConfigs: List<String> = emptyList()\n\n  override suspend fun start(configurations: List<String>): String {\n    started = true\n    receivedConfigs = configurations\n    return \"context\"\n  }\n\n  override suspend fun stop() = Unit\n}\n\nprivate class TestLifecycleSystem(\n  override val stove: Stove\n) : PluggedSystem,\n  BeforeRunAware,\n  RunAware,\n  AfterRunAware,\n  ExposesConfiguration {\n  var beforeRunCalled: Boolean = false\n  var runCalled: Boolean = false\n  var afterRunCalled: Boolean = false\n\n  override suspend fun beforeRun() {\n    beforeRunCalled = true\n  }\n\n  override suspend fun run() {\n    runCalled = true\n  }\n\n  override suspend fun stop() = Unit\n\n  override suspend fun afterRun() {\n    afterRunCalled = true\n  }\n\n  override fun configuration(): List<String> = listOf(\"system.config=true\")\n\n  override fun then(): Stove = stove\n\n  override fun close() = Unit\n}\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/ValidationDslTest.kt",
    "content": "package com.trendyol.stove.system\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass ValidationDslTest :\n  FunSpec({\n    test(\"should expose stove instance\") {\n      val stove = Stove()\n      val dsl = ValidationDsl(stove)\n\n      dsl.stove shouldBe stove\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/abstractions/ProvidedSystemOptionsTest.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\n/**\n * Unit tests for the ProvidedSystemOptions interface.\n */\nclass ProvidedSystemOptionsTest :\n  FunSpec({\n\n    /**\n     * Test exposed configuration.\n     */\n    data class TestExposedConfiguration(\n      val host: String,\n      val port: Int\n    ) : ExposedConfiguration\n\n    /**\n     * Base system options (container mode).\n     */\n    open class TestSystemOptions(\n      val name: String,\n      override val configureExposedConfiguration: (TestExposedConfiguration) -> List<String>\n    ) : SystemOptions,\n      ConfiguresExposedConfiguration<TestExposedConfiguration>\n\n    /**\n     * Provided system options (external instance mode).\n     */\n    class ProvidedTestSystemOptions(\n      override val providedConfig: TestExposedConfiguration,\n      override val runMigrationsForProvided: Boolean = true,\n      name: String = \"provided\",\n      configureExposedConfiguration: (TestExposedConfiguration) -> List<String>\n    ) : TestSystemOptions(name, configureExposedConfiguration),\n      ProvidedSystemOptions<TestExposedConfiguration>\n\n    test(\"ProvidedSystemOptions instance check should work with base type reference\") {\n      val providedOptions: TestSystemOptions = ProvidedTestSystemOptions(\n        providedConfig = TestExposedConfiguration(\"localhost\", 8080),\n        runMigrationsForProvided = true,\n        configureExposedConfiguration = { listOf() }\n      )\n\n      // When referenced through base type, instance check is meaningful\n      (providedOptions is ProvidedSystemOptions<*>) shouldBe true\n    }\n\n    test(\"providedConfig should hold the configuration\") {\n      val config = TestExposedConfiguration(\"external-host\", 9090)\n      val providedOptions = ProvidedTestSystemOptions(\n        providedConfig = config,\n        configureExposedConfiguration = { listOf() }\n      )\n\n      providedOptions.providedConfig shouldBe config\n      providedOptions.providedConfig.host shouldBe \"external-host\"\n      providedOptions.providedConfig.port shouldBe 9090\n    }\n\n    test(\"runMigrationsForProvided should default to true\") {\n      val providedOptions = ProvidedTestSystemOptions(\n        providedConfig = TestExposedConfiguration(\"localhost\", 8080),\n        configureExposedConfiguration = { listOf() }\n      )\n\n      providedOptions.runMigrationsForProvided shouldBe true\n    }\n\n    test(\"runMigrationsForProvided can be set to false\") {\n      val providedOptions = ProvidedTestSystemOptions(\n        providedConfig = TestExposedConfiguration(\"localhost\", 8080),\n        runMigrationsForProvided = false,\n        configureExposedConfiguration = { listOf() }\n      )\n\n      providedOptions.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"base options should not be ProvidedSystemOptions\") {\n      val baseOptions = TestSystemOptions(\n        name = \"base\",\n        configureExposedConfiguration = { listOf() }\n      )\n\n      // Base options is not a ProvidedSystemOptions\n      (baseOptions is ProvidedSystemOptions<*>) shouldBe false\n    }\n\n    test(\"provided options should inherit from base options\") {\n      val providedOptions = ProvidedTestSystemOptions(\n        providedConfig = TestExposedConfiguration(\"localhost\", 8080),\n        name = \"inherited-name\",\n        configureExposedConfiguration = { cfg -> listOf(\"host=${cfg.host}\", \"port=${cfg.port}\") }\n      )\n\n      // Should have base class properties\n      providedOptions.name shouldBe \"inherited-name\"\n\n      // Should produce correct configuration\n      val config = providedOptions.configureExposedConfiguration(providedOptions.providedConfig)\n      config shouldBe listOf(\"host=localhost\", \"port=8080\")\n    }\n\n    test(\"type checking can distinguish between base and provided options\") {\n      val baseOptions: TestSystemOptions = TestSystemOptions(\n        name = \"base\",\n        configureExposedConfiguration = { listOf() }\n      )\n\n      val providedOptions: TestSystemOptions = ProvidedTestSystemOptions(\n        providedConfig = TestExposedConfiguration(\"localhost\", 8080),\n        configureExposedConfiguration = { listOf() }\n      )\n\n      // Using when expression to distinguish\n      fun getMode(options: TestSystemOptions): String = when (options) {\n        is ProvidedTestSystemOptions -> \"provided\"\n        else -> \"container\"\n      }\n\n      getMode(baseOptions) shouldBe \"container\"\n      getMode(providedOptions) shouldBe \"provided\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/abstractions/StateStorageKeyTest.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\nimport com.trendyol.stove.system.StoveOptions\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.shouldContain\nimport io.kotest.matchers.string.shouldNotContain\nimport java.nio.file.Paths\n\nclass StateStorageKeyTest :\n  FunSpec({\n    test(\"FileSystemStorage without key uses original path format\") {\n      val options = StoveOptions()\n      val storage = FileSystemStorage<TestConfig>(options, TestSystem::class, TestConfig::class)\n      val expectedPath = Paths.get(\n        System.getProperty(\"java.io.tmpdir\"),\n        \"com.trendyol.stove\",\n        \"stove-e2e-testsystem.lock\"\n      )\n\n      // Access via reflection to verify path — the class is internal\n      val pathField = storage.javaClass.getDeclaredField(\"pathForSystem\")\n      pathField.isAccessible = true\n      val path = pathField.get(storage) as java.nio.file.Path\n\n      path shouldBe expectedPath\n    }\n\n    test(\"FileSystemStorage with key includes key in path\") {\n      val options = StoveOptions()\n      val storage = FileSystemStorage<TestConfig>(options, TestSystem::class, TestConfig::class, keyName = \"AppDb\")\n      val expectedPath = Paths.get(\n        System.getProperty(\"java.io.tmpdir\"),\n        \"com.trendyol.stove\",\n        \"stove-e2e-testsystem-appdb.lock\"\n      )\n\n      val pathField = storage.javaClass.getDeclaredField(\"pathForSystem\")\n      pathField.isAccessible = true\n      val path = pathField.get(storage) as java.nio.file.Path\n\n      path shouldBe expectedPath\n    }\n\n    test(\"different keys produce different lock file paths\") {\n      val options = StoveOptions()\n      val storageA = FileSystemStorage<TestConfig>(options, TestSystem::class, TestConfig::class, keyName = \"AppDb\")\n      val storageB =\n        FileSystemStorage<TestConfig>(options, TestSystem::class, TestConfig::class, keyName = \"AnalyticsDb\")\n\n      val pathField = FileSystemStorage::class.java.getDeclaredField(\"pathForSystem\")\n      pathField.isAccessible = true\n\n      val pathA = pathField.get(storageA) as java.nio.file.Path\n      val pathB = pathField.get(storageB) as java.nio.file.Path\n\n      pathA shouldNotBe pathB\n      pathA.toString() shouldContain \"appdb\"\n      pathB.toString() shouldContain \"analyticsdb\"\n    }\n\n    test(\"StateStorageFactory createWithKey default delegates to invoke\") {\n      var invokedWithSystem: Class<*>? = null\n      val factory = object : StateStorageFactory {\n        override fun <T : Any> invoke(\n          options: StoveOptions,\n          system: kotlin.reflect.KClass<*>,\n          state: kotlin.reflect.KClass<T>\n        ): StateStorage<T> {\n          invokedWithSystem = system.java\n          return FileSystemStorage(options, system, state)\n        }\n      }\n\n      factory.createWithKey(StoveOptions(), TestSystem::class, TestConfig::class, \"SomeKey\")\n\n      invokedWithSystem shouldBe TestSystem::class.java\n    }\n\n    test(\"DefaultStateStorageFactory createWithKey passes keyName to FileSystemStorage\") {\n      val factory = StateStorageFactory.Default()\n      val storage = factory.createWithKey(StoveOptions(), TestSystem::class, TestConfig::class, \"MyKey\")\n\n      val pathField = storage.javaClass.getDeclaredField(\"pathForSystem\")\n      pathField.isAccessible = true\n      val path = pathField.get(storage) as java.nio.file.Path\n\n      path.toString() shouldContain \"mykey\"\n    }\n\n    test(\"DefaultStateStorageFactory createWithKey with null key behaves like no key\") {\n      val factory = StateStorageFactory.Default()\n      val storage = factory.createWithKey(StoveOptions(), TestSystem::class, TestConfig::class, null)\n\n      val pathField = storage.javaClass.getDeclaredField(\"pathForSystem\")\n      pathField.isAccessible = true\n      val path = pathField.get(storage) as java.nio.file.Path\n\n      path.fileName.toString() shouldBe \"stove-e2e-testsystem.lock\"\n      path.fileName.toString() shouldNotContain \"-null\"\n    }\n  })\n\nprivate class TestSystem\n\nprivate data class TestConfig(val value: String = \"test\") : ExposedConfiguration\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/system/abstractions/SystemKeyTest.kt",
    "content": "package com.trendyol.stove.system.abstractions\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.shouldMatch\nimport io.kotest.matchers.string.shouldNotContain\n\nprivate object PaymentService : SystemKey\n\nprivate object OrderService : SystemKey\n\nclass SystemKeyTest :\n  FunSpec({\n    test(\"keyDisplayName returns simpleName for named objects\") {\n      keyDisplayName(PaymentService) shouldBe \"PaymentService\"\n      keyDisplayName(OrderService) shouldBe \"OrderService\"\n    }\n\n    test(\"keyDisplayName sanitizes invalid filename characters\") {\n      val anonymousKey = object : SystemKey {}\n      val name = keyDisplayName(anonymousKey)\n      name shouldNotContain \"<\"\n      name shouldNotContain \">\"\n      name shouldNotContain \"/\"\n      name shouldNotContain \"\\\\\"\n      name shouldMatch Regex(\"[a-zA-Z0-9._-]+\")\n    }\n\n    test(\"different SystemKey objects have different classes\") {\n      PaymentService::class shouldNotBe OrderService::class\n    }\n\n    test(\"same SystemKey object always returns same class\") {\n      PaymentService::class shouldBe PaymentService::class\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/tracing/SpanInfoTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass SpanInfoTest :\n  FunSpec({\n\n    test(\"durationMs should calculate milliseconds from nanoseconds\") {\n      val span = createSpan(\n        startTimeNanos = 0L,\n        endTimeNanos = 5_000_000L // 5ms in nanoseconds\n      )\n\n      span.durationMs shouldBe 5L\n    }\n\n    test(\"durationMs should handle zero duration\") {\n      val span = createSpan(\n        startTimeNanos = 1000L,\n        endTimeNanos = 1000L\n      )\n\n      span.durationMs shouldBe 0L\n    }\n\n    test(\"durationNanos should return raw nanosecond difference\") {\n      val span = createSpan(\n        startTimeNanos = 100L,\n        endTimeNanos = 500L\n      )\n\n      span.durationNanos shouldBe 400L\n    }\n\n    test(\"isFailed should return true when status is ERROR\") {\n      val span = createSpan(status = SpanStatus.ERROR)\n\n      span.isFailed shouldBe true\n      span.isSuccess shouldBe false\n    }\n\n    test(\"isSuccess should return true when status is OK\") {\n      val span = createSpan(status = SpanStatus.OK)\n\n      span.isSuccess shouldBe true\n      span.isFailed shouldBe false\n    }\n\n    test(\"UNSET status should be neither failed nor success\") {\n      val span = createSpan(status = SpanStatus.UNSET)\n\n      span.isFailed shouldBe false\n      span.isSuccess shouldBe false\n    }\n\n    test(\"span with exception should preserve exception info\") {\n      val exception = ExceptionInfo(\n        type = \"java.lang.RuntimeException\",\n        message = \"Something went wrong\",\n        stackTrace = listOf(\"at com.example.Test.method(Test.kt:10)\")\n      )\n      val span = createSpan(exception = exception)\n\n      span.exception shouldBe exception\n      span.exception?.type shouldBe \"java.lang.RuntimeException\"\n      span.exception?.message shouldBe \"Something went wrong\"\n      span.exception?.stackTrace?.size shouldBe 1\n    }\n\n    test(\"span without exception should have null exception\") {\n      val span = createSpan()\n\n      span.exception shouldBe null\n    }\n\n    test(\"span should preserve attributes\") {\n      val attrs = mapOf(\n        \"http.method\" to \"GET\",\n        \"http.url\" to \"/api/test\"\n      )\n      val span = createSpan(attributes = attrs)\n\n      span.attributes shouldBe attrs\n      span.attributes[\"http.method\"] shouldBe \"GET\"\n    }\n\n    test(\"span with empty attributes should have empty map\") {\n      val span = createSpan(attributes = emptyMap())\n\n      span.attributes shouldBe emptyMap()\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span456\",\n  parentSpanId: String? = null,\n  operationName: String = \"test-operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 0L,\n  endTimeNanos: Long = 1_000_000L,\n  status: SpanStatus = SpanStatus.OK,\n  attributes: Map<String, String> = emptyMap(),\n  exception: ExceptionInfo? = null\n): SpanInfo = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = serviceName,\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status,\n  attributes = attributes,\n  exception = exception\n)\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/tracing/SpanTreeTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldContainExactly\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.nulls.shouldBeNull\nimport io.kotest.matchers.nulls.shouldNotBeNull\nimport io.kotest.matchers.shouldBe\n\nclass SpanTreeTest :\n  FunSpec({\n\n    context(\"SpanNode\") {\n      test(\"hasFailedDescendants should return true when span itself is failed\") {\n        val node = SpanNode(createSpan(status = SpanStatus.ERROR))\n\n        node.hasFailedDescendants shouldBe true\n      }\n\n      test(\"hasFailedDescendants should return true when child is failed\") {\n        val child = SpanNode(createSpan(spanId = \"child\", status = SpanStatus.ERROR))\n        val parent = SpanNode(createSpan(spanId = \"parent\", status = SpanStatus.OK), listOf(child))\n\n        parent.hasFailedDescendants shouldBe true\n      }\n\n      test(\"hasFailedDescendants should return false when all spans are OK\") {\n        val child = SpanNode(createSpan(spanId = \"child\", status = SpanStatus.OK))\n        val parent = SpanNode(createSpan(spanId = \"parent\", status = SpanStatus.OK), listOf(child))\n\n        parent.hasFailedDescendants shouldBe false\n      }\n\n      test(\"depth should be 1 for leaf node\") {\n        val node = SpanNode(createSpan())\n\n        node.depth shouldBe 1\n      }\n\n      test(\"depth should count nested levels\") {\n        val grandchild = SpanNode(createSpan(spanId = \"grandchild\"))\n        val child = SpanNode(createSpan(spanId = \"child\"), listOf(grandchild))\n        val parent = SpanNode(createSpan(spanId = \"parent\"), listOf(child))\n\n        parent.depth shouldBe 3\n      }\n\n      test(\"spanCount should count all spans in tree\") {\n        val grandchild = SpanNode(createSpan(spanId = \"grandchild\"))\n        val child1 = SpanNode(createSpan(spanId = \"child1\"), listOf(grandchild))\n        val child2 = SpanNode(createSpan(spanId = \"child2\"))\n        val parent = SpanNode(createSpan(spanId = \"parent\"), listOf(child1, child2))\n\n        parent.spanCount shouldBe 4\n      }\n\n      test(\"findFailurePoint should return failed leaf node\") {\n        val failedChild = SpanNode(createSpan(spanId = \"failed\", status = SpanStatus.ERROR))\n        val parent = SpanNode(createSpan(spanId = \"parent\", status = SpanStatus.OK), listOf(failedChild))\n\n        val failurePoint = parent.findFailurePoint()\n\n        failurePoint.shouldNotBeNull()\n        failurePoint.span.spanId shouldBe \"failed\"\n      }\n\n      test(\"findFailurePoint should return deepest failure in chain\") {\n        val deepFailed = SpanNode(createSpan(spanId = \"deep\", status = SpanStatus.ERROR))\n        val middleFailed = SpanNode(createSpan(spanId = \"middle\", status = SpanStatus.ERROR), listOf(deepFailed))\n        val parent = SpanNode(createSpan(spanId = \"parent\", status = SpanStatus.OK), listOf(middleFailed))\n\n        val failurePoint = parent.findFailurePoint()\n\n        failurePoint.shouldNotBeNull()\n        failurePoint.span.spanId shouldBe \"deep\"\n      }\n\n      test(\"findFailurePoint should return null when no failures\") {\n        val node = SpanNode(createSpan(status = SpanStatus.OK))\n\n        node.findFailurePoint().shouldBeNull()\n      }\n\n      test(\"flatten should return all spans in tree\") {\n        val grandchild = SpanNode(createSpan(spanId = \"grandchild\"))\n        val child = SpanNode(createSpan(spanId = \"child\"), listOf(grandchild))\n        val parent = SpanNode(createSpan(spanId = \"parent\"), listOf(child))\n\n        val flattened = parent.flatten()\n\n        flattened shouldHaveSize 3\n        flattened.map { it.spanId } shouldContainExactly listOf(\"parent\", \"child\", \"grandchild\")\n      }\n    }\n\n    context(\"SpanTree.build\") {\n      test(\"should return null for empty list\") {\n        val result = SpanTree.build(emptyList())\n\n        result.shouldBeNull()\n      }\n\n      test(\"should build single node tree\") {\n        val span = createSpan(spanId = \"root\", parentSpanId = null)\n\n        val result = SpanTree.build(listOf(span))\n\n        result.shouldNotBeNull()\n        result.span.spanId shouldBe \"root\"\n        result.children shouldHaveSize 0\n      }\n\n      test(\"should build parent-child relationship\") {\n        val parent = createSpan(spanId = \"parent\", parentSpanId = null, startTimeNanos = 0)\n        val child = createSpan(spanId = \"child\", parentSpanId = \"parent\", startTimeNanos = 100)\n\n        val result = SpanTree.build(listOf(parent, child))\n\n        result.shouldNotBeNull()\n        result.span.spanId shouldBe \"parent\"\n        result.children shouldHaveSize 1\n        result.children[0].span.spanId shouldBe \"child\"\n      }\n\n      test(\"should order children by start time\") {\n        val parent = createSpan(spanId = \"parent\", parentSpanId = null, startTimeNanos = 0)\n        val child1 = createSpan(spanId = \"child1\", parentSpanId = \"parent\", startTimeNanos = 200)\n        val child2 = createSpan(spanId = \"child2\", parentSpanId = \"parent\", startTimeNanos = 100)\n\n        val result = SpanTree.build(listOf(parent, child1, child2))\n\n        result.shouldNotBeNull()\n        result.children shouldHaveSize 2\n        result.children[0].span.spanId shouldBe \"child2\"\n        result.children[1].span.spanId shouldBe \"child1\"\n      }\n\n      test(\"should handle orphaned spans as roots\") {\n        val orphan = createSpan(spanId = \"orphan\", parentSpanId = \"nonexistent\")\n\n        val result = SpanTree.build(listOf(orphan))\n\n        result.shouldNotBeNull()\n        result.span.spanId shouldBe \"orphan\"\n      }\n\n      test(\"should create virtual root for multiple roots\") {\n        val root1 = createSpan(spanId = \"root1\", parentSpanId = null, startTimeNanos = 0)\n        val root2 = createSpan(spanId = \"root2\", parentSpanId = null, startTimeNanos = 100)\n\n        val result = SpanTree.build(listOf(root1, root2))\n\n        result.shouldNotBeNull()\n        result.span.operationName shouldBe \"trace-root\"\n        result.children shouldHaveSize 2\n      }\n\n      test(\"should build deep tree structure\") {\n        val root = createSpan(spanId = \"root\", parentSpanId = null, startTimeNanos = 0)\n        val child = createSpan(spanId = \"child\", parentSpanId = \"root\", startTimeNanos = 100)\n        val grandchild = createSpan(spanId = \"grandchild\", parentSpanId = \"child\", startTimeNanos = 200)\n\n        val result = SpanTree.build(listOf(root, child, grandchild))\n\n        result.shouldNotBeNull()\n        result.depth shouldBe 3\n        result.spanCount shouldBe 3\n      }\n    }\n\n    context(\"SpanTree.findSpan\") {\n      test(\"should find span matching predicate\") {\n        val root = createSpan(spanId = \"root\", operationName = \"root-op\")\n        val child = createSpan(spanId = \"child\", operationName = \"child-op\", parentSpanId = \"root\")\n        val tree = SpanTree.build(listOf(root, child))!!\n\n        val found = SpanTree.findSpan(tree) { it.operationName == \"child-op\" }\n\n        found.shouldNotBeNull()\n        found.span.spanId shouldBe \"child\"\n      }\n\n      test(\"should return null when no match\") {\n        val root = createSpan(spanId = \"root\")\n        val tree = SpanTree.build(listOf(root))!!\n\n        val found = SpanTree.findSpan(tree) { it.operationName == \"nonexistent\" }\n\n        found.shouldBeNull()\n      }\n    }\n\n    context(\"SpanTree.filterSpans\") {\n      test(\"should filter spans matching predicate\") {\n        val root = createSpan(spanId = \"root\", status = SpanStatus.OK)\n        val child1 = createSpan(spanId = \"child1\", status = SpanStatus.ERROR, parentSpanId = \"root\")\n        val child2 = createSpan(spanId = \"child2\", status = SpanStatus.ERROR, parentSpanId = \"root\")\n        val tree = SpanTree.build(listOf(root, child1, child2))!!\n\n        val filtered = SpanTree.filterSpans(tree) { it.status == SpanStatus.ERROR }\n\n        filtered shouldHaveSize 2\n        filtered.map { it.span.spanId } shouldContainExactly listOf(\"child1\", \"child2\")\n      }\n\n      test(\"should return empty list when no match\") {\n        val root = createSpan(spanId = \"root\", status = SpanStatus.OK)\n        val tree = SpanTree.build(listOf(root))!!\n\n        val filtered = SpanTree.filterSpans(tree) { it.status == SpanStatus.ERROR }\n\n        filtered shouldHaveSize 0\n      }\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span456\",\n  parentSpanId: String? = null,\n  operationName: String = \"test-operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 0L,\n  endTimeNanos: Long = 1_000_000L,\n  status: SpanStatus = SpanStatus.OK,\n  attributes: Map<String, String> = emptyMap(),\n  exception: ExceptionInfo? = null\n): SpanInfo = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = serviceName,\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status,\n  attributes = attributes,\n  exception = exception\n)\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/tracing/TraceContextTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.nulls.shouldBeNull\nimport io.kotest.matchers.nulls.shouldNotBeNull\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.shouldHaveLength\nimport io.kotest.matchers.string.shouldMatch\nimport io.opentelemetry.api.baggage.Baggage\nimport io.opentelemetry.context.Context\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nclass TraceContextTest :\n  FunSpec({\n\n    beforeTest {\n      TraceContext.clear()\n    }\n\n    afterTest {\n      TraceContext.clear()\n    }\n\n    test(\"start should create a new TraceContext with valid IDs\") {\n      val ctx = TraceContext.start(\"test-1\")\n\n      ctx.testId shouldBe \"test-1\"\n      ctx.traceId shouldHaveLength 32\n      ctx.rootSpanId shouldHaveLength 16\n      ctx.traceId shouldMatch Regex(\"[a-f0-9]{32}\")\n      ctx.rootSpanId shouldMatch Regex(\"[a-f0-9]{16}\")\n    }\n\n    test(\"current should return the active context\") {\n      TraceContext.current().shouldBeNull()\n\n      val ctx = TraceContext.start(\"test-1\")\n\n      TraceContext.current().shouldNotBeNull()\n      TraceContext.current() shouldBe ctx\n    }\n\n    test(\"clear should remove the current context\") {\n      TraceContext.start(\"test-1\")\n      TraceContext.current().shouldNotBeNull()\n\n      TraceContext.clear()\n\n      TraceContext.current().shouldBeNull()\n    }\n\n    test(\"withCurrentPropagation should keep trace context across dispatcher switches\") {\n      val ctx = TraceContext.start(\"test-1\")\n\n      TraceContext.withCurrentPropagation {\n        withContext(Dispatchers.Default) {\n          TraceContext.current() shouldBe ctx\n        }\n      }\n    }\n\n    test(\"withPropagation should restore the previous context after completion\") {\n      val outer = TraceContext.start(\"outer-test\")\n      val inner = TraceContext(\n        traceId = TraceContext.generateTraceId(),\n        testId = \"inner-test\",\n        rootSpanId = TraceContext.generateSpanId()\n      )\n\n      TraceContext.withPropagation(inner) {\n        TraceContext.current() shouldBe inner\n      }\n\n      TraceContext.current() shouldBe outer\n    }\n\n    test(\"toTraceparent should generate valid W3C traceparent header\") {\n      val ctx = TraceContext.start(\"test-1\")\n\n      val traceparent = ctx.toTraceparent()\n\n      traceparent shouldMatch Regex(\"00-[a-f0-9]{32}-[a-f0-9]{16}-01\")\n      traceparent shouldBe \"00-${ctx.traceId}-${ctx.rootSpanId}-01\"\n    }\n\n    test(\"parseTraceparent should extract traceId and spanId\") {\n      val traceparent = \"00-abcd1234abcd1234abcd1234abcd1234-1234567890abcdef-01\"\n\n      val result = TraceContext.parseTraceparent(traceparent)\n\n      result.shouldNotBeNull()\n      result.first shouldBe \"abcd1234abcd1234abcd1234abcd1234\"\n      result.second shouldBe \"1234567890abcdef\"\n    }\n\n    test(\"parseTraceparent should return null for invalid format\") {\n      val invalidTraceparent = \"invalid\"\n\n      val result = TraceContext.parseTraceparent(invalidTraceparent)\n\n      result.shouldBeNull()\n    }\n\n    test(\"generateTraceId should produce unique IDs\") {\n      val ids = (1..100).map { TraceContext.generateTraceId() }.toSet()\n\n      ids.size shouldBe 100\n    }\n\n    test(\"generateSpanId should produce unique IDs\") {\n      val ids = (1..100).map { TraceContext.generateSpanId() }.toSet()\n\n      ids.size shouldBe 100\n    }\n\n    test(\"start should preserve existing OTel baggage entries\") {\n      val existingBaggage = Baggage\n        .builder()\n        .put(\"tenant.id\", \"acme-corp\")\n        .put(\"region\", \"eu-west-1\")\n        .build()\n      val preExistingScope = Context.current().with(existingBaggage).makeCurrent()\n\n      try {\n        TraceContext.start(\"test-baggage\")\n\n        val activeBaggage = Baggage.fromContext(Context.current())\n        activeBaggage.getEntryValue(\"tenant.id\") shouldBe \"acme-corp\"\n        activeBaggage.getEntryValue(\"region\") shouldBe \"eu-west-1\"\n        activeBaggage.getEntryValue(TraceContext.BAGGAGE_TEST_ID_KEY) shouldBe \"test-baggage\"\n      } finally {\n        TraceContext.clear()\n        preExistingScope.close()\n      }\n    }\n\n    test(\"start should work when no pre-existing baggage exists\") {\n      TraceContext.start(\"test-no-prior-baggage\")\n\n      val activeBaggage = Baggage.fromContext(Context.current())\n      activeBaggage.getEntryValue(TraceContext.BAGGAGE_TEST_ID_KEY) shouldBe \"test-no-prior-baggage\"\n    }\n\n    test(\"sanitizeToAscii should sanitize Turkish characters\") {\n      val input = \"ProductCreateCodeValidationTests::Geçerli, benzersiz code ile ürün oluşturma (happy-path)\"\n\n      val sanitized = TraceContext.sanitizeToAscii(input)\n\n      sanitized shouldBe \"ProductCreateCodeValidationTests::Gecerli, benzersiz code ile urun olusturma (happy-path)\"\n      // Verify all characters are ASCII printable\n      sanitized.all { it.code in 0x20..0x7E } shouldBe true\n    }\n\n    test(\"sanitizeToAscii should handle various non-ASCII characters\") {\n      val input = \"Test::äöü ñ café résumé naïve\"\n\n      val sanitized = TraceContext.sanitizeToAscii(input)\n\n      sanitized shouldBe \"Test::aou n cafe resume naive\"\n      sanitized.all { it.code in 0x20..0x7E } shouldBe true\n    }\n\n    test(\"sanitizeToAscii should preserve ASCII characters\") {\n      val input = \"SimpleTest::simple test name 123\"\n\n      val sanitized = TraceContext.sanitizeToAscii(input)\n\n      sanitized shouldBe \"SimpleTest::simple test name 123\"\n    }\n\n    test(\"sanitizeToAscii should handle Japanese characters with hash for uniqueness\") {\n      val input1 = \"MyTest::日本語テスト\"\n      val input2 = \"MyTest::別のテスト\"\n\n      val sanitized1 = TraceContext.sanitizeToAscii(input1)\n      val sanitized2 = TraceContext.sanitizeToAscii(input2)\n\n      // Japanese characters become underscores, but hash suffix ensures uniqueness\n      sanitized1.all { it.code in 0x20..0x7E } shouldBe true\n      sanitized2.all { it.code in 0x20..0x7E } shouldBe true\n      // Different inputs should produce different outputs\n      sanitized1 shouldNotBe sanitized2\n      // Should contain hash suffix (underscore followed by hex chars)\n      sanitized1 shouldMatch Regex(\"MyTest::_______.+\")\n    }\n\n    test(\"sanitizeToAscii should handle mixed scripts with hash\") {\n      val input = \"Test::Hello世界Test\"\n\n      val sanitized = TraceContext.sanitizeToAscii(input)\n\n      // Contains non-decomposable chars, so hash is added\n      sanitized.all { it.code in 0x20..0x7E } shouldBe true\n      sanitized shouldMatch Regex(\"Test::Hello__Test_.+\")\n    }\n  })\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/tracing/TraceTreeRendererTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.string.shouldContain\nimport io.kotest.matchers.string.shouldNotContain\n\nclass TraceTreeRendererTest :\n  FunSpec({\n\n    context(\"render\") {\n      test(\"should render single node with operation name and duration\") {\n        val node = SpanNode(createSpan(operationName = \"GET /api/test\", durationMs = 100))\n\n        val result = TraceTreeRenderer.render(node)\n\n        result shouldContain \"GET /api/test\"\n        result shouldContain \"[100ms]\"\n        result shouldContain \"✓\"\n      }\n\n      test(\"should render failed span with failure marker\") {\n        val node = SpanNode(createSpan(status = SpanStatus.ERROR))\n\n        val result = TraceTreeRenderer.render(node)\n\n        result shouldContain \"✗\"\n        result shouldContain \"FAILURE POINT\"\n      }\n\n      test(\"should render parent-child hierarchy\") {\n        val child = SpanNode(createSpan(spanId = \"child\", operationName = \"child-op\"))\n        val parent = SpanNode(createSpan(spanId = \"parent\", operationName = \"parent-op\"), listOf(child))\n\n        val result = TraceTreeRenderer.render(parent)\n\n        result shouldContain \"parent-op\"\n        result shouldContain \"child-op\"\n      }\n\n      test(\"should render all children\") {\n        val child1 = SpanNode(createSpan(spanId = \"child1\", operationName = \"first-child\"))\n        val child2 = SpanNode(createSpan(spanId = \"child2\", operationName = \"second-child\"))\n        val parent = SpanNode(createSpan(spanId = \"parent\", operationName = \"parent-op\"), listOf(child1, child2))\n\n        val result = TraceTreeRenderer.render(parent)\n\n        result shouldContain \"parent-op\"\n        result shouldContain \"first-child\"\n        result shouldContain \"second-child\"\n      }\n\n      test(\"should render exception info for failed span\") {\n        val exception = ExceptionInfo(\n          type = \"RuntimeException\",\n          message = \"Something failed\",\n          stackTrace = listOf(\"at Test.method(Test.kt:10)\")\n        )\n        val node = SpanNode(createSpan(status = SpanStatus.ERROR, exception = exception))\n\n        val result = TraceTreeRenderer.render(node)\n\n        result shouldContain \"Error: RuntimeException: Something failed\"\n        result shouldContain \"at Test.method(Test.kt:10)\"\n      }\n\n      test(\"should render relevant attributes when enabled\") {\n        val node = SpanNode(\n          createSpan(\n            attributes = mapOf(\n              \"http.method\" to \"POST\",\n              \"http.url\" to \"/api/users\",\n              \"custom.attr\" to \"ignored\"\n            )\n          )\n        )\n\n        val result = TraceTreeRenderer.render(node, includeAttributes = true)\n\n        result shouldContain \"http.method: POST\"\n        result shouldContain \"http.url: /api/users\"\n        result shouldNotContain \"custom.attr\"\n      }\n\n      test(\"should not render attributes when disabled\") {\n        val node = SpanNode(\n          createSpan(\n            attributes = mapOf(\"http.method\" to \"GET\")\n          )\n        )\n\n        val result = TraceTreeRenderer.render(node, includeAttributes = false)\n\n        result shouldNotContain \"http.method\"\n      }\n\n      test(\"should use custom attribute prefixes\") {\n        val node = SpanNode(\n          createSpan(\n            attributes = mapOf(\n              \"custom.key\" to \"value\",\n              \"http.method\" to \"GET\"\n            )\n          )\n        )\n\n        val result = TraceTreeRenderer.render(\n          node,\n          includeAttributes = true,\n          attributePrefixes = listOf(\"custom.\")\n        )\n\n        result shouldContain \"custom.key: value\"\n        result shouldNotContain \"http.method\"\n      }\n\n      test(\"should mark deepest failure point only\") {\n        val deepFailed = SpanNode(createSpan(spanId = \"deep\", status = SpanStatus.ERROR))\n        val middleFailed = SpanNode(createSpan(spanId = \"middle\", status = SpanStatus.ERROR), listOf(deepFailed))\n        val parent = SpanNode(createSpan(spanId = \"parent\", status = SpanStatus.OK), listOf(middleFailed))\n\n        val result = TraceTreeRenderer.render(parent)\n\n        // Only the deepest failure should have the marker\n        val lines = result.lines()\n        val markerCount = lines.count { it.contains(\"FAILURE POINT\") }\n        markerCount == 1\n      }\n    }\n\n    context(\"renderColored\") {\n      test(\"should include ANSI color codes for failed spans\") {\n        val node = SpanNode(createSpan(status = SpanStatus.ERROR, operationName = \"failed-op\"))\n\n        val result = TraceTreeRenderer.renderColored(node)\n\n        // Should contain ANSI escape codes\n        result shouldContain \"\\u001B[\"\n        result shouldContain \"failed-op\"\n        result shouldContain \"✗\"\n        result shouldContain \"FAILURE POINT\"\n      }\n\n      test(\"should color success spans green\") {\n        val node = SpanNode(createSpan(status = SpanStatus.OK, operationName = \"success-op\"))\n\n        val result = TraceTreeRenderer.renderColored(node)\n\n        // Should contain bright green color code for checkmark\n        result shouldContain \"\\u001B[92m✓\"\n      }\n\n      test(\"should color failure marker in bold yellow\") {\n        val node = SpanNode(createSpan(status = SpanStatus.ERROR))\n\n        val result = TraceTreeRenderer.renderColored(node)\n\n        // Should contain bold + bright yellow for failure marker\n        result shouldContain \"\\u001B[1m\\u001B[93m◄── FAILURE POINT\"\n      }\n\n      test(\"should color exception info with red and yellow\") {\n        val exception = ExceptionInfo(\n          type = \"RuntimeException\",\n          message = \"Test error\",\n          stackTrace = listOf(\"at Test.method(Test.kt:10)\")\n        )\n        val node = SpanNode(createSpan(status = SpanStatus.ERROR, exception = exception))\n\n        val result = TraceTreeRenderer.renderColored(node)\n\n        // Exception type should be yellow\n        result shouldContain \"\\u001B[33mRuntimeException\"\n      }\n    }\n\n    context(\"renderCompact\") {\n      test(\"should render compact format with indentation\") {\n        val child = SpanNode(createSpan(spanId = \"child\", operationName = \"child-op\", durationMs = 50))\n        val parent =\n          SpanNode(createSpan(spanId = \"parent\", operationName = \"parent-op\", durationMs = 100), listOf(child))\n\n        val result = TraceTreeRenderer.renderCompact(parent)\n\n        result shouldContain \"✓ parent-op (100ms)\"\n        result shouldContain \"  ✓ child-op (50ms)\"\n      }\n\n      test(\"should show failure status in compact format\") {\n        val node = SpanNode(createSpan(status = SpanStatus.ERROR, operationName = \"failed-op\"))\n\n        val result = TraceTreeRenderer.renderCompact(node)\n\n        result shouldContain \"✗ failed-op\"\n      }\n    }\n\n    context(\"renderSummary\") {\n      test(\"should render trace summary with counts\") {\n        val child = SpanNode(createSpan(spanId = \"child\"))\n        val parent = SpanNode(createSpan(spanId = \"parent\", durationMs = 200), listOf(child))\n\n        val result = TraceTreeRenderer.renderSummary(parent)\n\n        result shouldContain \"Trace Summary:\"\n        result shouldContain \"Total spans: 2\"\n        result shouldContain \"Failed spans: 0\"\n        result shouldContain \"Total duration: 200ms\"\n        result shouldContain \"Max depth: 2\"\n      }\n\n      test(\"should include failure point info when failures exist\") {\n        val exception = ExceptionInfo(\n          type = \"TestException\",\n          message = \"Test error\"\n        )\n        val failed = SpanNode(\n          createSpan(\n            spanId = \"failed\",\n            operationName = \"failed-operation\",\n            status = SpanStatus.ERROR,\n            exception = exception\n          )\n        )\n        val parent = SpanNode(createSpan(spanId = \"parent\"), listOf(failed))\n\n        val result = TraceTreeRenderer.renderSummary(parent)\n\n        result shouldContain \"Failed spans: 1\"\n        result shouldContain \"Failure point: failed-operation\"\n        result shouldContain \"Error: TestException: Test error\"\n      }\n\n      test(\"should not include failure info when no failures\") {\n        val node = SpanNode(createSpan(status = SpanStatus.OK))\n\n        val result = TraceTreeRenderer.renderSummary(node)\n\n        result shouldNotContain \"Failure point\"\n        result shouldNotContain \"Error:\"\n      }\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span456\",\n  parentSpanId: String? = null,\n  operationName: String = \"test-operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 0L,\n  endTimeNanos: Long = 1_000_000L,\n  durationMs: Long = 1L,\n  status: SpanStatus = SpanStatus.OK,\n  attributes: Map<String, String> = emptyMap(),\n  exception: ExceptionInfo? = null\n): SpanInfo {\n  val actualEndTime = if (durationMs != 1L) {\n    startTimeNanos + (durationMs * 1_000_000L)\n  } else {\n    endTimeNanos\n  }\n  return SpanInfo(\n    traceId = traceId,\n    spanId = spanId,\n    parentSpanId = parentSpanId,\n    operationName = operationName,\n    serviceName = serviceName,\n    startTimeNanos = startTimeNanos,\n    endTimeNanos = actualEndTime,\n    status = status,\n    attributes = attributes,\n    exception = exception\n  )\n}\n"
  },
  {
    "path": "lib/stove/src/test/kotlin/com/trendyol/stove/tracing/TraceVisualizationTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.doubles.shouldBeGreaterThan\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\n\nclass TraceVisualizationTest :\n  FunSpec({\n\n    context(\"TraceVisualization.from\") {\n      test(\"should create visualization from spans\") {\n        val spans = listOf(\n          createSpan(spanId = \"span1\", operationName = \"op1\"),\n          createSpan(spanId = \"span2\", operationName = \"op2\", parentSpanId = \"span1\")\n        )\n\n        val viz = TraceVisualization.from(\"trace-123\", \"test-1\", spans)\n\n        viz.traceId shouldBe \"trace-123\"\n        viz.testId shouldBe \"test-1\"\n        viz.totalSpans shouldBe 2\n        viz.spans.size shouldBe 2\n      }\n\n      test(\"should count failed spans\") {\n        val spans = listOf(\n          createSpan(spanId = \"span1\", status = SpanStatus.OK),\n          createSpan(spanId = \"span2\", status = SpanStatus.ERROR),\n          createSpan(spanId = \"span3\", status = SpanStatus.ERROR)\n        )\n\n        val viz = TraceVisualization.from(\"trace-123\", \"test-1\", spans)\n\n        viz.failedSpans shouldBe 2\n      }\n\n      test(\"should build tree representation\") {\n        val spans = listOf(\n          createSpan(spanId = \"root\", operationName = \"root-op\"),\n          createSpan(spanId = \"child\", operationName = \"child-op\", parentSpanId = \"root\")\n        )\n\n        val viz = TraceVisualization.from(\"trace-123\", \"test-1\", spans)\n\n        viz.tree shouldContain \"root-op\"\n        viz.tree shouldContain \"child-op\"\n      }\n\n      test(\"should handle empty spans list\") {\n        val viz = TraceVisualization.from(\"trace-123\", \"test-1\", emptyList())\n\n        viz.totalSpans shouldBe 0\n        viz.failedSpans shouldBe 0\n        viz.spans shouldBe emptyList()\n        viz.tree shouldBe \"No spans in trace\"\n      }\n    }\n\n    context(\"VisualSpan.from\") {\n      test(\"should convert SpanInfo to VisualSpan\") {\n        val span = createSpan(\n          spanId = \"span-123\",\n          parentSpanId = \"parent-456\",\n          operationName = \"GET /api/test\",\n          serviceName = \"test-service\",\n          startTimeNanos = 1000000L,\n          endTimeNanos = 6000000L,\n          status = SpanStatus.OK,\n          attributes = mapOf(\"http.method\" to \"GET\")\n        )\n\n        val visual = VisualSpan.from(span)\n\n        visual.spanId shouldBe \"span-123\"\n        visual.parentSpanId shouldBe \"parent-456\"\n        visual.operationName shouldBe \"GET /api/test\"\n        visual.serviceName shouldBe \"test-service\"\n        visual.durationMs shouldBe 5.0\n        visual.status shouldBe \"OK\"\n        visual.attributes shouldBe mapOf(\"http.method\" to \"GET\")\n      }\n\n      test(\"should calculate duration in milliseconds\") {\n        val span = createSpan(\n          startTimeNanos = 0L,\n          endTimeNanos = 10_000_000L // 10ms\n        )\n\n        val visual = VisualSpan.from(span)\n\n        visual.durationMs shouldBe 10.0\n      }\n\n      test(\"should handle zero duration\") {\n        val span = createSpan(\n          startTimeNanos = 1000L,\n          endTimeNanos = 1000L\n        )\n\n        val visual = VisualSpan.from(span)\n\n        visual.durationMs shouldBe 0.0\n      }\n\n      test(\"should handle sub-millisecond duration\") {\n        val span = createSpan(\n          startTimeNanos = 0L,\n          endTimeNanos = 500_000L // 0.5ms\n        )\n\n        val visual = VisualSpan.from(span)\n\n        visual.durationMs shouldBe 0.5\n      }\n\n      test(\"should convert ERROR status\") {\n        val span = createSpan(status = SpanStatus.ERROR)\n\n        val visual = VisualSpan.from(span)\n\n        visual.status shouldBe \"ERROR\"\n      }\n\n      test(\"should convert UNSET status\") {\n        val span = createSpan(status = SpanStatus.UNSET)\n\n        val visual = VisualSpan.from(span)\n\n        visual.status shouldBe \"UNSET\"\n      }\n\n      test(\"should handle null parent span id\") {\n        val span = createSpan(parentSpanId = null)\n\n        val visual = VisualSpan.from(span)\n\n        visual.parentSpanId shouldBe null\n      }\n\n      test(\"should preserve empty attributes\") {\n        val span = createSpan(attributes = emptyMap())\n\n        val visual = VisualSpan.from(span)\n\n        visual.attributes shouldBe emptyMap()\n      }\n\n      test(\"should return zero duration for invalid end time\") {\n        val span = SpanInfo(\n          traceId = \"trace\",\n          spanId = \"span\",\n          parentSpanId = null,\n          operationName = \"op\",\n          serviceName = \"svc\",\n          startTimeNanos = 1000L,\n          endTimeNanos = 0L, // Invalid: end before start\n          status = SpanStatus.OK\n        )\n\n        val visual = VisualSpan.from(span)\n\n        visual.durationMs shouldBe 0.0\n      }\n\n      test(\"should handle large duration values\") {\n        val span = createSpan(\n          startTimeNanos = 0L,\n          endTimeNanos = 60_000_000_000L // 60 seconds\n        )\n\n        val visual = VisualSpan.from(span)\n\n        visual.durationMs shouldBe 60000.0\n        visual.durationMs shouldBeGreaterThan 0.0\n      }\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span456\",\n  parentSpanId: String? = null,\n  operationName: String = \"test-operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 0L,\n  endTimeNanos: Long = 1_000_000L,\n  status: SpanStatus = SpanStatus.OK,\n  attributes: Map<String, String> = emptyMap(),\n  exception: ExceptionInfo? = null\n): SpanInfo = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = serviceName,\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status,\n  attributes = attributes,\n  exception = exception\n)\n"
  },
  {
    "path": "lib/stove/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.eclipse.jetty\" level=\"INFO\"/>\n    <logger name=\"io.netty\" level=\"INFO\"/>\n</configuration>\n"
  },
  {
    "path": "lib/stove/src/testFixtures/kotlin/com/trendyol/stove/CapturedOutput.kt",
    "content": "package com.trendyol.stove\n\nimport io.kotest.core.spec.style.FunSpec\nimport java.io.ByteArrayOutputStream\nimport java.io.PrintStream\n\nclass CapturedOutput(\n  private val outBuffer: ByteArrayOutputStream,\n  private val errBuffer: ByteArrayOutputStream\n) {\n  val out: String get() = outBuffer.toString()\n  val err: String get() = errBuffer.toString()\n}\n\nabstract class ConsoleSpec(\n  body: ConsoleSpec.(CapturedOutput) -> Unit = {}\n) : FunSpec({\n  val originalOut = System.out\n  val originalErr = System.err\n  val outBuffer = ByteArrayOutputStream()\n  val errBuffer = ByteArrayOutputStream()\n  val capturedOutput = CapturedOutput(outBuffer, errBuffer)\n\n  beforeSpec {\n    System.setOut(PrintStream(outBuffer))\n    System.setErr(PrintStream(outBuffer))\n  }\n\n  afterSpec {\n    System.setOut(originalOut)\n    System.setOut(originalErr)\n  }\n\n  beforeEach {\n    outBuffer.reset()\n    errBuffer.reset()\n  }\n\n  body(this as ConsoleSpec, capturedOutput)\n})\n"
  },
  {
    "path": "lib/stove-bom/build.gradle.kts",
    "content": "plugins {\n  `java-platform`\n  alias(libs.plugins.maven.publish)\n}\n\njavaPlatform {\n  allowDependencies()\n}\n\ndependencies {\n  constraints {\n    // Core\n    api(projects.lib.stove)\n\n    // Infrastructure\n    api(projects.lib.stoveCouchbase)\n    api(projects.lib.stoveElasticsearch)\n    api(projects.lib.stoveGrpc)\n    api(projects.lib.stoveHttp)\n    api(projects.lib.stoveKafka)\n    api(projects.lib.stoveMongodb)\n    api(projects.lib.stoveRdbms)\n    api(projects.lib.stovePostgres)\n    api(projects.lib.stoveMysql)\n    api(projects.lib.stoveMssql)\n    api(projects.lib.stoveRedis)\n    api(projects.lib.stoveCassandra)\n    api(projects.lib.stoveWiremock)\n    api(projects.lib.stoveGrpcMock)\n    api(projects.lib.stoveTracing)\n    api(projects.lib.stoveDashboard)\n    api(projects.lib.stoveDashboardApi)\n\n    // Starters\n    api(projects.starters.spring.stoveSpring)\n    api(projects.starters.spring.stoveSpringKafka)\n    api(projects.starters.ktor.stoveKtor)\n    api(projects.starters.quarkus.stoveQuarkus)\n    api(projects.starters.micronaut.stoveMicronaut)\n    api(projects.starters.container.stoveContainer)\n    api(projects.starters.process.stoveProcess)\n\n    // Extensions\n    api(projects.testExtensions.stoveExtensionsKotest)\n    api(projects.testExtensions.stoveExtensionsJunit)\n\n    // Gradle Plugins\n    api(projects.plugins.stoveTracingGradlePlugin)\n  }\n}\n\nmavenPublishing {\n  coordinates(groupId = rootProject.group.toString(), artifactId = project.name, version = rootProject.version.toString())\n  publishToMavenCentral()\n  pom {\n    name.set(project.name)\n    description.set(project.properties[\"projectDescription\"].toString())\n    url.set(project.properties[\"projectUrl\"].toString())\n    licenses {\n      license {\n        name.set(project.properties[\"licence\"].toString())\n        url.set(project.properties[\"licenceUrl\"].toString())\n      }\n    }\n    developers {\n      developer {\n        id.set(\"osoykan\")\n        name.set(\"Oguzhan Soykan\")\n        email.set(\"oguzhan.soykan@trendyol.com\")\n      }\n    }\n    scm {\n      connection.set(\"scm:git@github.com:Trendyol/stove.git\")\n      developerConnection.set(\"scm:git:ssh://github.com:Trendyol/stove.git\")\n      url.set(project.properties[\"projectUrl\"].toString())\n    }\n  }\n  if (hasSigningKey) signAllPublications()\n}\n"
  },
  {
    "path": "lib/stove-cassandra/api/stove-cassandra.api",
    "content": "public final class com/trendyol/stove/cassandra/CassandraContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/cassandra/CassandraContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/cassandra/CassandraContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/cassandra/CassandraContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/cassandra/CassandraContext {\n\tpublic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/cassandra/CassandraSystemOptions;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/cassandra/CassandraSystemOptions;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic final fun component2 ()Lcom/trendyol/stove/cassandra/CassandraSystemOptions;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/cassandra/CassandraSystemOptions;Ljava/lang/String;)Lcom/trendyol/stove/cassandra/CassandraContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/cassandra/CassandraContext;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/cassandra/CassandraSystemOptions;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/cassandra/CassandraContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getKeyName ()Ljava/lang/String;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/cassandra/CassandraSystemOptions;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract interface annotation class com/trendyol/stove/cassandra/CassandraDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/cassandra/CassandraExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()I\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/cassandra/CassandraExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/cassandra/CassandraExposedConfiguration;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/cassandra/CassandraExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getDatacenter ()Ljava/lang/String;\n\tpublic final fun getHost ()Ljava/lang/String;\n\tpublic final fun getKeyspace ()Ljava/lang/String;\n\tpublic final fun getPort ()I\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/cassandra/CassandraMigrationContext {\n\tpublic fun <init> (Lcom/datastax/oss/driver/api/core/CqlSession;Lcom/trendyol/stove/cassandra/CassandraSystemOptions;)V\n\tpublic final fun component1 ()Lcom/datastax/oss/driver/api/core/CqlSession;\n\tpublic final fun component2 ()Lcom/trendyol/stove/cassandra/CassandraSystemOptions;\n\tpublic final fun copy (Lcom/datastax/oss/driver/api/core/CqlSession;Lcom/trendyol/stove/cassandra/CassandraSystemOptions;)Lcom/trendyol/stove/cassandra/CassandraMigrationContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/cassandra/CassandraMigrationContext;Lcom/datastax/oss/driver/api/core/CqlSession;Lcom/trendyol/stove/cassandra/CassandraSystemOptions;ILjava/lang/Object;)Lcom/trendyol/stove/cassandra/CassandraMigrationContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getOptions ()Lcom/trendyol/stove/cassandra/CassandraSystemOptions;\n\tpublic final fun getSession ()Lcom/datastax/oss/driver/api/core/CqlSession;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/cassandra/CassandraSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field CASSANDRA_PORT I\n\tpublic static final field Companion Lcom/trendyol/stove/cassandra/CassandraSystem$Companion;\n\tpublic field cqlSession Lcom/datastax/oss/driver/api/core/CqlSession;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun getCqlSession ()Lcom/datastax/oss/driver/api/core/CqlSession;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun pause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun setCqlSession (Lcom/datastax/oss/driver/api/core/CqlSession;)V\n\tpublic final fun shouldExecute (Lcom/datastax/oss/driver/api/core/cql/BoundStatement;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldExecute (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldQuery (Lcom/datastax/oss/driver/api/core/cql/BoundStatement;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldQuery (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/cassandra/CassandraSystem$Companion {\n\tpublic final fun session (Lcom/trendyol/stove/cassandra/CassandraSystem;)Lcom/datastax/oss/driver/api/core/CqlSession;\n}\n\npublic class com/trendyol/stove/cassandra/CassandraSystemOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/cassandra/CassandraSystemOptions$Companion;\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/cassandra/CassandraContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/cassandra/CassandraContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainer ()Lcom/trendyol/stove/cassandra/CassandraContainerOptions;\n\tpublic fun getDatacenter ()Ljava/lang/String;\n\tpublic fun getKeyspace ()Ljava/lang/String;\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/cassandra/CassandraSystemOptions;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n}\n\npublic final class com/trendyol/stove/cassandra/CassandraSystemOptions$Companion {\n\tpublic final fun provided (Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/cassandra/ProvidedCassandraSystemOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/cassandra/CassandraSystemOptions$Companion;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/cassandra/ProvidedCassandraSystemOptions;\n}\n\npublic final class com/trendyol/stove/cassandra/OptionsKt {\n\tpublic static final fun cassandra-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun cassandra-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun cassandra-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun cassandra-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/cassandra/ProvidedCassandraSystemOptions : com/trendyol/stove/cassandra/CassandraSystemOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/cassandra/CassandraExposedConfiguration;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/cassandra/CassandraExposedConfiguration;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/cassandra/CassandraExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/cassandra/CassandraExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic class com/trendyol/stove/cassandra/StoveCassandraContainer : org/testcontainers/cassandra/CassandraContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\n"
  },
  {
    "path": "lib/stove-cassandra/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(libs.cassandra.driver.core)\n  api(libs.testcontainers.cassandra)\n}\n\ndependencies {\n  testImplementation(projects.testExtensions.stoveExtensionsKotest)\n  testImplementation(libs.logback.classic)\n}\n\nval testWithProvided = tasks.register<Test>(\"testWithProvided\") {\n  group = \"verification\"\n  description = \"Runs tests with an externally provided Cassandra instance\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useProvided\", \"true\")\n  doFirst {\n    println(\"Starting Cassandra tests with provided instance...\")\n  }\n}\n\ntasks.test.configure {\n  dependsOn(testWithProvided)\n}\n"
  },
  {
    "path": "lib/stove-cassandra/src/main/kotlin/com/trendyol/stove/cassandra/CassandraDsl.kt",
    "content": "package com.trendyol.stove.cassandra\n\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class CassandraDsl\n"
  },
  {
    "path": "lib/stove-cassandra/src/main/kotlin/com/trendyol/stove/cassandra/CassandraSystem.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.cassandra\n\nimport com.datastax.oss.driver.api.core.CqlSession\nimport com.datastax.oss.driver.api.core.cql.*\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport kotlinx.coroutines.*\nimport org.slf4j.*\nimport java.net.InetSocketAddress\n\n/**\n * Cassandra database system for testing CQL operations.\n *\n * Provides a DSL for testing Cassandra operations:\n * - CQL statement execution\n * - Query result assertions\n * - Keyspace and table management\n *\n * ## Executing Statements\n *\n * ```kotlin\n * cassandra {\n *     shouldExecute(\"INSERT INTO my_keyspace.users (id, name) VALUES (uuid(), 'John')\")\n * }\n * ```\n *\n * ## Querying Data\n *\n * ```kotlin\n * cassandra {\n *     shouldQuery(\"SELECT * FROM my_keyspace.users\") { resultSet ->\n *         val rows = resultSet.all()\n *         rows shouldHaveSize 1\n *         rows.first().getString(\"name\") shouldBe \"John\"\n *     }\n * }\n * ```\n *\n * ## Using the Raw Session\n *\n * ```kotlin\n * cassandra {\n *     session().execute(\"TRUNCATE my_keyspace.users\")\n * }\n * ```\n *\n * ## Test Workflow Example\n *\n * ```kotlin\n * test(\"should store user in Cassandra via API\") {\n *     stove {\n *         // Create user via API\n *         http {\n *             postAndExpectBody<UserResponse>(\n *                 uri = \"/users\",\n *                 body = CreateUserRequest(name = \"John\").some()\n *             ) { response ->\n *                 response.status shouldBe 201\n *             }\n *         }\n *\n *         // Verify in Cassandra\n *         cassandra {\n *             shouldQuery(\"SELECT * FROM my_keyspace.users WHERE name = 'John'\") { resultSet ->\n *                 val rows = resultSet.all()\n *                 rows shouldHaveSize 1\n *             }\n *         }\n *     }\n * }\n * ```\n *\n * ## Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         cassandra {\n *             CassandraSystemOptions(\n *                 keyspace = \"my_keyspace\",\n *                 configureExposedConfiguration = { cfg ->\n *                     listOf(\n *                         \"spring.cassandra.contact-points=${cfg.host}:${cfg.port}\",\n *                         \"spring.cassandra.local-datacenter=${cfg.datacenter}\",\n *                         \"spring.cassandra.keyspace-name=${cfg.keyspace}\"\n *                     )\n *                 }\n *             )\n *         }\n *     }\n * ```\n *\n * @property stove The parent test system.\n * @see CassandraSystemOptions\n * @see CassandraExposedConfiguration\n */\n@CassandraDsl\nclass CassandraSystem internal constructor(\n  override val stove: Stove,\n  private val context: CassandraContext\n) : PluggedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  @PublishedApi\n  internal lateinit var cqlSession: CqlSession\n\n  override val reportSystemName: String = \"Cassandra\" + (context.keyName?.let { \" [$it]\" } ?: \"\")\n  private lateinit var exposedConfiguration: CassandraExposedConfiguration\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val state: StateStorage<CassandraExposedConfiguration> =\n    stove.createStateStorage<CassandraExposedConfiguration, CassandraSystem>(context.keyName)\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n    cqlSession = createSession(exposedConfiguration)\n    runMigrationsIfNeeded()\n    rebindSessionToDefaultKeyspaceIfAvailable(exposedConfiguration)\n  }\n\n  override suspend fun stop(): Unit = whenContainer { it.stop() }\n\n  override fun configuration(): List<String> = context.options.configureExposedConfiguration(exposedConfiguration)\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      if (::cqlSession.isInitialized) {\n        context.options.cleanup(cqlSession)\n        cqlSession.close()\n      }\n    }.recover { logger.warn(\"Cassandra cleanup failed\", it) }\n    Try {\n      executeWithReuseCheck { stop() }\n    }.recover { logger.warn(\"Cassandra stop failed\", it) }\n  }\n\n  /**\n   * Executes a CQL statement and asserts that it completes without errors.\n   *\n   * @param cql The CQL statement to execute\n   * @return This [CassandraSystem] for chaining\n   */\n  suspend fun shouldExecute(cql: String): CassandraSystem {\n    report(\n      action = \"Execute CQL\",\n      input = arrow.core.Some(mapOf(\"cql\" to cql))\n    ) {\n      cqlSession.execute(cql)\n    }\n    return this\n  }\n\n  /**\n   * Executes a CQL query and passes the [ResultSet] to the [assertion] block.\n   *\n   * @param cql The CQL query to execute\n   * @param assertion A block that receives the [ResultSet] for assertions\n   * @return This [CassandraSystem] for chaining\n   */\n  suspend fun shouldQuery(\n    cql: String,\n    assertion: (ResultSet) -> Unit\n  ): CassandraSystem {\n    report(\n      action = \"Query Cassandra\",\n      input = arrow.core.Some(mapOf(\"cql\" to cql))\n    ) {\n      val resultSet = cqlSession.execute(cql)\n      assertion(resultSet)\n      resultSet\n    }\n    return this\n  }\n\n  /**\n   * Executes a [BoundStatement] and asserts that it completes without errors.\n   *\n   * @param statement The prepared [BoundStatement] to execute\n   * @return This [CassandraSystem] for chaining\n   */\n  suspend fun shouldExecute(statement: BoundStatement): CassandraSystem {\n    report(action = \"Execute Bound Statement\") {\n      cqlSession.execute(statement)\n    }\n    return this\n  }\n\n  /**\n   * Executes a [BoundStatement] query and passes the [ResultSet] to the [assertion] block.\n   *\n   * @param statement The prepared [BoundStatement] to execute\n   * @param assertion A block that receives the [ResultSet] for assertions\n   * @return This [CassandraSystem] for chaining\n   */\n  suspend fun shouldQuery(\n    statement: BoundStatement,\n    assertion: (ResultSet) -> Unit\n  ): CassandraSystem {\n    report(action = \"Query Cassandra (bound)\") {\n      val resultSet = cqlSession.execute(statement)\n      assertion(resultSet)\n      resultSet\n    }\n    return this\n  }\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return CassandraSystem\n   */\n  suspend fun pause(): CassandraSystem {\n    report(\n      action = \"Pause container\",\n      metadata = mapOf(\"operation\" to \"fault-injection\")\n    ) {\n      withContainerOrWarn(\"pause\") { it.pause() }\n    }\n    return this\n  }\n\n  /**\n   * Unpauses the container. Use with care, as it will unpause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return CassandraSystem\n   */\n  suspend fun unpause(): CassandraSystem {\n    report(action = \"Unpause container\") {\n      withContainerOrWarn(\"unpause\") { it.unpause() }\n    }\n    return this\n  }\n\n  private suspend fun obtainExposedConfiguration(): CassandraExposedConfiguration =\n    when {\n      context.options is ProvidedCassandraSystemOptions -> context.options.config\n      context.runtime is StoveCassandraContainer -> startCassandraContainer(context.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n    }\n\n  private suspend fun startCassandraContainer(container: StoveCassandraContainer): CassandraExposedConfiguration =\n    state.capture {\n      container.start()\n      CassandraExposedConfiguration(\n        host = container.host,\n        port = container.getMappedPort(CASSANDRA_PORT),\n        datacenter = container.localDatacenter,\n        keyspace = context.options.keyspace\n      )\n    }\n\n  @Suppress(\"TooGenericExceptionCaught\")\n  private suspend fun createSession(\n    config: CassandraExposedConfiguration\n  ): CqlSession = createSession(config, useKeyspace = false)\n\n  @Suppress(\"TooGenericExceptionCaught\")\n  private suspend fun createSession(\n    config: CassandraExposedConfiguration,\n    useKeyspace: Boolean\n  ): CqlSession {\n    var lastException: Exception? = null\n    repeat(SESSION_CREATE_MAX_ATTEMPTS) { attempt ->\n      try {\n        return CqlSession\n          .builder()\n          .addContactPoint(InetSocketAddress(config.host, config.port))\n          .withLocalDatacenter(config.datacenter)\n          .apply {\n            if (useKeyspace) {\n              withKeyspace(config.keyspace)\n            }\n          }.build()\n      } catch (e: CancellationException) {\n        throw e\n      } catch (e: Exception) {\n        lastException = e\n        logger.warn(\n          \"Failed to create CQL session (attempt ${attempt + 1}/$SESSION_CREATE_MAX_ATTEMPTS): \" +\n            \"${e.message}. Retrying in ${SESSION_CREATE_RETRY_DELAY_MS}ms...\"\n        )\n        if (attempt < SESSION_CREATE_MAX_ATTEMPTS - 1) {\n          delay(SESSION_CREATE_RETRY_DELAY_MS)\n        }\n      }\n    }\n    throw IllegalStateException(\n      \"Failed to create CQL session after $SESSION_CREATE_MAX_ATTEMPTS attempts\",\n      lastException\n    )\n  }\n\n  @Suppress(\"TooGenericExceptionCaught\")\n  private suspend fun rebindSessionToDefaultKeyspaceIfAvailable(config: CassandraExposedConfiguration) {\n    if (!configuredKeyspaceExists(config.keyspace)) {\n      logger.info(\n        \"Configured Cassandra keyspace '{}' is not available yet; continuing without a default keyspace\",\n        config.keyspace\n      )\n      return\n    }\n\n    val currentSession = cqlSession\n    try {\n      cqlSession = createSession(config, useKeyspace = true)\n      currentSession.close()\n    } catch (e: CancellationException) {\n      throw e\n    } catch (e: Exception) {\n      logger.warn(\"Failed to rebind session to keyspace '${config.keyspace}', continuing with keyspace-less session\", e)\n    }\n  }\n\n  private fun configuredKeyspaceExists(keyspace: String): Boolean {\n    val statement = cqlSession\n      .prepare(\"SELECT keyspace_name FROM system_schema.keyspaces WHERE keyspace_name = ?\")\n      .bind(keyspace)\n    return cqlSession.execute(statement).one() != null\n  }\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StoveCassandraContainer) -> Unit\n  ): CassandraSystem = when (val runtime = context.runtime) {\n    is StoveCassandraContainer -> {\n      action(runtime)\n      this\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"$operation() is not supported when using a provided instance\")\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private inline fun whenContainer(action: (StoveCassandraContainer) -> Unit) {\n    if (context.runtime is StoveCassandraContainer) {\n      action(context.runtime)\n    }\n  }\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (shouldRunMigrations()) {\n      context.options.migrationCollection.run(CassandraMigrationContext(cqlSession, context.options))\n    }\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    context.options is ProvidedCassandraSystemOptions -> context.options.runMigrations\n    context.runtime is StoveCassandraContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n  }\n\n  companion object {\n    const val CASSANDRA_PORT = 9042\n    private const val SESSION_CREATE_MAX_ATTEMPTS = 10\n    private const val SESSION_CREATE_RETRY_DELAY_MS = 3_000L\n\n    /**\n     * Exposes the [CqlSession] to the [CassandraSystem].\n     * Use this for advanced Cassandra operations not covered by the DSL.\n     */\n    fun CassandraSystem.session(): CqlSession = cqlSession\n  }\n}\n"
  },
  {
    "path": "lib/stove-cassandra/src/main/kotlin/com/trendyol/stove/cassandra/CassandraSystemOptions.kt",
    "content": "package com.trendyol.stove.cassandra\n\nimport com.datastax.oss.driver.api.core.CqlSession\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n/**\n * Context provided to Cassandra migrations.\n * Contains the CQL session and options for performing setup operations.\n *\n * @property session The CQL session for executing statements\n * @property options The Cassandra system options\n */\n@StoveDsl\ndata class CassandraMigrationContext(\n  val session: CqlSession,\n  val options: CassandraSystemOptions\n)\n\n/**\n * Convenience type alias for Cassandra migrations.\n *\n * Instead of writing `DatabaseMigration<CassandraMigrationContext>`, use `CassandraMigration`:\n * ```kotlin\n * class MyMigration : CassandraMigration {\n *   override val order: Int = 1\n *   override suspend fun execute(connection: CassandraMigrationContext) { ... }\n * }\n * ```\n */\ntypealias CassandraMigration = DatabaseMigration<CassandraMigrationContext>\n\n/**\n * Options for configuring the Cassandra system in container mode.\n */\n@StoveDsl\nopen class CassandraSystemOptions(\n  open val keyspace: String = \"stove\",\n  open val datacenter: String = \"datacenter1\",\n  open val container: CassandraContainerOptions = CassandraContainerOptions(),\n  open val cleanup: suspend (CqlSession) -> Unit = {},\n  override val configureExposedConfiguration: (CassandraExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<CassandraExposedConfiguration>,\n  SupportsMigrations<CassandraMigrationContext, CassandraSystemOptions> {\n  override val migrationCollection: MigrationCollection<CassandraMigrationContext> = MigrationCollection()\n\n  companion object {\n    /**\n     * Creates options configured to use an externally provided Cassandra instance\n     * instead of a testcontainer.\n     *\n     * @param host The Cassandra host\n     * @param port The Cassandra native transport port (default: 9042)\n     * @param datacenter The local datacenter name (default: \"datacenter1\")\n     * @param keyspace The default keyspace to use\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      host: String,\n      port: Int = 9042,\n      datacenter: String = \"datacenter1\",\n      keyspace: String = \"stove\",\n      runMigrations: Boolean = true,\n      cleanup: suspend (CqlSession) -> Unit = {},\n      configureExposedConfiguration: (CassandraExposedConfiguration) -> List<String>\n    ): ProvidedCassandraSystemOptions = ProvidedCassandraSystemOptions(\n      config = CassandraExposedConfiguration(\n        host = host,\n        port = port,\n        datacenter = datacenter,\n        keyspace = keyspace\n      ),\n      keyspace = keyspace,\n      datacenter = datacenter,\n      runMigrations = runMigrations,\n      cleanup = cleanup,\n      configureExposedConfiguration = configureExposedConfiguration\n    )\n  }\n}\n\n/**\n * Options for using an externally provided Cassandra instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedCassandraSystemOptions(\n  /**\n   * The configuration for the provided Cassandra instance.\n   */\n  val config: CassandraExposedConfiguration,\n  keyspace: String = \"stove\",\n  datacenter: String = \"datacenter1\",\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  cleanup: suspend (CqlSession) -> Unit = {},\n  configureExposedConfiguration: (CassandraExposedConfiguration) -> List<String>\n) : CassandraSystemOptions(\n  keyspace = keyspace,\n  datacenter = datacenter,\n  container = CassandraContainerOptions(),\n  cleanup = cleanup,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<CassandraExposedConfiguration> {\n  override val providedConfig: CassandraExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n"
  },
  {
    "path": "lib/stove-cassandra/src/main/kotlin/com/trendyol/stove/cassandra/Options.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.cassandra\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.containers.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.testcontainers.cassandra.CassandraContainer\nimport org.testcontainers.utility.DockerImageName\n\n@StoveDsl\ndata class CassandraExposedConfiguration(\n  val host: String,\n  val port: Int,\n  val datacenter: String,\n  val keyspace: String\n) : ExposedConfiguration\n\n@StoveDsl\ndata class CassandraContext(\n  val runtime: SystemRuntime,\n  val options: CassandraSystemOptions,\n  val keyName: String? = null\n)\n\nopen class StoveCassandraContainer(\n  override val imageNameAccess: DockerImageName\n) : CassandraContainer(imageNameAccess),\n  StoveContainer\n\n@StoveDsl\ndata class CassandraContainerOptions(\n  override val registry: String = DEFAULT_REGISTRY,\n  override val image: String = \"cassandra\",\n  override val tag: String = \"4\",\n  override val compatibleSubstitute: String? = null,\n  override val useContainerFn: UseContainerFn<StoveCassandraContainer> = { StoveCassandraContainer(it) },\n  override val containerFn: ContainerFn<StoveCassandraContainer> = { }\n) : ContainerOptions<StoveCassandraContainer>\n\ninternal fun Stove.withCassandra(\n  options: CassandraSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(CassandraSystem(this, CassandraContext(runtime, options)))\n  return this\n}\n\ninternal fun Stove.withCassandra(\n  key: SystemKey,\n  options: CassandraSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(key, CassandraSystem(this, CassandraContext(runtime, options, keyName = keyDisplayName(key))))\n  return this\n}\n\ninternal fun Stove.cassandra(): CassandraSystem =\n  getOrNone<CassandraSystem>().getOrElse {\n    throw SystemNotRegisteredException(CassandraSystem::class)\n  }\n\ninternal fun Stove.cassandra(key: SystemKey): CassandraSystem =\n  getOrNone<CassandraSystem>(key).getOrElse {\n    throw SystemNotRegisteredException(CassandraSystem::class, \"No CassandraSystem registered with key '${keyDisplayName(key)}'\")\n  }\n\n/**\n * Configures Cassandra system.\n *\n * For container-based setup:\n * ```kotlin\n * cassandra {\n *   CassandraSystemOptions(\n *     keyspace = \"my_keyspace\",\n *     cleanup = { session -> session.execute(\"TRUNCATE my_keyspace.my_table\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * cassandra {\n *   CassandraSystemOptions.provided(\n *     host = \"localhost\",\n *     port = 9042,\n *     datacenter = \"datacenter1\",\n *     keyspace = \"my_keyspace\",\n *     cleanup = { session -> session.execute(\"TRUNCATE my_keyspace.my_table\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n */\nfun WithDsl.cassandra(\n  configure: () -> CassandraSystemOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedCassandraSystemOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveCassandraContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withCassandra(options, runtime)\n}\n\nfun WithDsl.cassandra(\n  key: SystemKey,\n  configure: () -> CassandraSystemOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedCassandraSystemOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveCassandraContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withCassandra(key, options, runtime)\n}\n\nsuspend fun ValidationDsl.cassandra(\n  validation: @CassandraDsl suspend CassandraSystem.() -> Unit\n): Unit = validation(this.stove.cassandra())\n\nsuspend fun ValidationDsl.cassandra(\n  key: SystemKey,\n  validation: @CassandraDsl suspend CassandraSystem.() -> Unit\n): Unit = validation(this.stove.cassandra(key))\n"
  },
  {
    "path": "lib/stove-cassandra/src/test/kotlin/com/trendyol/stove/cassandra/CassandraOptionsTests.kt",
    "content": "package com.trendyol.stove.cassandra\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\n\nclass CassandraOptionsTests :\n  FunSpec({\n\n    test(\"CassandraExposedConfiguration should hold connection details\") {\n      val cfg = CassandraExposedConfiguration(\n        host = \"localhost\",\n        port = 9042,\n        datacenter = \"datacenter1\",\n        keyspace = \"my_keyspace\"\n      )\n\n      cfg.host shouldBe \"localhost\"\n      cfg.port shouldBe 9042\n      cfg.datacenter shouldBe \"datacenter1\"\n      cfg.keyspace shouldBe \"my_keyspace\"\n    }\n\n    test(\"CassandraSystemOptions.provided should create ProvidedCassandraSystemOptions with correct config\") {\n      val options = CassandraSystemOptions.provided(\n        host = \"cassandra-host\",\n        port = 9042,\n        datacenter = \"datacenter1\",\n        keyspace = \"test_keyspace\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"cassandra.contact-points=${cfg.host}:${cfg.port}\",\n            \"cassandra.local-datacenter=${cfg.datacenter}\"\n          )\n        }\n      )\n\n      options.providedConfig.host shouldBe \"cassandra-host\"\n      options.providedConfig.port shouldBe 9042\n      options.providedConfig.datacenter shouldBe \"datacenter1\"\n      options.providedConfig.keyspace shouldBe \"test_keyspace\"\n      options.runMigrationsForProvided shouldBe true\n    }\n\n    test(\"ProvidedCassandraSystemOptions should expose correct properties\") {\n      val config = CassandraExposedConfiguration(\n        host = \"remote\",\n        port = 9043,\n        datacenter = \"dc1\",\n        keyspace = \"prod_keyspace\"\n      )\n      val options = ProvidedCassandraSystemOptions(\n        config = config,\n        keyspace = \"prod_keyspace\",\n        datacenter = \"dc1\",\n        runMigrations = false,\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.config shouldBe config\n      options.providedConfig shouldBe config\n      options.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"CassandraSystemOptions should have sensible defaults\") {\n      val options = object : CassandraSystemOptions(\n        configureExposedConfiguration = { _ -> listOf() }\n      ) {}\n\n      options.keyspace shouldBe \"stove\"\n      options.datacenter shouldBe \"datacenter1\"\n      options.container shouldNotBe null\n    }\n\n    test(\"CassandraSystemOptions.provided should default port to 9042\") {\n      val options = CassandraSystemOptions.provided(\n        host = \"localhost\",\n        keyspace = \"test\",\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.providedConfig.port shouldBe 9042\n    }\n\n    test(\"CassandraSystemOptions.provided should default datacenter to datacenter1\") {\n      val options = CassandraSystemOptions.provided(\n        host = \"localhost\",\n        keyspace = \"test\",\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.providedConfig.datacenter shouldBe \"datacenter1\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove-cassandra/src/test/kotlin/com/trendyol/stove/cassandra/CassandraSystemTests.kt",
    "content": "package com.trendyol.stove.cassandra\n\nimport com.trendyol.stove.cassandra.CassandraSystem.Companion.session\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport io.kotest.core.spec.style.ShouldSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport org.slf4j.*\nimport org.testcontainers.cassandra.CassandraContainer\nimport org.testcontainers.utility.DockerImageName\n\n// ============================================================================\n// Shared components\n// ============================================================================\n\nclass NoOpApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) = Unit\n\n  override suspend fun stop() = Unit\n}\n\n// ============================================================================\n// Strategy interface\n// ============================================================================\n\nsealed interface CassandraTestStrategy {\n  val logger: Logger\n\n  suspend fun start()\n\n  suspend fun stop()\n\n  companion object {\n    fun select(): CassandraTestStrategy {\n      val useProvided = System.getenv(\"USE_PROVIDED\")?.toBoolean()\n        ?: System.getProperty(\"useProvided\")?.toBoolean()\n        ?: false\n\n      return if (useProvided) ProvidedCassandraStrategy() else ContainerCassandraStrategy()\n    }\n  }\n}\n\n// ============================================================================\n// Container-based strategy (default)\n// ============================================================================\n\nclass ContainerCassandraStrategy : CassandraTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun start() {\n    logger.info(\"Starting Cassandra tests with container mode\")\n\n    val options = CassandraSystemOptions(\n      keyspace = \"stove\",\n      container = CassandraContainerOptions(),\n      configureExposedConfiguration = { _ -> listOf() }\n    ).migrations {\n      register<CreateKeyspaceMigration>()\n    }\n\n    Stove()\n      .with {\n        cassandra { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    logger.info(\"Cassandra container tests completed\")\n  }\n}\n\n// ============================================================================\n// Provided instance strategy\n// ============================================================================\n\nclass ProvidedCassandraStrategy : CassandraTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  private lateinit var externalContainer: CassandraContainer\n\n  override suspend fun start() {\n    logger.info(\"Starting Cassandra tests with provided mode\")\n\n    externalContainer = CassandraContainer(DockerImageName.parse(\"cassandra:4\"))\n      .apply { start() }\n\n    logger.info(\"External Cassandra container started at ${externalContainer.host}:${externalContainer.firstMappedPort}\")\n\n    val options = CassandraSystemOptions\n      .provided(\n        host = externalContainer.host,\n        port = externalContainer.firstMappedPort,\n        datacenter = externalContainer.localDatacenter,\n        keyspace = \"stove\",\n        runMigrations = true,\n        cleanup = { _ ->\n          logger.info(\"Running cleanup on provided instance\")\n        },\n        configureExposedConfiguration = { _ -> listOf() }\n      ).migrations {\n        register<CreateKeyspaceMigration>()\n      }\n\n    Stove()\n      .with {\n        cassandra { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    try {\n      com.trendyol.stove.system.Stove\n        .stop()\n    } catch (e: IllegalStateException) {\n      logger.warn(\"Stove.stop() failed (may not have been initialized): ${e.message}\")\n    }\n    if (::externalContainer.isInitialized) {\n      externalContainer.stop()\n    }\n    logger.info(\"Cassandra provided tests completed\")\n  }\n}\n\n// ============================================================================\n// Migrations\n// ============================================================================\n\nclass CreateKeyspaceMigration : CassandraMigration {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: CassandraMigrationContext) {\n    connection.session.execute(\n      \"\"\"\n      CREATE KEYSPACE IF NOT EXISTS ${connection.options.keyspace}\n        WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}\n      \"\"\".trimIndent()\n    )\n  }\n}\n\n// ============================================================================\n// Kotest project config - selects strategy based on environment\n// ============================================================================\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val strategy = CassandraTestStrategy.select()\n\n  override suspend fun beforeProject() = strategy.start()\n\n  override suspend fun afterProject() = strategy.stop()\n}\n\n// ============================================================================\n// Tests\n// ============================================================================\n\nclass CassandraSystemTests :\n  ShouldSpec({\n\n    should(\"execute a CQL statement without error\") {\n      stove {\n        cassandra {\n          shouldExecute(\"CREATE TABLE IF NOT EXISTS stove.test_table (id uuid PRIMARY KEY, value text)\")\n        }\n      }\n    }\n\n    should(\"query data from Cassandra\") {\n      stove {\n        cassandra {\n          shouldExecute(\"CREATE TABLE IF NOT EXISTS stove.query_test (id uuid PRIMARY KEY, name text)\")\n          shouldExecute(\"INSERT INTO stove.query_test (id, name) VALUES (uuid(), 'test-value')\")\n          shouldQuery(\"SELECT * FROM stove.query_test\") { resultSet ->\n            val rows = resultSet.all()\n            rows.isNotEmpty() shouldBe true\n            rows.first().getString(\"name\") shouldBe \"test-value\"\n          }\n        }\n      }\n    }\n\n    should(\"use the configured keyspace as the default session keyspace\") {\n      stove {\n        cassandra {\n          shouldExecute(\"CREATE TABLE IF NOT EXISTS default_keyspace_test (id uuid PRIMARY KEY, name text)\")\n          shouldExecute(\"INSERT INTO default_keyspace_test (id, name) VALUES (uuid(), 'default-keyspace')\")\n          shouldQuery(\"SELECT * FROM default_keyspace_test\") { resultSet ->\n            val rows = resultSet.all()\n            rows.isNotEmpty() shouldBe true\n            rows.first().getString(\"name\") shouldBe \"default-keyspace\"\n          }\n        }\n      }\n    }\n\n    should(\"provide access to the raw CQL session\") {\n      stove {\n        cassandra {\n          val result = session().execute(\"SELECT release_version FROM system.local\")\n          result.one()?.getString(\"release_version\") shouldNotBe null\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-cassandra/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.cassandra.StoveConfig\n"
  },
  {
    "path": "lib/stove-cassandra/src/test/resources/logback.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "lib/stove-couchbase/api/stove-couchbase.api",
    "content": "public final class com/trendyol/stove/couchbase/CouchbaseContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/couchbase/CouchbaseContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/couchbase/CouchbaseContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/couchbase/CouchbaseContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/couchbase/CouchbaseContext {\n\tpublic fun <init> (Lorg/testcontainers/couchbase/BucketDefinition;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/couchbase/CouchbaseSystemOptions;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lorg/testcontainers/couchbase/BucketDefinition;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/couchbase/CouchbaseSystemOptions;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lorg/testcontainers/couchbase/BucketDefinition;\n\tpublic final fun component2 ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic final fun component3 ()Lcom/trendyol/stove/couchbase/CouchbaseSystemOptions;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun copy (Lorg/testcontainers/couchbase/BucketDefinition;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/couchbase/CouchbaseSystemOptions;Ljava/lang/String;)Lcom/trendyol/stove/couchbase/CouchbaseContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/couchbase/CouchbaseContext;Lorg/testcontainers/couchbase/BucketDefinition;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/couchbase/CouchbaseSystemOptions;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/couchbase/CouchbaseContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBucket ()Lorg/testcontainers/couchbase/BucketDefinition;\n\tpublic final fun getKeyName ()Ljava/lang/String;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/couchbase/CouchbaseSystemOptions;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract interface annotation class com/trendyol/stove/couchbase/CouchbaseDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/couchbase/CouchbaseExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/couchbase/CouchbaseExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/couchbase/CouchbaseExposedConfiguration;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/couchbase/CouchbaseExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getConnectionString ()Ljava/lang/String;\n\tpublic final fun getHostsWithPort ()Ljava/lang/String;\n\tpublic final fun getPassword ()Ljava/lang/String;\n\tpublic final fun getUsername ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/couchbase/CouchbaseSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field Companion Lcom/trendyol/stove/couchbase/CouchbaseSystem$Companion;\n\tpublic field cluster Lcom/couchbase/client/kotlin/Cluster;\n\tpublic field collection Lcom/couchbase/client/kotlin/Collection;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun getCluster ()Lcom/couchbase/client/kotlin/Cluster;\n\tpublic final fun getCollection ()Lcom/couchbase/client/kotlin/Collection;\n\tpublic final fun getContext ()Lcom/trendyol/stove/couchbase/CouchbaseContext;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun pause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun setCluster (Lcom/couchbase/client/kotlin/Cluster;)V\n\tpublic final fun setCollection (Lcom/couchbase/client/kotlin/Collection;)V\n\tpublic final fun shouldDelete (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldDelete (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldNotExist (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldNotExist (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/couchbase/CouchbaseSystem$Companion {\n\tpublic final fun bucket (Lcom/trendyol/stove/couchbase/CouchbaseSystem;)Lcom/couchbase/client/kotlin/Bucket;\n\tpublic final fun cluster (Lcom/trendyol/stove/couchbase/CouchbaseSystem;)Lcom/couchbase/client/kotlin/Cluster;\n}\n\npublic class com/trendyol/stove/couchbase/CouchbaseSystemOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/couchbase/CouchbaseSystemOptions$Companion;\n\tpublic fun <init> (Ljava/lang/String;Lcom/trendyol/stove/couchbase/CouchbaseContainerOptions;Lcom/couchbase/client/kotlin/codec/JsonSerializer;Lcom/couchbase/client/kotlin/codec/Transcoder;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Lcom/trendyol/stove/couchbase/CouchbaseContainerOptions;Lcom/couchbase/client/kotlin/codec/JsonSerializer;Lcom/couchbase/client/kotlin/codec/Transcoder;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getClusterSerDe ()Lcom/couchbase/client/kotlin/codec/JsonSerializer;\n\tpublic fun getClusterTranscoder ()Lcom/couchbase/client/kotlin/codec/Transcoder;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainerOptions ()Lcom/trendyol/stove/couchbase/CouchbaseContainerOptions;\n\tpublic fun getDefaultBucket ()Ljava/lang/String;\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/couchbase/CouchbaseSystemOptions;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n}\n\npublic final class com/trendyol/stove/couchbase/CouchbaseSystemOptions$Companion {\n\tpublic final fun provided (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/couchbase/ProvidedCouchbaseSystemOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/couchbase/CouchbaseSystemOptions$Companion;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/couchbase/ProvidedCouchbaseSystemOptions;\n}\n\npublic final class com/trendyol/stove/couchbase/OptionsKt {\n\tpublic static final fun couchbase-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun couchbase-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun couchbase-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun couchbase-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/couchbase/ProvidedCouchbaseSystemOptions : com/trendyol/stove/couchbase/CouchbaseSystemOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/couchbase/CouchbaseExposedConfiguration;Ljava/lang/String;Lcom/couchbase/client/kotlin/codec/JsonSerializer;Lcom/couchbase/client/kotlin/codec/Transcoder;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/couchbase/CouchbaseExposedConfiguration;Ljava/lang/String;Lcom/couchbase/client/kotlin/codec/JsonSerializer;Lcom/couchbase/client/kotlin/codec/Transcoder;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/couchbase/CouchbaseExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/couchbase/CouchbaseExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic class com/trendyol/stove/couchbase/StoveCouchbaseContainer : org/testcontainers/couchbase/CouchbaseContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\npublic final class com/trendyol/stove/couchbase/UtilKt {\n\tpublic static final fun waitForKeySpaceAvailability-45ZY6uE (Lcom/couchbase/client/kotlin/Cluster;Ljava/lang/String;Ljava/lang/String;JJLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun waitForKeySpaceAvailability-45ZY6uE$default (Lcom/couchbase/client/kotlin/Cluster;Ljava/lang/String;Ljava/lang/String;JJLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic static final fun waitUntilIndexIsCreated-WPi__2c (Lcom/couchbase/client/kotlin/Cluster;Ljava/lang/String;JJLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun waitUntilIndexIsCreated-WPi__2c$default (Lcom/couchbase/client/kotlin/Cluster;Ljava/lang/String;JJLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic static final fun waitUntilSucceeds-SYHnMyU (Lcom/couchbase/client/kotlin/Cluster;Lkotlin/jvm/functions/Function1;JJLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun waitUntilSucceeds-SYHnMyU$default (Lcom/couchbase/client/kotlin/Cluster;Lkotlin/jvm/functions/Function1;JJLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n}\n\n"
  },
  {
    "path": "lib/stove-couchbase/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(libs.couchbase.kotlin)\n  api(libs.testcontainers.couchbase)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.slf4j.simple)\n}\n\nval testWithProvided = tasks.register<Test>(\"testWithProvided\") {\n  group = \"verification\"\n  description = \"Runs tests with an externally provided Couchbase instance\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useProvided\", \"true\")\n  doFirst {\n    println(\"Starting Couchbase tests with provided instance...\")\n  }\n}\n\ntasks.test.configure {\n  dependsOn(testWithProvided)\n}\n"
  },
  {
    "path": "lib/stove-couchbase/src/main/kotlin/com/trendyol/stove/couchbase/CouchbaseDsl.kt",
    "content": "package com.trendyol.stove.couchbase\n\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class CouchbaseDsl\n"
  },
  {
    "path": "lib/stove-couchbase/src/main/kotlin/com/trendyol/stove/couchbase/CouchbaseSystem.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.couchbase\n\nimport com.couchbase.client.kotlin.*\nimport com.couchbase.client.kotlin.Collection\nimport com.couchbase.client.kotlin.codec.typeRef\nimport com.couchbase.client.kotlin.query.*\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.runBlocking\nimport org.slf4j.*\n\n/**\n * Couchbase document database system for testing document storage operations.\n *\n * Provides a DSL for testing Couchbase operations:\n * - Document CRUD operations (save, get, delete)\n * - N1QL queries\n * - Collection management\n * - Existence checks\n *\n * ## Saving Documents\n *\n * ```kotlin\n * couchbase {\n *     // Save to default collection\n *     save(\"user::123\", User(id = \"123\", name = \"John\"))\n *\n *     // Save to specific collection\n *     save(\"users\", \"user::123\", User(id = \"123\", name = \"John\"))\n *\n *     // Save with custom options\n *     saveWithOptions(\"user::123\", user) { options ->\n *         options.expiry(Duration.ofHours(24))\n *     }\n * }\n * ```\n *\n * ## Retrieving Documents\n *\n * ```kotlin\n * couchbase {\n *     // Get from default collection and assert\n *     shouldGet<User>(\"user::123\") { user ->\n *         user.name shouldBe \"John\"\n *         user.email shouldBe \"john@example.com\"\n *     }\n *\n *     // Get from specific collection\n *     shouldGet<User>(\"users\", \"user::123\") { user ->\n *         user.name shouldBe \"John\"\n *     }\n * }\n * ```\n *\n * ## N1QL Queries\n *\n * ```kotlin\n * couchbase {\n *     // Execute N1QL query and assert results\n *     shouldQuery<User>(\n *         \"SELECT * FROM `my-bucket` WHERE type = 'user' AND status = 'active'\"\n *     ) { users ->\n *         users.size shouldBeGreaterThan 0\n *         users.all { it.status == \"active\" } shouldBe true\n *     }\n * }\n * ```\n *\n * ## Deleting Documents\n *\n * ```kotlin\n * couchbase {\n *     // Delete from default collection\n *     shouldDelete(\"user::123\")\n *\n *     // Delete from specific collection\n *     shouldDelete(\"users\", \"user::123\")\n * }\n * ```\n *\n * ## Existence Checks\n *\n * ```kotlin\n * couchbase {\n *     // Assert document doesn't exist\n *     shouldNotExist(\"user::deleted\")\n *\n *     // In specific collection\n *     shouldNotExist(\"users\", \"user::deleted\")\n * }\n * ```\n *\n * ## Test Workflow Example\n *\n * ```kotlin\n * test(\"should create user via API and store in Couchbase\") {\n *     stove {\n *         // Setup: ensure clean state\n *         couchbase {\n *             shouldNotExist(\"user::new-user\")\n *         }\n *\n *         // Action: create user via API\n *         http {\n *             postAndExpectBodilessResponse(\n *                 uri = \"/users\",\n *                 body = CreateUserRequest(name = \"New User\").some()\n *             ) { response ->\n *                 response.status shouldBe 201\n *             }\n *         }\n *\n *         // Assert: verify in Couchbase\n *         couchbase {\n *             shouldGet<User>(\"users\", \"user::new-user\") { user ->\n *                 user.name shouldBe \"New User\"\n *                 user.createdAt shouldNotBe null\n *             }\n *         }\n *     }\n * }\n * ```\n *\n * ## Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         couchbase {\n *             CouchbaseSystemOptions(\n *                 defaultBucket = \"my-bucket\",\n *                 configureExposedConfiguration = { cfg ->\n *                     listOf(\n *                         \"couchbase.connection-string=${cfg.connectionString}\",\n *                         \"couchbase.username=${cfg.username}\",\n *                         \"couchbase.password=${cfg.password}\"\n *                     )\n *                 }\n *             )\n *         }\n *     }\n * ```\n *\n * @property stove The parent test system.\n * @property context Couchbase context containing bucket and options.\n * @see CouchbaseSystemOptions\n * @see CouchbaseExposedConfiguration\n */\n@CouchbaseDsl\nclass CouchbaseSystem internal constructor(\n  override val stove: Stove,\n  val context: CouchbaseContext\n) : PluggedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  @PublishedApi\n  internal lateinit var cluster: Cluster\n\n  @PublishedApi\n  internal lateinit var collection: Collection\n\n  override val reportSystemName: String = \"Couchbase\" + (context.keyName?.let { \" [$it]\" } ?: \"\")\n\n  private lateinit var exposedConfiguration: CouchbaseExposedConfiguration\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val state: StateStorage<CouchbaseExposedConfiguration> =\n    stove.createStateStorage<CouchbaseExposedConfiguration, CouchbaseSystem>(context.keyName)\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n    cluster = createCluster(exposedConfiguration)\n    collection = cluster.bucket(context.bucket.name).defaultCollection()\n    runMigrationsIfNeeded()\n  }\n\n  override suspend fun stop(): Unit = whenContainer { it.stop() }\n\n  override fun configuration(): List<String> = context.options.configureExposedConfiguration(exposedConfiguration)\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      context.options.cleanup(cluster)\n      cluster.disconnect()\n      executeWithReuseCheck { stop() }\n    }.recover {\n      logger.warn(\"Disconnecting the couchbase cluster got an error: $it\")\n    }\n  }\n\n  suspend inline fun <reified T : Any> shouldQuery(\n    query: String,\n    crossinline assertion: (List<T>) -> Unit\n  ): CouchbaseSystem {\n    val typeRef = typeRef<T>()\n    report(\n      action = \"N1QL Query\",\n      input = arrow.core.Some(query)\n    ) {\n      val results = flow {\n        cluster\n          .query(\n            statement = query,\n            metrics = false,\n            consistency = QueryScanConsistency.requestPlus(),\n            serializer = context.options.clusterSerDe\n          ).execute { row -> emit(context.options.clusterSerDe.deserialize(row.content, typeRef)) }\n      }.toList()\n      assertion(results)\n      results\n    }\n    return this\n  }\n\n  suspend inline fun <reified T : Any> shouldGet(\n    key: String,\n    crossinline assertion: (T) -> Unit\n  ): CouchbaseSystem {\n    report(\n      action = \"Get document\",\n      input = arrow.core.Some(mapOf(\"id\" to key))\n    ) {\n      val document = collection.get(key).contentAs<T>()\n      assertion(document)\n      document\n    }\n    return this\n  }\n\n  suspend inline fun <reified T : Any> shouldGet(\n    collection: String,\n    key: String,\n    crossinline assertion: (T) -> Unit\n  ): CouchbaseSystem {\n    report(\n      action = \"Get document\",\n      input = arrow.core.Some(mapOf(\"collection\" to collection, \"id\" to key))\n    ) {\n      val document = cluster\n        .bucket(context.bucket.name)\n        .collection(collection)\n        .get(key)\n        .contentAs<T>()\n      assertion(document)\n      document\n    }\n    return this\n  }\n\n  suspend fun shouldNotExist(key: String): CouchbaseSystem {\n    report(\n      action = \"Document should not exist\",\n      input = arrow.core.Some(mapOf(\"id\" to key)),\n      expected = arrow.core.Some(\"Document not found\")\n    ) {\n      val exists = collection.getOrNull(key) != null\n      if (exists) throw AssertionError(\"The document with the given id($key) was not expected, but found!\")\n    }\n    return this\n  }\n\n  suspend fun shouldNotExist(\n    collection: String,\n    key: String\n  ): CouchbaseSystem {\n    report(\n      action = \"Document should not exist\",\n      input = arrow.core.Some(mapOf(\"collection\" to collection, \"id\" to key)),\n      expected = arrow.core.Some(\"Document not found\")\n    ) {\n      val exists = cluster\n        .bucket(context.bucket.name)\n        .collection(collection)\n        .getOrNull(key) != null\n      if (exists) throw AssertionError(\"The document with the given id($key) was not expected, but found!\")\n    }\n    return this\n  }\n\n  suspend fun shouldDelete(key: String): CouchbaseSystem {\n    report(\n      action = \"Delete document\",\n      input = arrow.core.Some(mapOf(\"id\" to key))\n    ) {\n      collection.remove(key)\n    }\n    return this\n  }\n\n  suspend fun shouldDelete(\n    collection: String,\n    key: String\n  ): CouchbaseSystem {\n    report(\n      action = \"Delete document\",\n      input = arrow.core.Some(mapOf(\"collection\" to collection, \"id\" to key))\n    ) {\n      cluster\n        .bucket(context.bucket.name)\n        .collection(collection)\n        .remove(key)\n    }\n    return this\n  }\n\n  /**\n   * Saves the [instance] with given [id] to the [collection]\n   * To save to the default collection use [saveToDefaultCollection]\n   */\n  suspend inline fun <reified T : Any> save(\n    collection: String,\n    id: String,\n    instance: T\n  ): CouchbaseSystem {\n    report(\n      action = \"Save document\",\n      input = arrow.core.Some(instance),\n      metadata = mapOf(\"collection\" to collection, \"id\" to id)\n    ) {\n      cluster.bucket(context.bucket.name).collection(collection).insert(id, instance)\n    }\n    return this\n  }\n\n  /**\n   * Saves the [instance] with given [id] to the default collection\n   * In couchbase the default collection is `_default`\n   */\n  suspend inline fun <reified T : Any> saveToDefaultCollection(\n    id: String,\n    instance: T\n  ): CouchbaseSystem = this.save(\"_default\", id, instance)\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return CouchbaseSystem\n   */\n  suspend fun pause(): CouchbaseSystem {\n    report(\n      action = \"Pause container\",\n      metadata = mapOf(\"operation\" to \"fault-injection\")\n    ) {\n      withContainerOrWarn(\"pause\") { it.pause() }\n    }\n    return this\n  }\n\n  /**\n   * Unpauses the container. Use with care, as it will unpause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return CouchbaseSystem\n   */\n  suspend fun unpause(): CouchbaseSystem {\n    report(action = \"Unpause container\") {\n      withContainerOrWarn(\"unpause\") { it.unpause() }\n    }\n    return this\n  }\n\n  private suspend fun obtainExposedConfiguration(): CouchbaseExposedConfiguration =\n    when {\n      context.options is ProvidedCouchbaseSystemOptions -> context.options.config\n      context.runtime is StoveCouchbaseContainer -> startCouchbaseContainer(context.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n    }\n\n  private suspend fun startCouchbaseContainer(container: StoveCouchbaseContainer): CouchbaseExposedConfiguration =\n    state.capture {\n      container.start()\n      CouchbaseExposedConfiguration(\n        connectionString = container.connectionString,\n        hostsWithPort = container.connectionString.replace(\"couchbase://\", \"\"),\n        username = container.username,\n        password = container.password\n      )\n    }\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (shouldRunMigrations()) {\n      context.options.migrationCollection.run(cluster)\n    }\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    context.options is ProvidedCouchbaseSystemOptions -> context.options.runMigrations\n    context.runtime is StoveCouchbaseContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n  }\n\n  private fun createCluster(exposedConfiguration: CouchbaseExposedConfiguration): Cluster = Cluster.connect(\n    exposedConfiguration.hostsWithPort,\n    exposedConfiguration.username,\n    exposedConfiguration.password\n  ) {\n    jsonSerializer = context.options.clusterSerDe\n    transcoder = context.options.clusterTranscoder\n  }\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StoveCouchbaseContainer) -> Unit\n  ): CouchbaseSystem = when (val runtime = context.runtime) {\n    is StoveCouchbaseContainer -> {\n      action(runtime)\n      this\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"$operation() is not supported when using a provided instance\")\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private inline fun whenContainer(action: (StoveCouchbaseContainer) -> Unit) {\n    if (context.runtime is StoveCouchbaseContainer) {\n      action(context.runtime)\n    }\n  }\n\n  companion object {\n    /**\n     * Exposes the [Cluster] to the [CouchbaseSystem].\n     * Use this for advanced Couchbase operations not covered by the DSL.\n     */\n    fun CouchbaseSystem.cluster(): Cluster = this.cluster\n\n    /**\n     * Exposes the [Bucket] to the [CouchbaseSystem].\n     * Use this for advanced Couchbase operations not covered by the DSL.\n     */\n    fun CouchbaseSystem.bucket(): Bucket = this.cluster.bucket(this.context.bucket.name)\n  }\n}\n"
  },
  {
    "path": "lib/stove-couchbase/src/main/kotlin/com/trendyol/stove/couchbase/Options.kt",
    "content": "package com.trendyol.stove.couchbase\n\nimport arrow.core.getOrElse\nimport com.couchbase.client.kotlin.Cluster\nimport com.couchbase.client.kotlin.codec.*\nimport com.trendyol.stove.containers.*\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.serialization.E2eObjectMapperConfig\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.testcontainers.couchbase.BucketDefinition\n\ndata class CouchbaseExposedConfiguration(\n  val connectionString: String,\n  val hostsWithPort: String,\n  val username: String,\n  val password: String\n) : ExposedConfiguration\n\n/**\n * Options for configuring the Couchbase system in container mode.\n */\n@StoveDsl\nopen class CouchbaseSystemOptions(\n  open val defaultBucket: String,\n  open val containerOptions: CouchbaseContainerOptions = CouchbaseContainerOptions(),\n  open val clusterSerDe: JsonSerializer = JacksonJsonSerializer(E2eObjectMapperConfig.createObjectMapperWithDefaults()),\n  open val clusterTranscoder: Transcoder = JsonTranscoder(clusterSerDe),\n  open val cleanup: suspend (Cluster) -> Unit = {},\n  override val configureExposedConfiguration: (CouchbaseExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<CouchbaseExposedConfiguration>,\n  SupportsMigrations<Cluster, CouchbaseSystemOptions> {\n  override val migrationCollection: MigrationCollection<Cluster> = MigrationCollection()\n\n  companion object {\n    /**\n     * Creates options configured to use an externally provided Couchbase instance\n     * instead of a testcontainer.\n     *\n     * @param connectionString The Couchbase connection string (e.g., \"couchbase://localhost:8091\")\n     * @param username The username for authentication\n     * @param password The password for authentication\n     * @param defaultBucket The default bucket name\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      connectionString: String,\n      username: String,\n      password: String,\n      defaultBucket: String,\n      runMigrations: Boolean = true,\n      cleanup: suspend (Cluster) -> Unit = {},\n      configureExposedConfiguration: (CouchbaseExposedConfiguration) -> List<String>\n    ): ProvidedCouchbaseSystemOptions {\n      val hostsWithPort = connectionString.replace(\"couchbase://\", \"\")\n      return ProvidedCouchbaseSystemOptions(\n        config = CouchbaseExposedConfiguration(\n          connectionString = connectionString,\n          hostsWithPort = hostsWithPort,\n          username = username,\n          password = password\n        ),\n        defaultBucket = defaultBucket,\n        runMigrations = runMigrations,\n        cleanup = cleanup,\n        configureExposedConfiguration = configureExposedConfiguration\n      )\n    }\n  }\n}\n\n/**\n * Options for using an externally provided Couchbase instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedCouchbaseSystemOptions(\n  /**\n   * The configuration for the provided Couchbase instance.\n   */\n  val config: CouchbaseExposedConfiguration,\n  defaultBucket: String,\n  clusterSerDe: JsonSerializer = JacksonJsonSerializer(E2eObjectMapperConfig.createObjectMapperWithDefaults()),\n  clusterTranscoder: Transcoder = JsonTranscoder(clusterSerDe),\n  cleanup: suspend (Cluster) -> Unit = {},\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  configureExposedConfiguration: (CouchbaseExposedConfiguration) -> List<String>\n) : CouchbaseSystemOptions(\n  defaultBucket = defaultBucket,\n  containerOptions = CouchbaseContainerOptions(),\n  clusterSerDe = clusterSerDe,\n  clusterTranscoder = clusterTranscoder,\n  cleanup = cleanup,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<CouchbaseExposedConfiguration> {\n  override val providedConfig: CouchbaseExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n\n/**\n * Convenience type alias for Couchbase migrations.\n *\n * Instead of writing `DatabaseMigration<Cluster>`, use `CouchbaseMigration`:\n * ```kotlin\n * class MyMigration : CouchbaseMigration {\n *   override val order: Int = 1\n *   override suspend fun execute(connection: Cluster) { ... }\n * }\n * ```\n */\ntypealias CouchbaseMigration = DatabaseMigration<Cluster>\n\n@StoveDsl\ndata class CouchbaseContext(\n  val bucket: BucketDefinition,\n  val runtime: SystemRuntime,\n  val options: CouchbaseSystemOptions,\n  val keyName: String? = null\n)\n\n@StoveDsl\ndata class CouchbaseContainerOptions(\n  override val registry: String = DEFAULT_REGISTRY,\n  override val image: String = \"couchbase/server\",\n  override val tag: String = \"latest\",\n  override val compatibleSubstitute: String? = null,\n  override val useContainerFn: UseContainerFn<StoveCouchbaseContainer> = { StoveCouchbaseContainer(it) },\n  override val containerFn: ContainerFn<StoveCouchbaseContainer> = { }\n) : ContainerOptions<StoveCouchbaseContainer>\n\ninternal fun Stove.withCouchbase(\n  options: CouchbaseSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  val bucketDefinition = BucketDefinition(options.defaultBucket)\n  this.getOrRegister(\n    CouchbaseSystem(this, CouchbaseContext(bucketDefinition, runtime, options))\n  )\n  return this\n}\n\ninternal fun Stove.withCouchbase(\n  key: SystemKey,\n  options: CouchbaseSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  val bucketDefinition = BucketDefinition(options.defaultBucket)\n  this.getOrRegister(\n    key,\n    CouchbaseSystem(this, CouchbaseContext(bucketDefinition, runtime, options, keyName = keyDisplayName(key)))\n  )\n  return this\n}\n\ninternal fun Stove.couchbase(): CouchbaseSystem =\n  getOrNone<CouchbaseSystem>().getOrElse {\n    throw SystemNotRegisteredException(CouchbaseSystem::class)\n  }\n\ninternal fun Stove.couchbase(key: SystemKey): CouchbaseSystem =\n  getOrNone<CouchbaseSystem>(key).getOrElse {\n    throw SystemNotRegisteredException(CouchbaseSystem::class, \"No CouchbaseSystem registered with key '${keyDisplayName(key)}'\")\n  }\n\n/**\n * Configures Couchbase system.\n *\n * For container-based setup:\n * ```kotlin\n * couchbase {\n *   CouchbaseSystemOptions(\n *     defaultBucket = \"myBucket\",\n *     cleanup = { cluster -> cluster.query(\"DELETE FROM ...\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * couchbase {\n *   CouchbaseSystemOptions.provided(\n *     connectionString = \"couchbase://localhost:8091\",\n *     username = \"admin\",\n *     password = \"password\",\n *     defaultBucket = \"myBucket\",\n *     runMigrations = true,\n *     cleanup = { cluster -> cluster.query(\"DELETE FROM ...\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n */\nfun WithDsl.couchbase(\n  configure: @StoveDsl () -> CouchbaseSystemOptions\n): Stove {\n  val options = configure()\n  val bucketDefinition = BucketDefinition(options.defaultBucket)\n\n  val runtime: SystemRuntime = if (options is ProvidedCouchbaseSystemOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      imageName = options.containerOptions.imageWithTag,\n      registry = options.containerOptions.registry,\n      compatibleSubstitute = options.containerOptions.compatibleSubstitute\n    ) { dockerImageName ->\n      options.containerOptions\n        .useContainerFn(dockerImageName)\n        .withBucket(bucketDefinition)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveCouchbaseContainer }\n        .apply(options.containerOptions.containerFn)\n    }\n  }\n\n  return stove.withCouchbase(options, runtime)\n}\n\nfun WithDsl.couchbase(\n  key: SystemKey,\n  configure: @StoveDsl () -> CouchbaseSystemOptions\n): Stove {\n  val options = configure()\n  val bucketDefinition = BucketDefinition(options.defaultBucket)\n\n  val runtime: SystemRuntime = if (options is ProvidedCouchbaseSystemOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      imageName = options.containerOptions.imageWithTag,\n      registry = options.containerOptions.registry,\n      compatibleSubstitute = options.containerOptions.compatibleSubstitute\n    ) { dockerImageName ->\n      options.containerOptions\n        .useContainerFn(dockerImageName)\n        .withBucket(bucketDefinition)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveCouchbaseContainer }\n        .apply(options.containerOptions.containerFn)\n    }\n  }\n\n  return stove.withCouchbase(key, options, runtime)\n}\n\nsuspend fun ValidationDsl.couchbase(\n  validation: @CouchbaseDsl suspend CouchbaseSystem.() -> Unit\n): Unit = validation(this.stove.couchbase())\n\nsuspend fun ValidationDsl.couchbase(\n  key: SystemKey,\n  validation: @CouchbaseDsl suspend CouchbaseSystem.() -> Unit\n): Unit = validation(this.stove.couchbase(key))\n"
  },
  {
    "path": "lib/stove-couchbase/src/main/kotlin/com/trendyol/stove/couchbase/StoveCouchbaseContainer.kt",
    "content": "package com.trendyol.stove.couchbase\n\nimport com.trendyol.stove.containers.StoveContainer\nimport org.testcontainers.couchbase.CouchbaseContainer\nimport org.testcontainers.utility.DockerImageName\n\nopen class StoveCouchbaseContainer(\n  override val imageNameAccess: DockerImageName\n) : CouchbaseContainer(imageNameAccess),\n  StoveContainer\n"
  },
  {
    "path": "lib/stove-couchbase/src/main/kotlin/com/trendyol/stove/couchbase/util.kt",
    "content": "package com.trendyol.stove.couchbase\n\nimport com.couchbase.client.core.error.*\nimport com.couchbase.client.kotlin.Cluster\nimport com.couchbase.client.kotlin.query.execute\nimport com.trendyol.stove.functional.*\nimport kotlinx.coroutines.delay\nimport java.util.concurrent.TimeoutException\nimport kotlin.time.Duration.Companion.minutes\n\nsuspend fun Cluster.waitForKeySpaceAvailability(\n  bucketName: String,\n  keyspaceName: String,\n  duration: kotlin.time.Duration,\n  delayMillis: Long = 1000,\n  logger: (log: String) -> Unit = ::println\n): Unit = waitUntilSucceeds(\n  continueIf = { it is CollectionNotFoundException },\n  duration = duration,\n  delayMillis = delayMillis,\n  logger = logger\n) { bucket(bucketName).defaultScope().collection(keyspaceName).exists(\"not-important\") }\n\nsuspend fun Cluster.waitUntilIndexIsCreated(\n  query: String,\n  duration: kotlin.time.Duration,\n  delayMillis: Long = 50,\n  logger: (log: String) -> Unit = ::println\n): Unit = waitUntilSucceeds(\n  continueIf = { it is IndexFailureException },\n  duration = duration,\n  delayMillis = delayMillis,\n  logger = logger\n) { query(query, readonly = false).execute() }\n\nsuspend fun Cluster.waitUntilSucceeds(\n  continueIf: (Throwable) -> Boolean,\n  duration: kotlin.time.Duration = 10.minutes,\n  delayMillis: Long = 50,\n  logger: (log: String) -> Unit = ::println,\n  block: suspend Cluster.() -> Unit\n) {\n  val startTime = System.currentTimeMillis()\n  while (System.currentTimeMillis() - startTime < duration.inWholeMilliseconds) {\n    val executed = Try {\n      this.block()\n      true\n    }.recover { throwable ->\n      logger(\"Operation failed.\\nBecause of: $throwable\")\n      when {\n        continueIf(throwable) -> false\n        else -> throw throwable\n      }\n    }.get()\n\n    if (executed) {\n      logger(\"Operation executed successfully\")\n      return\n    }\n\n    logger(\"Operation is not successful. Waiting for $delayMillis ms...\")\n    delay(delayMillis)\n  }\n\n  throw TimeoutException(\"Timed out waiting for the operation!\")\n}\n"
  },
  {
    "path": "lib/stove-couchbase/src/test/kotlin/com/trendyol/stove/couchbase/CouchbaseOptionsTest.kt",
    "content": "package com.trendyol.stove.couchbase\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\n\nclass CouchbaseOptionsTest :\n  FunSpec({\n\n    test(\"CouchbaseExposedConfiguration should hold connection details\") {\n      val cfg = CouchbaseExposedConfiguration(\n        connectionString = \"couchbase://localhost:8091\",\n        hostsWithPort = \"localhost:8091\",\n        username = \"admin\",\n        password = \"password\"\n      )\n\n      cfg.connectionString shouldBe \"couchbase://localhost:8091\"\n      cfg.hostsWithPort shouldBe \"localhost:8091\"\n      cfg.username shouldBe \"admin\"\n      cfg.password shouldBe \"password\"\n    }\n\n    test(\"CouchbaseSystemOptions.provided should create ProvidedCouchbaseSystemOptions\") {\n      val options = CouchbaseSystemOptions.provided(\n        connectionString = \"couchbase://cb-host:8091\",\n        username = \"admin\",\n        password = \"pass\",\n        defaultBucket = \"test-bucket\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\"couchbase.hosts=${cfg.hostsWithPort}\")\n        }\n      )\n\n      options.providedConfig.connectionString shouldBe \"couchbase://cb-host:8091\"\n      options.providedConfig.hostsWithPort shouldBe \"cb-host:8091\"\n      options.providedConfig.username shouldBe \"admin\"\n      options.providedConfig.password shouldBe \"pass\"\n      options.runMigrationsForProvided shouldBe true\n    }\n\n    test(\"CouchbaseSystemOptions.provided should strip couchbase:// prefix for hostsWithPort\") {\n      val options = CouchbaseSystemOptions.provided(\n        connectionString = \"couchbase://node1:8091,node2:8091\",\n        username = \"u\",\n        password = \"p\",\n        defaultBucket = \"b\",\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.providedConfig.hostsWithPort shouldBe \"node1:8091,node2:8091\"\n    }\n\n    test(\"ProvidedCouchbaseSystemOptions should expose correct properties\") {\n      val config = CouchbaseExposedConfiguration(\n        connectionString = \"couchbase://remote:8091\",\n        hostsWithPort = \"remote:8091\",\n        username = \"u\",\n        password = \"p\"\n      )\n      val options = ProvidedCouchbaseSystemOptions(\n        config = config,\n        defaultBucket = \"bucket\",\n        runMigrations = false,\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.config shouldBe config\n      options.providedConfig shouldBe config\n      options.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"CouchbaseContainerOptions should have defaults\") {\n      val opts = CouchbaseContainerOptions()\n      opts.image shouldBe \"couchbase/server\"\n      opts.tag shouldBe \"latest\"\n    }\n\n    test(\"CouchbaseSystemOptions should have sensible defaults\") {\n      val options = object : CouchbaseSystemOptions(\n        defaultBucket = \"default\",\n        configureExposedConfiguration = { _ -> listOf() }\n      ) {}\n\n      options.defaultBucket shouldBe \"default\"\n      options.containerOptions shouldNotBe null\n      options.clusterSerDe shouldNotBe null\n      options.clusterTranscoder shouldNotBe null\n    }\n  })\n"
  },
  {
    "path": "lib/stove-couchbase/src/test/kotlin/com/trendyol/stove/couchbase/CouchbaseTestSystemTests.kt",
    "content": "package com.trendyol.stove.couchbase\n\nimport com.couchbase.client.core.error.DocumentNotFoundException\nimport com.trendyol.stove.couchbase.CouchbaseSystem.Companion.bucket\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.ints.shouldBeGreaterThanOrEqual\nimport io.kotest.matchers.shouldBe\nimport org.junit.jupiter.api.assertThrows\nimport java.util.*\n\n/**\n * Couchbase system tests that run against both container-based and provided instances.\n *\n * These tests verify:\n * - Basic CRUD operations work correctly\n * - Migrations are executed properly (creating collections)\n * - The same test code works for both container and provided modes\n *\n * To run with provided instance mode:\n * ```\n * ./gradlew :lib:stove-testing-e2e-couchbase:test -DuseProvided=true\n * ```\n */\nclass CouchbaseTestSystemUsesDslTests :\n  FunSpec({\n\n    data class ExampleInstance(\n      val id: String,\n      val description: String\n    )\n\n    test(\"migration should create 'another' collection\") {\n      val id = UUID.randomUUID().toString()\n      val anotherCollectionName = \"another\"\n      stove {\n        couchbase {\n          // This test verifies that the migration created the 'another' collection\n          save(anotherCollectionName, id = id, ExampleInstance(id = id, description = \"migration test\"))\n          shouldGet<ExampleInstance>(anotherCollectionName, id) { actual ->\n            actual.id shouldBe id\n            actual.description shouldBe \"migration test\"\n          }\n          shouldDelete(anotherCollectionName, id)\n        }\n      }\n    }\n\n    test(\"should save and get\") {\n      val id = UUID.randomUUID().toString()\n      val anotherCollectionName = \"another\"\n      stove {\n        couchbase {\n          saveToDefaultCollection(id, ExampleInstance(id = id, description = testCase.name.name))\n          save(anotherCollectionName, id = id, ExampleInstance(id = id, description = testCase.name.name))\n          shouldGet<ExampleInstance>(id) { actual ->\n            actual.id shouldBe id\n            actual.description shouldBe testCase.name.name\n          }\n          shouldGet<ExampleInstance>(anotherCollectionName, id) { actual ->\n            actual.id shouldBe id\n            actual.description shouldBe testCase.name.name\n          }\n        }\n      }\n    }\n\n    test(\"should not get when document does not exist\") {\n      val id = UUID.randomUUID().toString()\n      val notExistDocId = UUID.randomUUID().toString()\n      stove {\n        couchbase {\n          saveToDefaultCollection(id, ExampleInstance(id = id, description = testCase.name.name))\n          shouldGet<ExampleInstance>(id) { actual ->\n            actual.id shouldBe id\n            actual.description shouldBe testCase.name.name\n          }\n          shouldNotExist(notExistDocId)\n        }\n      }\n    }\n\n    test(\"should throw assertion exception when document exist\") {\n      val id = UUID.randomUUID().toString()\n      stove {\n        couchbase {\n          saveToDefaultCollection(id, ExampleInstance(id = id, description = testCase.name.name))\n          shouldGet<ExampleInstance>(id) { actual ->\n            actual.id shouldBe id\n            actual.description shouldBe testCase.name.name\n          }\n          assertThrows<AssertionError> { shouldNotExist(id) }\n        }\n      }\n    }\n\n    test(\"should delete\") {\n      val id = UUID.randomUUID().toString()\n      stove {\n        couchbase {\n          saveToDefaultCollection(id, ExampleInstance(id = id, description = testCase.name.name))\n          shouldGet<ExampleInstance>(id) { actual ->\n            actual.id shouldBe id\n            actual.description shouldBe testCase.name.name\n          }\n          shouldDelete(id)\n          shouldNotExist(id)\n        }\n      }\n    }\n\n    test(\"should delete from another collection\") {\n      val id = UUID.randomUUID().toString()\n      val anotherCollectionName = \"another\"\n      stove {\n        couchbase {\n          save(anotherCollectionName, id = id, ExampleInstance(id = id, description = testCase.name.name))\n          shouldGet<ExampleInstance>(anotherCollectionName, id) { actual ->\n            actual.id shouldBe id\n            actual.description shouldBe testCase.name.name\n          }\n          shouldDelete(anotherCollectionName, id)\n          shouldNotExist(anotherCollectionName, id)\n        }\n      }\n    }\n\n    test(\"should not delete when document does not exist\") {\n      val id = UUID.randomUUID().toString()\n      stove {\n        couchbase {\n          shouldNotExist(id)\n          assertThrows<DocumentNotFoundException> { shouldDelete(id) }\n        }\n      }\n    }\n\n    test(\"should not delete from another collection when document does not exist\") {\n      val id = UUID.randomUUID().toString()\n      val anotherCollectionName = \"another\"\n      stove {\n        couchbase {\n          shouldNotExist(anotherCollectionName, id)\n          assertThrows<DocumentNotFoundException> { shouldDelete(anotherCollectionName, id) }\n        }\n      }\n    }\n\n    test(\"should query\") {\n      val id = UUID.randomUUID().toString()\n      val id2 = UUID.randomUUID().toString()\n      stove {\n        couchbase {\n          saveToDefaultCollection(id, ExampleInstance(id = id, description = testCase.name.name))\n          saveToDefaultCollection(id2, ExampleInstance(id = id2, description = testCase.name.name))\n          shouldQuery<ExampleInstance>(\n            \"SELECT c.id, c.* FROM `${this.bucket().name}`.`${this.collection.scope.name}`.`${this.collection.name}` c\"\n          ) { result ->\n            result.size shouldBeGreaterThanOrEqual 2\n            result.contains(ExampleInstance(id = id, description = testCase.name.name)) shouldBe true\n            result.contains(ExampleInstance(id = id2, description = testCase.name.name)) shouldBe true\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-couchbase/src/test/kotlin/com/trendyol/stove/couchbase/TestSystemConfig.kt",
    "content": "package com.trendyol.stove.couchbase\n\nimport com.couchbase.client.kotlin.Cluster\nimport com.trendyol.stove.couchbase.CouchbaseSystem.Companion.bucket\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.slf4j.*\nimport org.testcontainers.couchbase.*\nimport org.testcontainers.utility.DockerImageName\nimport kotlin.time.Duration.Companion.seconds\n\n// ============================================================================\n// Shared components\n// ============================================================================\n\nconst val TEST_BUCKET = \"test-couchbase-bucket\"\n\nclass NoOpApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) = Unit\n\n  override suspend fun stop() = Unit\n}\n\n/**\n * Extended container for testing container customization.\n */\nclass ExtendedCouchbaseContainer(\n  dockerImageName: DockerImageName\n) : StoveCouchbaseContainer(dockerImageName) {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override fun start() {\n    logger.info(\"starting extended couchbase container\")\n    super.start()\n  }\n\n  override fun stop() {\n    logger.info(\"stopping extended couchbase container\")\n    super.stop()\n  }\n}\n\n/**\n * Migration that creates the 'another' collection for testing.\n */\nclass DefaultMigration : CouchbaseMigration {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override val order: Int = MigrationPriority.HIGHEST.value\n\n  override suspend fun execute(connection: Cluster) {\n    connection\n      .bucket(TEST_BUCKET)\n      .collections\n      .createCollection(\"_default\", \"another\")\n\n    connection.bucket(TEST_BUCKET).waitUntilReady(30.seconds)\n    connection.waitUntilIndexIsCreated(\n      \"CREATE PRIMARY INDEX ON `${connection.bucket(TEST_BUCKET).name}`.`_default`.`another`\",\n      30.seconds\n    )\n    connection.waitForKeySpaceAvailability(TEST_BUCKET, \"another\", 30.seconds)\n    logger.info(\"default migration is executed\")\n  }\n}\n\n// ============================================================================\n// Strategy interface\n// ============================================================================\n\nsealed interface CouchbaseTestStrategy {\n  val logger: Logger\n\n  suspend fun start()\n\n  suspend fun stop()\n\n  companion object {\n    fun select(): CouchbaseTestStrategy {\n      val useProvided = System.getenv(\"USE_PROVIDED\")?.toBoolean()\n        ?: System.getProperty(\"useProvided\")?.toBoolean()\n        ?: false\n\n      return if (useProvided) ProvidedCouchbaseStrategy() else ContainerCouchbaseStrategy()\n    }\n  }\n}\n\n// ============================================================================\n// Container-based strategy (default)\n// ============================================================================\n\nclass ContainerCouchbaseStrategy : CouchbaseTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun start() {\n    logger.info(\"Starting Couchbase tests with container mode\")\n\n    val options = CouchbaseSystemOptions(\n      defaultBucket = TEST_BUCKET,\n      configureExposedConfiguration = { _ -> listOf() },\n      containerOptions = CouchbaseContainerOptions(\n        useContainerFn = { ExtendedCouchbaseContainer(it) },\n        tag = \"7.6.1\"\n      ) {\n        withStartupAttempts(3)\n        withEnabledServices(CouchbaseService.KV, CouchbaseService.INDEX, CouchbaseService.QUERY)\n      }\n    ).migrations {\n      register<DefaultMigration>()\n    }\n\n    Stove {}\n      .with {\n        couchbase { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    logger.info(\"Couchbase container tests completed\")\n  }\n}\n\n// ============================================================================\n// Provided instance strategy\n// ============================================================================\n\nclass ProvidedCouchbaseStrategy : CouchbaseTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  private lateinit var externalContainer: CouchbaseContainer\n\n  override suspend fun start() {\n    logger.info(\"Starting Couchbase tests with provided mode\")\n\n    // Start an external container to simulate a provided instance\n    externalContainer = CouchbaseContainer(DockerImageName.parse(\"couchbase/server:7.6.1\"))\n      .withBucket(BucketDefinition(TEST_BUCKET))\n      .withEnabledServices(CouchbaseService.KV, CouchbaseService.INDEX, CouchbaseService.QUERY)\n      .apply { start() }\n\n    logger.info(\"External Couchbase container started at ${externalContainer.connectionString}\")\n\n    val options = CouchbaseSystemOptions\n      .provided(\n        connectionString = externalContainer.connectionString,\n        username = externalContainer.username,\n        password = externalContainer.password,\n        defaultBucket = TEST_BUCKET,\n        runMigrations = true,\n        cleanup = { cluster ->\n          logger.info(\"Running cleanup on provided instance\")\n          // Clean up test data if needed\n        },\n        configureExposedConfiguration = { _ -> listOf() }\n      ).migrations {\n        register<DefaultMigration>()\n      }\n\n    Stove {}\n      .with {\n        couchbase { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    externalContainer.stop()\n    logger.info(\"Couchbase provided tests completed\")\n  }\n}\n\n// ============================================================================\n// Kotest project config - selects strategy based on environment\n// ============================================================================\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val strategy = CouchbaseTestStrategy.select()\n\n  override suspend fun beforeProject() = strategy.start()\n\n  override suspend fun afterProject() = strategy.stop()\n}\n"
  },
  {
    "path": "lib/stove-couchbase/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.couchbase.StoveConfig\n"
  },
  {
    "path": "lib/stove-dashboard/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension\n\nval generatedDashboardSourcesDir = layout.buildDirectory.dir(\"generated/source/stoveVersion/kotlin\")\nval stoveCompatibilityVersionValue = providers\n  .fileContents(rootProject.layout.projectDirectory.file(\"gradle.properties\"))\n  .asText\n  .map { gradleProperties ->\n    gradleProperties\n      .lineSequence()\n      .first { it.startsWith(\"version=\") }\n      .substringAfter(\"version=\")\n      .trim()\n  }\n\nval generateDashboardVersionSource by tasks.registering(GenerateDashboardVersionSourceTask::class) {\n  description = \"Generates a source file containing the Stove version.\"\n  stoveCompatibilityVersion.set(stoveCompatibilityVersionValue)\n  outputDir.set(generatedDashboardSourcesDir)\n}\n\nextensions.configure<KotlinJvmProjectExtension> {\n  sourceSets.getByName(\"main\").kotlin.srcDir(generatedDashboardSourcesDir)\n}\n\ntasks.named(\"compileKotlin\") {\n  dependsOn(generateDashboardVersionSource)\n}\n\ntasks.named(\"sourcesJar\") {\n  dependsOn(generateDashboardVersionSource)\n}\n\ndependencies {\n  api(projects.lib.stove)\n  api(projects.lib.stoveDashboardApi)\n  implementation(libs.io.grpc.netty)\n  implementation(libs.kotlinx.core)\n\n  testImplementation(projects.lib.stoveTracing)\n}\n"
  },
  {
    "path": "lib/stove-dashboard/src/main/kotlin/com/trendyol/stove/dashboard/DashboardDsl.kt",
    "content": "package com.trendyol.stove.dashboard\n\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.WithDsl\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n/**\n * Registers the Dashboard system with Stove.\n *\n * Usage:\n * ```kotlin\n * Stove { }.with {\n *   dashboard { DashboardSystemOptions(appName = \"product-api\") }\n *   // ... other systems\n * }\n * ```\n */\nfun WithDsl.dashboard(\n  configure: @StoveDsl () -> DashboardSystemOptions\n): Stove {\n  this.stove.getOrRegister(DashboardSystem(this.stove, configure()))\n  return this.stove\n}\n"
  },
  {
    "path": "lib/stove-dashboard/src/main/kotlin/com/trendyol/stove/dashboard/DashboardEmitter.kt",
    "content": "@file:Suppress(\"TooGenericExceptionCaught\")\n\npackage com.trendyol.stove.dashboard\n\nimport com.trendyol.stove.dashboard.api.*\nimport com.trendyol.stove.dashboard.api.DashboardEventServiceGrpcKt.DashboardEventServiceCoroutineStub\nimport io.grpc.*\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport org.slf4j.LoggerFactory\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.atomic.*\nimport kotlin.time.Duration.Companion.milliseconds\n\n/**\n * Emits dashboard events to the CLI via gRPC.\n *\n * Events are buffered in a coroutine channel and drained by a background coroutine.\n * On connection failure, retries with auto-disable after [maxFailures] consecutive failures.\n *\n * Thread-safe: [tryEmit] can be called from any thread.\n */\nclass DashboardEmitter(\n  host: String,\n  port: Int,\n  private val maxFailures: Int = MAX_FAILURES\n) {\n  private val logger = LoggerFactory.getLogger(DashboardEmitter::class.java)\n  private val channel: ManagedChannel = ManagedChannelBuilder\n    .forAddress(host, port)\n    .usePlaintext()\n    .build()\n  private val stub = DashboardEventServiceCoroutineStub(channel)\n\n  // Test runs can emit thousands of spans/entries in a short burst.\n  // A bounded queue silently drops lifecycle events and leaves the CLI in a stale state.\n  private val eventQueue = Channel<DashboardEvent>(Channel.UNLIMITED)\n  private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n  private val disabled = AtomicBoolean(false)\n  private val consecutiveFailures = AtomicInteger(0)\n  private val drainJob: Job\n\n  init {\n    drainJob = scope.launch { drainLoop() }\n  }\n\n  /**\n   * Non-blocking emit. Drops the event only if the emitter is disabled or already closed.\n   */\n  fun tryEmit(event: DashboardEvent) {\n    if (disabled.get()) return\n    val result = eventQueue.trySend(event)\n    if (result.isFailure) {\n      if (!disabled.get()) {\n        logger.debug(\"Dropping dashboard event because emitter queue is closed\")\n      }\n    }\n  }\n\n  /**\n   * Graceful shutdown: drains remaining events (with timeout), then closes the gRPC channel.\n   */\n  fun close() {\n    eventQueue.close()\n    // Wait for the existing drainLoop to finish consuming buffered events.\n    // Closing the channel causes the `for (event in eventQueue)` iterator to terminate\n    // once all buffered events are consumed, so drainJob completes naturally.\n    runBlocking { withTimeoutOrNull(DRAIN_TIMEOUT_MS.milliseconds) { drainJob.join() } }\n    scope.cancel()\n    channel.shutdown()\n    try {\n      channel.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n    } catch (_: InterruptedException) {\n      Thread.currentThread().interrupt()\n    }\n    if (!channel.isTerminated) {\n      channel.shutdownNow()\n    }\n  }\n\n  private suspend fun drainLoop() {\n    for (event in eventQueue) {\n      if (!scope.isActive || disabled.get()) break\n      sendSafe(event)\n    }\n  }\n\n  private suspend fun sendSafe(event: DashboardEvent): EventAck? =\n    try {\n      val ack = stub.sendEvent(event)\n      consecutiveFailures.set(0)\n      ack\n    } catch (e: StatusException) {\n      handleFailure(e, isGrpc = true)\n      null\n    } catch (e: Exception) {\n      handleFailure(e, isGrpc = false)\n      null\n    }\n\n  private fun handleFailure(e: Exception, isGrpc: Boolean) {\n    val count = consecutiveFailures.incrementAndGet()\n    if (count == 1) {\n      if (isGrpc) {\n        logger.warn(\"Dashboard CLI gRPC error: ${e.message}. Events will be dropped after $maxFailures consecutive failures.\")\n      } else {\n        logger.error(\"Unexpected dashboard emitter error: ${e.message}\", e)\n      }\n    }\n    if (count >= maxFailures) {\n      disabled.set(true)\n      logger.info(\"Dashboard emitter disabled after $count consecutive failures. Tests will continue normally.\")\n    }\n  }\n\n  companion object {\n    private const val MAX_FAILURES = 5\n    private const val DRAIN_TIMEOUT_MS = 30000L\n    private const val SHUTDOWN_TIMEOUT_SECONDS = 5L\n  }\n}\n"
  },
  {
    "path": "lib/stove-dashboard/src/main/kotlin/com/trendyol/stove/dashboard/DashboardOptions.kt",
    "content": "package com.trendyol.stove.dashboard\n\nimport com.trendyol.stove.system.abstractions.SystemOptions\n\n/**\n * Configuration for the Dashboard system.\n *\n * @param appName Application name for grouping runs (e.g., \"product-api\").\n *   Required — identifies which application this test suite targets.\n * @param cliHost Hostname where the stove CLI is running.\n * @param cliPort gRPC port where the stove CLI is listening.\n */\ndata class DashboardSystemOptions(\n  val appName: String,\n  val cliHost: String = \"localhost\",\n  val cliPort: Int = 4041\n) : SystemOptions\n"
  },
  {
    "path": "lib/stove-dashboard/src/main/kotlin/com/trendyol/stove/dashboard/DashboardSystem.kt",
    "content": "package com.trendyol.stove.dashboard\n\nimport arrow.core.getOrElse\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.google.protobuf.Timestamp\nimport com.trendyol.stove.dashboard.api.DashboardEvent\nimport com.trendyol.stove.dashboard.api.EntryRecordedEvent\nimport com.trendyol.stove.dashboard.api.RunEndedEvent\nimport com.trendyol.stove.dashboard.api.RunStartedEvent\nimport com.trendyol.stove.dashboard.api.SpanRecordedEvent\nimport com.trendyol.stove.dashboard.api.TestEndedEvent\nimport com.trendyol.stove.dashboard.api.TestStartedEvent\nimport com.trendyol.stove.reporting.ReportEntry\nimport com.trendyol.stove.reporting.ReportEventListener\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.reporting.SpanEventListener\nimport com.trendyol.stove.reporting.SpanListenerRegistry\nimport com.trendyol.stove.reporting.StoveTestContext\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.PluggedSystem\nimport com.trendyol.stove.system.abstractions.RunAware\nimport com.trendyol.stove.tracing.SpanInfo\nimport java.time.Duration\nimport java.time.Instant\nimport java.util.UUID\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.locks.ReentrantLock\nimport kotlin.concurrent.withLock\n\n/**\n * Dashboard system that streams test events to the stove CLI via gRPC.\n *\n * Add to your Stove config:\n * ```kotlin\n * Stove { }.with {\n *   dashboard { DashboardSystemOptions(appName = \"my-api\") }\n * }\n * ```\n */\nclass DashboardSystem(\n  override val stove: Stove,\n  private val options: DashboardSystemOptions\n) : PluggedSystem,\n  RunAware,\n  ReportEventListener,\n  SpanEventListener {\n\n  private val logger = org.slf4j.LoggerFactory.getLogger(DashboardSystem::class.java)\n  private val jsonMapper = ObjectMapper()\n  private val runId = UUID.randomUUID().toString()\n  private lateinit var emitter: DashboardEmitter\n  private var startTime: Instant = Instant.now()\n  private var totalTests = 0\n  private var passedTests = 0\n  private var failedTests = 0\n  private val lifecycleLock = ReentrantLock()\n  private val testStartTimes = ConcurrentHashMap<String, Instant>()\n  private val testFailures = ConcurrentHashMap<String, String>()\n\n  override suspend fun run() {\n    emitter = DashboardEmitter(options.cliHost, options.cliPort)\n    stove.addReportListener(this)\n    registerSpanListener()\n    startTime = Instant.now()\n    emitter.tryEmit(\n      dashboardEvent {\n        runStarted = RunStartedEvent.newBuilder()\n          .setTimestamp(now())\n          .setAppName(options.appName)\n          .addAllSystems(stove.systemsOf<Reports>().map { it.reportSystemName })\n          .apply {\n            StoveCompatibilityVersion.VALUE\n              .takeIf(String::isNotBlank)\n              ?.let(::setStoveVersion)\n          }\n          .build()\n      }\n    )\n  }\n\n  override suspend fun stop() {\n    close()\n  }\n\n  override fun onTestStarted(ctx: StoveTestContext) {\n    totalTests++\n    testStartTimes[ctx.testId] = Instant.now()\n    emitter.tryEmit(\n      dashboardEvent {\n        testStarted = TestStartedEvent.newBuilder()\n          .setTestId(ctx.testId)\n          .setTestName(ctx.testName)\n          .setSpecName(ctx.specName ?: \"\")\n          .setTimestamp(now())\n          .addAllTestPath(ctx.testPath)\n          .build()\n      }\n    )\n  }\n\n  override fun onTestFailed(testId: String, error: String) {\n    testFailures[testId] = error\n  }\n\n  override fun onTestEnded(testId: String) {\n    lifecycleLock.withLock {\n      finishTestIfOpen(testId)\n    }\n  }\n\n  override fun onEntryRecorded(entry: ReportEntry) {\n    emitter.tryEmit(\n      dashboardEvent {\n        entryRecorded = EntryRecordedEvent.newBuilder()\n          .setTestId(entry.testId)\n          .setTimestamp(now())\n          .setSystem(entry.system)\n          .setAction(entry.action)\n          .setResult(entry.result.name)\n          .setInput(entry.input.getOrElse { \"\" }.toString())\n          .setOutput(entry.output.getOrElse { \"\" }.toString())\n          .putAllMetadata(entry.metadata.mapValues { it.value.toString() })\n          .setExpected(entry.expected.getOrElse { \"\" }.toString())\n          .setActual(entry.actual.getOrElse { \"\" }.toString())\n          .setError(entry.error.getOrElse { \"\" })\n          .setTraceId(entry.traceId.getOrElse { \"\" })\n          .build()\n      }\n    )\n    if (entry.isFailed) {\n      testFailures.putIfAbsent(entry.testId, entry.error.getOrElse { \"Assertion failed\" })\n    }\n  }\n\n  override fun onSpanRecorded(span: SpanInfo) {\n    emitter.tryEmit(\n      dashboardEvent {\n        spanRecorded = SpanRecordedEvent.newBuilder()\n          .setTraceId(span.traceId)\n          .setSpanId(span.spanId)\n          .setParentSpanId(span.parentSpanId ?: \"\")\n          .setOperationName(span.operationName)\n          .setServiceName(span.serviceName)\n          .setStartTimeNanos(span.startTimeNanos)\n          .setEndTimeNanos(span.endTimeNanos)\n          .setStatus(span.status.name)\n          .putAllAttributes(span.attributes)\n          .apply {\n            span.exception?.let { ex ->\n              exception = com.trendyol.stove.dashboard.api.ExceptionInfo.newBuilder()\n                .setType(ex.type)\n                .setMessage(ex.message)\n                .addAllStackTrace(ex.stackTrace)\n                .build()\n            }\n          }\n          .build()\n      }\n    )\n  }\n\n  override fun close() {\n    lifecycleLock.withLock {\n      if (!::emitter.isInitialized) return\n      finalizeOpenTests()\n      val duration = Duration.between(startTime, Instant.now()).toMillis()\n      emitter.tryEmit(\n        dashboardEvent {\n          runEnded = RunEndedEvent.newBuilder()\n            .setTimestamp(now())\n            .setTotalTests(totalTests)\n            .setPassed(passedTests)\n            .setFailed(failedTests)\n            .setDurationMs(duration)\n            .build()\n        }\n      )\n      stove.removeReportListener(this)\n      emitter.close()\n    }\n  }\n\n  private fun finalizeOpenTests() {\n    val stillRunning = testStartTimes.keys.toList()\n    stillRunning.forEach { testId ->\n      logger.debug(\"Finalizing still-running test {} during dashboard shutdown\", testId)\n      finishTestIfOpen(testId)\n    }\n  }\n\n  private fun finishTestIfOpen(testId: String) {\n    val startedAt = testStartTimes.remove(testId) ?: run {\n      logger.debug(\"Ignoring duplicate or late test end for {}\", testId)\n      return\n    }\n\n    emitSnapshots(testId)\n    val durationMs = Duration.between(startedAt, Instant.now()).toMillis()\n    val failure = testFailures.remove(testId)\n    val status = if (failure != null) \"FAILED\" else \"PASSED\"\n    emitter.tryEmit(\n      dashboardEvent {\n        testEnded = TestEndedEvent.newBuilder()\n          .setTestId(testId)\n          .setStatus(status)\n          .setDurationMs(durationMs)\n          .setError(failure ?: \"\")\n          .setTimestamp(now())\n          .build()\n      }\n    )\n    if (failure != null) {\n      failedTests++\n    } else {\n      passedTests++\n    }\n  }\n\n  private fun emitSnapshots(testId: String) {\n    stove.systemsOf<Reports>()\n      .forEach { system ->\n        runCatching { system.snapshot() }\n          .onFailure { e ->\n            logger.warn(\"Failed to collect snapshot from ${system.reportSystemName}: ${e.message}\")\n          }\n          .onSuccess { snap ->\n            val stateJson = runCatching { jsonMapper.writeValueAsString(snap.state) }\n              .getOrDefault(\"{}\")\n            emitter.tryEmit(\n              dashboardEvent {\n                snapshot = com.trendyol.stove.dashboard.api.SnapshotEvent.newBuilder()\n                  .setTestId(testId)\n                  .setSystem(snap.system)\n                  .setStateJson(stateJson)\n                  .setSummary(snap.summary)\n                  .build()\n              }\n            )\n          }\n      }\n  }\n\n  private fun registerSpanListener() {\n    stove.systemsOf<SpanListenerRegistry>()\n      .firstOrNull()\n      ?.addSpanListener(this)\n  }\n\n  private fun dashboardEvent(block: DashboardEvent.Builder.() -> Unit): DashboardEvent =\n    DashboardEvent.newBuilder()\n      .setRunId(runId)\n      .apply(block)\n      .build()\n\n  private fun now(): Timestamp {\n    val instant = Instant.now()\n    return Timestamp.newBuilder()\n      .setSeconds(instant.epochSecond)\n      .setNanos(instant.nano)\n      .build()\n  }\n}\n"
  },
  {
    "path": "lib/stove-dashboard/src/test/kotlin/com/trendyol/stove/dashboard/DashboardEmitterTest.kt",
    "content": "package com.trendyol.stove.dashboard\n\nimport com.trendyol.stove.dashboard.api.*\nimport com.trendyol.stove.dashboard.api.DashboardEventServiceGrpcKt.DashboardEventServiceCoroutineImplBase\nimport io.grpc.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport java.util.concurrent.CopyOnWriteArrayList\nimport java.util.concurrent.CountDownLatch\nimport java.util.concurrent.TimeUnit\n\nclass DashboardEmitterTest :\n  FunSpec({\n\n    test(\"emits events to a running gRPC server\") {\n      val received = CopyOnWriteArrayList<DashboardEvent>()\n      val server = startMockServer(received, port = 0)\n      val port = server.port\n\n      try {\n        val emitter = DashboardEmitter(\"localhost\", port)\n        val event = DashboardEvent.newBuilder()\n          .setRunId(\"run-1\")\n          .setRunStarted(\n            RunStartedEvent.newBuilder()\n              .setAppName(\"test-app\")\n              .build()\n          )\n          .build()\n\n        emitter.tryEmit(event)\n        emitter.tryEmit(event)\n\n        // Wait for async drain\n        delay(500)\n        emitter.close()\n\n        received.size shouldBe 2\n        received[0].runId shouldBe \"run-1\"\n      } finally {\n        server.shutdownNow()\n      }\n    }\n\n    test(\"auto-disables after consecutive failures without throwing\") {\n      // Connect to a port that is not listening\n      val emitter = DashboardEmitter(\"localhost\", 1, maxFailures = 2)\n\n      // These should not throw\n      repeat(10) {\n        emitter.tryEmit(\n          DashboardEvent.newBuilder()\n            .setRunId(\"run-1\")\n            .setRunStarted(RunStartedEvent.newBuilder().setAppName(\"test\").build())\n            .build()\n        )\n      }\n\n      // Wait for the drain loop to process and fail\n      delay(2000)\n      emitter.close()\n\n      // If we get here without exception, the test passes\n    }\n\n    test(\"does not drop burst events while receiver is temporarily blocked\") {\n      val received = CopyOnWriteArrayList<DashboardEvent>()\n      val firstRequestStarted = CountDownLatch(1)\n      val releaseFirstRequest = CountDownLatch(1)\n      val server = startMockServer(received, port = 0) {\n        if (firstRequestStarted.count > 0) {\n          firstRequestStarted.countDown()\n          releaseFirstRequest.await(5, TimeUnit.SECONDS)\n        }\n      }\n      val port = server.port\n\n      try {\n        val emitter = DashboardEmitter(\"localhost\", port)\n        val totalEvents = 700\n\n        repeat(totalEvents) { index ->\n          emitter.tryEmit(runStartedEvent(index))\n        }\n\n        firstRequestStarted.await(2, TimeUnit.SECONDS) shouldBe true\n        releaseFirstRequest.countDown()\n\n        delay(500)\n        emitter.close()\n\n        received.size shouldBe totalEvents\n      } finally {\n        server.shutdownNow()\n      }\n    }\n\n    test(\"close drains queued events before shutting down\") {\n      val received = CopyOnWriteArrayList<DashboardEvent>()\n      val server = startMockServer(received, port = 0) {\n        delay(12)\n      }\n      val port = server.port\n\n      try {\n        val emitter = DashboardEmitter(\"localhost\", port)\n        val totalEvents = 350\n\n        repeat(totalEvents) { index ->\n          emitter.tryEmit(runStartedEvent(index))\n        }\n\n        emitter.close()\n\n        received.size shouldBe totalEvents\n      } finally {\n        server.shutdownNow()\n      }\n    }\n  })\n\nprivate fun startMockServer(\n  received: MutableList<DashboardEvent>,\n  port: Int,\n  beforeAck: suspend (DashboardEvent) -> Unit = {}\n): Server {\n  val service = object : DashboardEventServiceCoroutineImplBase() {\n    override suspend fun sendEvent(request: DashboardEvent): EventAck {\n      beforeAck(request)\n      received.add(request)\n      return EventAck.newBuilder().setAccepted(true).build()\n    }\n\n    override suspend fun streamEvents(requests: Flow<DashboardEvent>): EventAck {\n      requests.collect { received.add(it) }\n      return EventAck.newBuilder().setAccepted(true).build()\n    }\n  }\n\n  return ServerBuilder.forPort(port)\n    .addService(service)\n    .build()\n    .start()\n}\n\nprivate fun runStartedEvent(index: Int): DashboardEvent =\n  DashboardEvent.newBuilder()\n    .setRunId(\"run-$index\")\n    .setRunStarted(\n      RunStartedEvent.newBuilder()\n        .setAppName(\"test-app\")\n        .build()\n    )\n    .build()\n"
  },
  {
    "path": "lib/stove-dashboard/src/test/kotlin/com/trendyol/stove/dashboard/DashboardSystemTest.kt",
    "content": "package com.trendyol.stove.dashboard\n\nimport com.trendyol.stove.dashboard.api.*\nimport com.trendyol.stove.dashboard.api.DashboardEventServiceGrpcKt.DashboardEventServiceCoroutineImplBase\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.PluggedSystem\nimport io.grpc.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport java.util.concurrent.CopyOnWriteArrayList\nimport java.util.concurrent.CountDownLatch\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.atomic.AtomicInteger\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass DashboardSystemTest : FunSpec({\n  test(\"lifecycle: registers as listener, emits events, unregisters on stop\") {\n    val received = CopyOnWriteArrayList<DashboardEvent>()\n    val server = startMockServer(received, port = 0)\n    val port = server.port\n\n    try {\n      val stove = Stove()\n      val options = DashboardSystemOptions(appName = \"test-api\", cliPort = port)\n      val system = DashboardSystem(stove, options)\n\n      // Start the system — should emit RunStartedEvent\n      system.run()\n      delay(200.milliseconds)\n\n      // Simulate test lifecycle via reporter\n      val ctx = StoveTestContext(\"test-1\", \"my test\", \"MySpec\")\n      stove.startTest(ctx)\n      stove.recordReport(ReportEntry.success(\"HTTP\", \"test-1\", \"GET /api\"))\n      stove.endTest()\n\n      // Wait for async events to be processed before stopping\n      delay(1000.milliseconds)\n\n      // Stop the system — should emit RunEndedEvent\n      system.stop()\n\n      // Wait for close to drain\n      delay(1000.milliseconds)\n\n      // Verify we received the key lifecycle events\n      val types = received.map {\n        when {\n          it.hasRunStarted() -> \"RunStarted\"\n          it.hasTestStarted() -> \"TestStarted\"\n          it.hasEntryRecorded() -> \"EntryRecorded\"\n          it.hasTestEnded() -> \"TestEnded\"\n          it.hasRunEnded() -> \"RunEnded\"\n          else -> \"Unknown\"\n        }\n      }\n      types.contains(\"RunStarted\") shouldBe true\n      received.first { it.hasRunStarted() }.runStarted.appName shouldBe \"test-api\"\n      received.first { it.hasRunStarted() }.runStarted.stoveVersion shouldBe StoveCompatibilityVersion.VALUE\n      types.contains(\"TestStarted\") shouldBe true\n      types.contains(\"EntryRecorded\") shouldBe true\n    } finally {\n      server.shutdownNow()\n    }\n  }\n\n  test(\"stop finalizes tests still marked running\") {\n    val received = CopyOnWriteArrayList<DashboardEvent>()\n    val server = startMockServer(received, port = 0)\n    val port = server.port\n\n    try {\n      val stove = Stove()\n      val options = DashboardSystemOptions(appName = \"test-api\", cliPort = port)\n      val system = DashboardSystem(stove, options)\n\n      system.run()\n      delay(200.milliseconds)\n\n      stove.startTest(StoveTestContext(\"test-still-running\", \"still running\", \"MySpec\"))\n      stove.recordReport(ReportEntry.success(\"HTTP\", \"test-still-running\", \"GET /health\"))\n\n      delay(300.milliseconds)\n      system.stop()\n      delay(1000.milliseconds)\n\n      received.any { it.hasTestEnded() && it.testEnded.testId == \"test-still-running\" } shouldBe true\n      received.first { it.hasRunEnded() }.runEnded.totalTests shouldBe 1\n    } finally {\n      server.shutdownNow()\n    }\n  }\n\n  test(\"stop does not re-finalize a test whose end callback is already in progress\") {\n    val received = CopyOnWriteArrayList<DashboardEvent>()\n    val server = startMockServer(received, port = 0)\n    val port = server.port\n\n    try {\n      val stove = Stove()\n      val snapshotSystem = BlockingSnapshotSystem(stove)\n      stove.getOrRegister(snapshotSystem)\n\n      val options = DashboardSystemOptions(appName = \"test-api\", cliPort = port)\n      val system = DashboardSystem(stove, options)\n\n      system.run()\n      delay(200.milliseconds)\n\n      stove.startTest(StoveTestContext(\"test-race\", \"race\", \"MySpec\"))\n\n      val endJob = async(Dispatchers.Default) {\n        system.onTestEnded(\"test-race\")\n      }\n\n      snapshotSystem.awaitFirstSnapshotCall() shouldBe true\n\n      val stopJob = async(Dispatchers.Default) {\n        system.stop()\n      }\n\n      try {\n        snapshotSystem.awaitSecondSnapshotCall() shouldBe false\n      } finally {\n        snapshotSystem.releaseSnapshots()\n        endJob.await()\n        stopJob.await()\n      }\n\n      delay(1000.milliseconds)\n\n      received.count { it.hasTestEnded() && it.testEnded.testId == \"test-race\" } shouldBe 1\n      received.first { it.hasRunEnded() }.runEnded.totalTests shouldBe 1\n      received.first { it.hasRunEnded() }.runEnded.passed shouldBe 1\n      received.first { it.hasRunEnded() }.runEnded.failed shouldBe 0\n    } finally {\n      server.shutdownNow()\n    }\n  }\n})\n\nprivate fun startMockServer(received: MutableList<DashboardEvent>, port: Int): Server {\n  val service = object : DashboardEventServiceCoroutineImplBase() {\n    override suspend fun sendEvent(request: DashboardEvent): EventAck {\n      received.add(request)\n      return EventAck.newBuilder().setAccepted(true).build()\n    }\n\n    override suspend fun streamEvents(requests: Flow<DashboardEvent>): EventAck {\n      requests.collect { received.add(it) }\n      return EventAck.newBuilder().setAccepted(true).build()\n    }\n  }\n\n  return ServerBuilder.forPort(port)\n    .addService(service)\n    .build()\n    .start()\n}\n\nprivate class BlockingSnapshotSystem(\n  override val stove: Stove\n) : PluggedSystem,\n  Reports {\n  private val snapshotCalls = AtomicInteger(0)\n  private val firstSnapshotCall = CountDownLatch(1)\n  private val secondSnapshotCall = CountDownLatch(1)\n  private val releaseSnapshots = CountDownLatch(1)\n\n  override fun snapshot(): SystemSnapshot {\n    when (snapshotCalls.incrementAndGet()) {\n      1 -> {\n        firstSnapshotCall.countDown()\n        releaseSnapshots.await(5, TimeUnit.SECONDS)\n      }\n\n      2 -> secondSnapshotCall.countDown()\n    }\n\n    return SystemSnapshot(\n      system = \"BlockingSnapshot\",\n      state = emptyMap<String, Any>(),\n      summary = \"blocking snapshot\"\n    )\n  }\n\n  fun awaitFirstSnapshotCall(): Boolean = firstSnapshotCall.await(2, TimeUnit.SECONDS)\n\n  fun awaitSecondSnapshotCall(): Boolean = secondSnapshotCall.await(1, TimeUnit.SECONDS)\n\n  fun releaseSnapshots() {\n    releaseSnapshots.countDown()\n  }\n\n  override fun close() = Unit\n}\n"
  },
  {
    "path": "lib/stove-dashboard-api/build.gradle.kts",
    "content": "plugins {\n  alias(libs.plugins.protobuf)\n}\n\ndependencies {\n  api(libs.io.grpc)\n  api(libs.io.grpc.stub)\n  api(libs.io.grpc.protobuf)\n  api(libs.io.grpc.kotlin)\n  api(libs.google.protobuf.kotlin)\n  api(libs.kotlinx.core)\n}\n\ntasks.withType<Javadoc> {\n  // All Java sources in this module are protobuf-generated; suppress missing-comment warnings\n  (options as StandardJavadocDocletOptions).addBooleanOption(\"Xdoclint:none\", true)\n}\n\nprotobuf {\n  protoc {\n    artifact = libs.protoc.get().toString()\n  }\n\n  plugins {\n    create(\"grpc\").apply {\n      artifact = libs.grpc.protoc.gen.java.get().toString()\n    }\n    create(\"grpckt\").apply {\n      artifact = \"${libs.grpc.protoc.gen.kotlin.get()}:jdk8@jar\"\n    }\n  }\n\n  generateProtoTasks {\n    all().forEach { task ->\n      task.plugins {\n        create(\"grpc\")\n        create(\"grpckt\")\n      }\n      task.builtins {\n        create(\"kotlin\")\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove-dashboard-api/src/main/proto/stove/dashboard/v1/dashboard_events.proto",
    "content": "syntax = \"proto3\";\npackage stove.dashboard.v1;\n\noption java_package = \"com.trendyol.stove.dashboard.api\";\noption java_multiple_files = true;\n\nimport \"google/protobuf/timestamp.proto\";\n\nmessage DashboardEvent {\n  string run_id = 1;\n  oneof event {\n    RunStartedEvent run_started = 10;\n    RunEndedEvent run_ended = 11;\n    TestStartedEvent test_started = 12;\n    TestEndedEvent test_ended = 13;\n    EntryRecordedEvent entry_recorded = 14;\n    SpanRecordedEvent span_recorded = 15;\n    SnapshotEvent snapshot = 16;\n  }\n}\n\nmessage RunStartedEvent {\n  google.protobuf.Timestamp timestamp = 1;\n  string app_name = 2;\n  repeated string systems = 3;\n  string stove_version = 4;\n}\n\nmessage RunEndedEvent {\n  google.protobuf.Timestamp timestamp = 1;\n  int32 total_tests = 2;\n  int32 passed = 3;\n  int32 failed = 4;\n  int64 duration_ms = 5;\n}\n\nmessage TestStartedEvent {\n  string test_id = 1;\n  string test_name = 2;\n  string spec_name = 3;\n  google.protobuf.Timestamp timestamp = 4;\n  repeated string test_path = 5;\n}\n\nmessage TestEndedEvent {\n  string test_id = 1;\n  string status = 2;\n  int64 duration_ms = 3;\n  string error = 4;\n  google.protobuf.Timestamp timestamp = 5;\n}\n\nmessage EntryRecordedEvent {\n  string test_id = 1;\n  google.protobuf.Timestamp timestamp = 2;\n  string system = 3;\n  string action = 4;\n  string result = 5;\n  string input = 6;\n  string output = 7;\n  map<string, string> metadata = 8;\n  string expected = 9;\n  string actual = 10;\n  string error = 11;\n  string trace_id = 12;\n}\n\nmessage SpanRecordedEvent {\n  string trace_id = 1;\n  string span_id = 2;\n  string parent_span_id = 3;\n  string operation_name = 4;\n  string service_name = 5;\n  int64 start_time_nanos = 6;\n  int64 end_time_nanos = 7;\n  string status = 8;\n  map<string, string> attributes = 9;\n  ExceptionInfo exception = 10;\n}\n\nmessage ExceptionInfo {\n  string type = 1;\n  string message = 2;\n  repeated string stack_trace = 3;\n}\n\nmessage SnapshotEvent {\n  string test_id = 1;\n  string system = 2;\n  string state_json = 3;\n  string summary = 4;\n}\n\nmessage EventAck {\n  bool accepted = 1;\n}\n"
  },
  {
    "path": "lib/stove-dashboard-api/src/main/proto/stove/dashboard/v1/dashboard_service.proto",
    "content": "syntax = \"proto3\";\npackage stove.dashboard.v1;\n\noption java_package = \"com.trendyol.stove.dashboard.api\";\noption java_multiple_files = true;\n\nimport \"stove/dashboard/v1/dashboard_events.proto\";\n\nservice DashboardEventService {\n  rpc StreamEvents(stream DashboardEvent) returns (EventAck);\n  rpc SendEvent(DashboardEvent) returns (EventAck);\n}\n"
  },
  {
    "path": "lib/stove-elasticsearch/api/stove-elasticsearch.api",
    "content": "public final class com/trendyol/stove/elasticsearch/ElasticClientConfigurer {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lkotlin/jvm/functions/Function1;Larrow/core/Option;)V\n\tpublic synthetic fun <init> (Lkotlin/jvm/functions/Function1;Larrow/core/Option;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component2 ()Larrow/core/Option;\n\tpublic final fun copy (Lkotlin/jvm/functions/Function1;Larrow/core/Option;)Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;Lkotlin/jvm/functions/Function1;Larrow/core/Option;ILjava/lang/Object;)Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getHttpClientBuilder ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getRestClientOverrideFn ()Larrow/core/Option;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ElasticContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/elasticsearch/ElasticContainerOptions$Companion;\n\tpublic static final field DEFAULT_ELASTIC_PORT I\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Ljava/util/List;\n\tpublic final fun component6 ()Ljava/lang/String;\n\tpublic final fun component7 ()Z\n\tpublic final fun component8 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component9 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/elasticsearch/ElasticContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/elasticsearch/ElasticContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ZLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/elasticsearch/ElasticContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getDisableSecurity ()Z\n\tpublic final fun getExposedPorts ()Ljava/util/List;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic final fun getPassword ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ElasticContainerOptions$Companion {\n}\n\npublic abstract interface annotation class com/trendyol/stove/elasticsearch/ElasticDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/elasticsearch/ElasticSearchExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;ILjava/lang/String;Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()I\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;\n\tpublic final fun copy (Ljava/lang/String;ILjava/lang/String;Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;)Lcom/trendyol/stove/elasticsearch/ElasticSearchExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/elasticsearch/ElasticSearchExposedConfiguration;Ljava/lang/String;ILjava/lang/String;Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;ILjava/lang/Object;)Lcom/trendyol/stove/elasticsearch/ElasticSearchExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getCertificate ()Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;\n\tpublic final fun getHost ()Ljava/lang/String;\n\tpublic final fun getPassword ()Ljava/lang/String;\n\tpublic final fun getPort ()I\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ElasticsearchContext {\n\tpublic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/elasticsearch/ElasticsearchSystemOptions;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/elasticsearch/ElasticsearchSystemOptions;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic final fun component2 ()Lcom/trendyol/stove/elasticsearch/ElasticsearchSystemOptions;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/elasticsearch/ElasticsearchSystemOptions;Ljava/lang/String;)Lcom/trendyol/stove/elasticsearch/ElasticsearchContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/elasticsearch/ElasticsearchContext;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/elasticsearch/ElasticsearchSystemOptions;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/elasticsearch/ElasticsearchContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getKeyName ()Ljava/lang/String;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/elasticsearch/ElasticsearchSystemOptions;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate {\n\tpublic static final field Companion Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate$Companion;\n\tpublic fun <init> ([B)V\n\tpublic final fun component1 ()[B\n\tpublic final fun copy ([B)Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;[BILjava/lang/Object;)Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;\n\tpublic static final fun create ([B)Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBytes ()[B\n\tpublic final fun getSslContext ()Ljavax/net/ssl/SSLContext;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate$Companion {\n\tpublic final fun create ([B)Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ElasticsearchSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/AfterRunAware, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field Companion Lcom/trendyol/stove/elasticsearch/ElasticsearchSystem$Companion;\n\tpublic field esClient Lco/elastic/clients/elasticsearch/ElasticsearchClient;\n\tpublic fun afterRun (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun getEsClient ()Lco/elastic/clients/elasticsearch/ElasticsearchClient;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun pause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun save (Ljava/lang/String;Ljava/lang/Object;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun setEsClient (Lco/elastic/clients/elasticsearch/ElasticsearchClient;)V\n\tpublic final fun shouldDelete (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldNotExist (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ElasticsearchSystem$Companion {\n\tpublic final fun client (Lcom/trendyol/stove/elasticsearch/ElasticsearchSystem;)Lco/elastic/clients/elasticsearch/ElasticsearchClient;\n}\n\npublic class com/trendyol/stove/elasticsearch/ElasticsearchSystemOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/elasticsearch/ElasticsearchSystemOptions$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;Lcom/trendyol/stove/elasticsearch/ElasticContainerOptions;Lco/elastic/clients/json/JsonpMapper;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;Lcom/trendyol/stove/elasticsearch/ElasticContainerOptions;Lco/elastic/clients/json/JsonpMapper;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getClientConfigurer ()Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainer ()Lcom/trendyol/stove/elasticsearch/ElasticContainerOptions;\n\tpublic fun getJsonpMapper ()Lco/elastic/clients/json/JsonpMapper;\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/elasticsearch/ElasticsearchSystemOptions;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ElasticsearchSystemOptions$Companion {\n\tpublic final fun provided (Ljava/lang/String;ILjava/lang/String;Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;Lco/elastic/clients/json/JsonpMapper;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/elasticsearch/ProvidedElasticsearchSystemOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/elasticsearch/ElasticsearchSystemOptions$Companion;Ljava/lang/String;ILjava/lang/String;Lcom/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate;Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;Lco/elastic/clients/json/JsonpMapper;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/elasticsearch/ProvidedElasticsearchSystemOptions;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ExtensionsKt {\n\tpublic static final fun elasticsearch-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun elasticsearch-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun elasticsearch-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun elasticsearch-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/elasticsearch/ProvidedElasticsearchSystemOptions : com/trendyol/stove/elasticsearch/ElasticsearchSystemOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/elasticsearch/ElasticSearchExposedConfiguration;Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;Lco/elastic/clients/json/JsonpMapper;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/elasticsearch/ElasticSearchExposedConfiguration;Lcom/trendyol/stove/elasticsearch/ElasticClientConfigurer;Lco/elastic/clients/json/JsonpMapper;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/elasticsearch/ElasticSearchExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/elasticsearch/ElasticSearchExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic class com/trendyol/stove/elasticsearch/StoveElasticSearchContainer : org/testcontainers/elasticsearch/ElasticsearchContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\n"
  },
  {
    "path": "lib/stove-elasticsearch/build.gradle.kts",
    "content": "plugins {}\n\nval elasticsearchTestTag =\n  providers\n    .systemProperty(\"elasticsearchTestTag\")\n    .orElse(providers.environmentVariable(\"ELASTICSEARCH_TEST_TAG\"))\n    .orElse(\"8.9.0\")\n\ndependencies {\n  api(projects.lib.stove)\n  api(libs.elastic)\n  api(libs.elastic.rest.client)\n  api(libs.testcontainers.elasticsearch)\n  implementation(libs.jackson.databind)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.slf4j.simple)\n}\n\nval testWithProvided = tasks.register<Test>(\"testWithProvided\") {\n  group = \"verification\"\n  description = \"Runs tests with an externally provided Elasticsearch instance\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useProvided\", \"true\")\n  val tag = elasticsearchTestTag.get()\n  systemProperty(\"elasticsearchTestTag\", tag)\n  doFirst {\n    println(\"Starting Elasticsearch tests with provided instance and tag=$tag...\")\n  }\n}\n\ntasks.withType<Test>().configureEach {\n  systemProperty(\"elasticsearchTestTag\", elasticsearchTestTag.get())\n}\n\ntasks.test.configure {\n  dependsOn(testWithProvided)\n}\n"
  },
  {
    "path": "lib/stove-elasticsearch/src/main/kotlin/com/trendyol/stove/elasticsearch/ElasticsearchExposedCertificate.kt",
    "content": "package com.trendyol.stove.elasticsearch\n\nimport com.fasterxml.jackson.annotation.*\nimport org.testcontainers.elasticsearch.ElasticsearchContainer\nimport java.io.ByteArrayInputStream\nimport java.security.KeyStore\nimport java.security.cert.CertificateFactory\nimport javax.net.ssl.*\n\ndata class ElasticsearchExposedCertificate(\n  val bytes: ByteArray\n) {\n  @get:JsonIgnore\n  @set:JsonIgnore\n  var sslContext: SSLContext = SSLContext.getDefault()\n    internal set\n\n  override fun equals(other: Any?): Boolean {\n    if (this === other) return true\n    if (javaClass != other?.javaClass) return false\n\n    other as ElasticsearchExposedCertificate\n\n    return bytes.contentEquals(other.bytes)\n  }\n\n  override fun hashCode(): Int = bytes.contentHashCode()\n\n  companion object {\n    @JsonCreator\n    @JvmStatic\n    fun create(\n      @JsonProperty bytes: ByteArray\n    ): ElasticsearchExposedCertificate = ElasticsearchExposedCertificate(bytes).apply {\n      sslContext = createSslContextFromCa(bytes)\n    }\n\n    /**\n     * An SSL context based on the self-signed CA, so that using this SSL Context allows to connect to the Elasticsearch service\n     * @return a customized SSL Context\n     * @see ElasticsearchContainer.createSslContextFromCa\n     */\n    @Suppress(\"TooGenericExceptionCaught\", \"TooGenericExceptionThrown\")\n    private fun createSslContextFromCa(bytes: ByteArray): SSLContext = try {\n      val factory = CertificateFactory.getInstance(\"X.509\")\n      val trustedCa = factory.generateCertificate(\n        ByteArrayInputStream(bytes)\n      )\n      val trustStore = KeyStore.getInstance(\"pkcs12\")\n      trustStore.load(null, null)\n      trustStore.setCertificateEntry(\"ca\", trustedCa)\n\n      val sslContext = SSLContext.getInstance(\"TLSv1.3\")\n      val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())\n      trustManagerFactory.init(trustStore)\n      sslContext.init(null, trustManagerFactory.trustManagers, null)\n      sslContext\n    } catch (e: Exception) {\n      throw RuntimeException(e)\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove-elasticsearch/src/main/kotlin/com/trendyol/stove/elasticsearch/ElasticsearchSystem.kt",
    "content": "package com.trendyol.stove.elasticsearch\n\nimport arrow.core.*\nimport co.elastic.clients.elasticsearch.ElasticsearchClient\nimport co.elastic.clients.elasticsearch._types.Refresh\nimport co.elastic.clients.elasticsearch._types.query_dsl.Query\nimport co.elastic.clients.elasticsearch.core.*\nimport co.elastic.clients.transport.rest_client.RestClientOptions\nimport co.elastic.clients.transport.rest_client.RestClientTransport\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport kotlinx.coroutines.runBlocking\nimport org.apache.http.HttpHost\nimport org.apache.http.auth.*\nimport org.apache.http.client.CredentialsProvider\nimport org.apache.http.impl.client.BasicCredentialsProvider\nimport org.apache.http.impl.nio.client.HttpAsyncClientBuilder\nimport org.elasticsearch.client.RequestOptions\nimport org.elasticsearch.client.RestClient\nimport org.slf4j.*\nimport javax.net.ssl.SSLContext\nimport kotlin.jvm.optionals.getOrElse\n\n/**\n * Elasticsearch search engine system for testing search operations.\n *\n * Provides a DSL for testing Elasticsearch operations:\n * - Document indexing and retrieval\n * - Search queries (JSON and Query builder)\n * - Index management\n * - Document deletion\n *\n * ## Indexing Documents\n *\n * ```kotlin\n * elasticsearch {\n *     // Save document with specific ID\n *     save(\"products\", \"product-123\", Product(id = \"123\", name = \"Widget\"))\n *\n *     // Save with refresh (immediately searchable)\n *     save(\"products\", \"product-123\", product, refresh = Refresh.True)\n * }\n * ```\n *\n * ## Retrieving Documents\n *\n * ```kotlin\n * elasticsearch {\n *     // Get by ID and assert\n *     shouldGet<Product>(\"products\", \"product-123\") { product ->\n *         product.name shouldBe \"Widget\"\n *         product.price shouldBeGreaterThan 0.0\n *     }\n * }\n * ```\n *\n * ## Search Queries\n *\n * ```kotlin\n * elasticsearch {\n *     // Query with JSON syntax\n *     shouldQuery<Product>(\n *         query = \"\"\"{ \"match\": { \"name\": \"widget\" } }\"\"\",\n *         index = \"products\"\n *     ) { products ->\n *         products.size shouldBeGreaterThan 0\n *     }\n *\n *     // Query with Elasticsearch Query builder\n *     shouldQuery<Product>(\n *         query = Query.of { q ->\n *             q.bool { b ->\n *                 b.must { m -> m.match { t -> t.field(\"category\").query(\"electronics\") } }\n *                 b.filter { f -> f.range { r -> r.field(\"price\").gte(JsonData.of(100)) } }\n *             }\n *         },\n *         index = \"products\"\n *     ) { products ->\n *         products.all { it.category == \"electronics\" } shouldBe true\n *     }\n *\n *     // Complex search with aggregations (using client directly)\n *     client { es ->\n *         val response = es.search(SearchRequest.of { s ->\n *             s.index(\"products\")\n *              .query(Query.of { q -> q.matchAll { } })\n *              .aggregations(\"by_category\", Aggregation.of { a ->\n *                  a.terms { t -> t.field(\"category.keyword\") }\n *              })\n *         }, Product::class.java)\n *\n *         response.aggregations()[\"by_category\"]?.sterms()?.buckets()?.array()?.size shouldBeGreaterThan 0\n *     }\n * }\n * ```\n *\n * ## Deleting Documents\n *\n * ```kotlin\n * elasticsearch {\n *     shouldDelete(\"products\", \"product-123\")\n * }\n * ```\n *\n * ## Test Workflow Example\n *\n * ```kotlin\n * test(\"should index product and make it searchable\") {\n *     stove {\n *         val productId = UUID.randomUUID().toString()\n *\n *         // Create product via API\n *         http {\n *             postAndExpectBodilessResponse(\n *                 uri = \"/products\",\n *                 body = CreateProductRequest(id = productId, name = \"Test Widget\").some()\n *             ) { response ->\n *                 response.status shouldBe 201\n *             }\n *         }\n *\n *         // Verify in Elasticsearch (with eventual consistency wait)\n *         elasticsearch {\n *             eventually(10.seconds) {\n *                 shouldGet<Product>(\"products\", productId) { product ->\n *                     product.name shouldBe \"Test Widget\"\n *                 }\n *             }\n *         }\n *     }\n * }\n * ```\n *\n * ## Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         elasticsearch {\n *             ElasticsearchSystemOptions(\n *                 configureExposedConfiguration = { cfg ->\n *                     listOf(\n *                         \"elasticsearch.host=${cfg.host}\",\n *                         \"elasticsearch.port=${cfg.port}\"\n *                     )\n *                 }\n *             ).migrations {\n *                 register<CreateProductIndexMigration>()\n *             }\n *         }\n *     }\n * ```\n *\n * @property stove The parent test system.\n * @see ElasticsearchSystemOptions\n * @see ElasticSearchExposedConfiguration\n */\n@ElasticDsl\nclass ElasticsearchSystem internal constructor(\n  override val stove: Stove,\n  private val context: ElasticsearchContext\n) : PluggedSystem,\n  RunAware,\n  AfterRunAware,\n  ExposesConfiguration,\n  Reports {\n  @PublishedApi\n  internal lateinit var esClient: ElasticsearchClient\n\n  override val reportSystemName: String = \"Elasticsearch\" + (context.keyName?.let { \" [$it]\" } ?: \"\")\n\n  private lateinit var exposedConfiguration: ElasticSearchExposedConfiguration\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val state: StateStorage<ElasticSearchExposedConfiguration> =\n    stove.createStateStorage<ElasticSearchExposedConfiguration, ElasticsearchSystem>(context.keyName)\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n  }\n\n  override suspend fun afterRun() {\n    esClient = createEsClient(exposedConfiguration)\n    runMigrationsIfNeeded()\n  }\n\n  override suspend fun stop(): Unit = whenContainer { it.stop() }\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      context.options.cleanup(esClient)\n      esClient._transport().close()\n      executeWithReuseCheck { stop() }\n    }.recover { logger.warn(\"got an error while stopping elasticsearch: ${it.message}\") }\n  }\n\n  override fun configuration(): List<String> = context.options.configureExposedConfiguration(exposedConfiguration)\n\n  suspend inline fun <reified T : Any> shouldQuery(\n    query: String,\n    index: String,\n    crossinline assertion: (List<T>) -> Unit\n  ): ElasticsearchSystem {\n    require(index.isNotBlank()) { \"Index cannot be blank\" }\n    require(query.isNotBlank()) { \"Query cannot be blank\" }\n\n    report(\n      action = \"Search '$index'\",\n      input = arrow.core.Some(mapOf(\"index\" to index, \"query\" to query))\n    ) {\n      val results = esClient\n        .search(\n          SearchRequest.of { req -> req.index(index).query { q -> q.withJson(query.reader()) } },\n          T::class.java\n        ).hits()\n        .hits()\n        .mapNotNull { it.source() }\n      assertion(results)\n      results\n    }\n    return this\n  }\n\n  suspend inline fun <reified T : Any> shouldQuery(\n    query: Query,\n    crossinline assertion: (List<T>) -> Unit\n  ): ElasticsearchSystem {\n    report(action = \"Search with Query DSL\") {\n      val results = esClient\n        .search(\n          SearchRequest.of { q -> q.query(query) },\n          T::class.java\n        ).hits()\n        .hits()\n        .mapNotNull { it.source() }\n      assertion(results)\n      results\n    }\n    return this\n  }\n\n  suspend inline fun <reified T : Any> shouldGet(\n    index: String,\n    key: String,\n    crossinline assertion: (T) -> Unit\n  ): ElasticsearchSystem {\n    require(index.isNotBlank()) { \"Index cannot be blank\" }\n    require(key.isNotBlank()) { \"Key cannot be blank\" }\n\n    report(\n      action = \"Get document\",\n      input = arrow.core.Some(mapOf(\"index\" to index, \"id\" to key))\n    ) {\n      val document = esClient\n        .get({ req -> req.index(index).id(key).refresh(true) }, T::class.java)\n        .source()\n        .toOption()\n      document.map(assertion).getOrElse { throw AssertionError(\"Resource with key ($key) is not found\") }\n      document\n    }\n    return this\n  }\n\n  suspend fun shouldNotExist(\n    key: String,\n    index: String\n  ): ElasticsearchSystem {\n    require(index.isNotBlank()) { \"Index cannot be blank\" }\n    require(key.isNotBlank()) { \"Key cannot be blank\" }\n\n    report(\n      action = \"Document should not exist\",\n      input = arrow.core.Some(mapOf(\"index\" to index, \"id\" to key)),\n      expected = arrow.core.Some(\"Document not found\")\n    ) {\n      val exists = esClient.exists { req -> req.index(index).id(key) }.value()\n      if (exists) throw AssertionError(\"The document with the given id($key) was not expected, but found!\")\n    }\n    return this\n  }\n\n  suspend fun shouldDelete(\n    key: String,\n    index: String\n  ): ElasticsearchSystem {\n    require(index.isNotBlank()) { \"Index cannot be blank\" }\n    require(key.isNotBlank()) { \"Key cannot be blank\" }\n\n    report(\n      action = \"Delete document\",\n      metadata = mapOf(\"index\" to index, \"id\" to key)\n    ) {\n      esClient.delete(DeleteRequest.of { req -> req.index(index).id(key).refresh(Refresh.WaitFor) })\n    }\n    return this\n  }\n\n  suspend fun <T : Any> save(\n    id: String,\n    instance: T,\n    index: String\n  ): ElasticsearchSystem {\n    require(index.isNotBlank()) { \"Index cannot be blank\" }\n    require(id.isNotBlank()) { \"Id cannot be blank\" }\n\n    report(\n      action = \"Index document\",\n      input = arrow.core.Some(instance),\n      metadata = mapOf(\"index\" to index, \"id\" to id)\n    ) {\n      esClient.index { req ->\n        req\n          .index(index)\n          .id(id)\n          .document(instance)\n          .refresh(Refresh.WaitFor)\n      }\n    }\n    return this\n  }\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return ElasticsearchSystem\n   */\n  @Suppress(\"unused\")\n  suspend fun pause(): ElasticsearchSystem {\n    report(\n      action = \"Pause container\",\n      metadata = mapOf(\"operation\" to \"fault-injection\")\n    ) {\n      withContainerOrWarn(\"pause\") { it.pause() }\n    }\n    return this\n  }\n\n  /**\n   * Unpauses the container. Use with care, as it will unpause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return ElasticsearchSystem\n   */\n  @Suppress(\"unused\")\n  suspend fun unpause(): ElasticsearchSystem {\n    report(action = \"Unpause container\") {\n      withContainerOrWarn(\"unpause\") { it.unpause() }\n    }\n    return this\n  }\n\n  private suspend fun obtainExposedConfiguration(): ElasticSearchExposedConfiguration =\n    when {\n      context.options is ProvidedElasticsearchSystemOptions -> context.options.config\n      context.runtime is StoveElasticSearchContainer -> startElasticsearchContainer(context.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n    }\n\n  private suspend fun startElasticsearchContainer(container: StoveElasticSearchContainer): ElasticSearchExposedConfiguration =\n    state.capture {\n      container.start()\n      ElasticSearchExposedConfiguration(\n        host = container.host,\n        port = container.firstMappedPort,\n        password = context.options.container.password,\n        certificate = determineCertificate(container).getOrNull()\n      )\n    }\n\n  private fun determineCertificate(container: StoveElasticSearchContainer): Option<ElasticsearchExposedCertificate> =\n    when (context.options.container.disableSecurity) {\n      true -> None\n\n      false -> ElasticsearchExposedCertificate(\n        container.caCertAsBytes().getOrElse { ByteArray(0) }\n      ).apply { sslContext = container.createSslContextFromCa() }.some()\n    }\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (shouldRunMigrations()) {\n      context.options.migrationCollection.run(esClient)\n    }\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    context.options is ProvidedElasticsearchSystemOptions -> context.options.runMigrations\n    context.runtime is StoveElasticSearchContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n  }\n\n  private fun createEsClient(exposedConfiguration: ElasticSearchExposedConfiguration): ElasticsearchClient =\n    context.options.clientConfigurer.restClientOverrideFn\n      .getOrElse { { cfg -> restClient(cfg) } }\n      .let { it(exposedConfiguration) }\n      .let { restClient ->\n        RestClientTransport(restClient, context.options.jsonpMapper, restClientCompatibilityOptions())\n      }.let { ElasticsearchClient(it) }\n\n  private fun restClientCompatibilityOptions(): RestClientOptions =\n    RestClientOptions\n      .Builder(RequestOptions.DEFAULT.toBuilder())\n      .apply {\n        setHeader(\"Accept\", ELASTICSEARCH_COMPATIBILITY_HEADER)\n        setHeader(\"Content-Type\", ELASTICSEARCH_COMPATIBILITY_HEADER)\n      }.build()\n\n  private fun restClient(cfg: ElasticSearchExposedConfiguration): RestClient =\n    when (isSecurityDisabled(cfg)) {\n      true -> createInsecureRestClient(cfg)\n      false -> createSecureRestClient(cfg, obtainSslContext(cfg))\n    }\n\n  private fun isSecurityDisabled(cfg: ElasticSearchExposedConfiguration): Boolean = when {\n    context.options is ProvidedElasticsearchSystemOptions -> cfg.certificate == null\n    context.runtime is StoveElasticSearchContainer -> context.options.container.disableSecurity\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n  }\n\n  private fun obtainSslContext(cfg: ElasticSearchExposedConfiguration): SSLContext = when {\n    context.options is ProvidedElasticsearchSystemOptions -> cfg.certificate?.sslContext ?: throw IllegalStateException(\n      \"SSL context is required for secure connections with provided instances. \" +\n        \"Set the certificate.sslContext in ElasticSearchExposedConfiguration.\"\n    )\n\n    context.runtime is StoveElasticSearchContainer -> context.runtime.createSslContextFromCa()\n\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n  }\n\n  private fun createInsecureRestClient(cfg: ElasticSearchExposedConfiguration): RestClient =\n    RestClient\n      .builder(HttpHost(cfg.host, cfg.port))\n      .apply { setHttpClientConfigCallback { http -> http.also(context.options.clientConfigurer.httpClientBuilder) } }\n      .build()\n\n  private fun createSecureRestClient(\n    cfg: ElasticSearchExposedConfiguration,\n    sslContext: SSLContext\n  ): RestClient {\n    val credentialsProvider: CredentialsProvider = BasicCredentialsProvider().apply {\n      setCredentials(AuthScope.ANY, UsernamePasswordCredentials(\"elastic\", cfg.password))\n    }\n    return RestClient\n      .builder(HttpHost(cfg.host, cfg.port, \"https\"))\n      .setHttpClientConfigCallback { clientBuilder: HttpAsyncClientBuilder ->\n        clientBuilder.setSSLContext(sslContext)\n        clientBuilder.setDefaultCredentialsProvider(credentialsProvider)\n        context.options.clientConfigurer.httpClientBuilder(clientBuilder)\n        clientBuilder\n      }.build()\n  }\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StoveElasticSearchContainer) -> Unit\n  ): ElasticsearchSystem = when (val runtime = context.runtime) {\n    is StoveElasticSearchContainer -> {\n      action(runtime)\n      this\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"$operation() is not supported when using a provided instance\")\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private inline fun whenContainer(action: (StoveElasticSearchContainer) -> Unit) {\n    if (context.runtime is StoveElasticSearchContainer) {\n      action(context.runtime)\n    }\n  }\n\n  companion object {\n    private const val ELASTICSEARCH_COMPATIBILITY_HEADER = \"application/vnd.elasticsearch+json; compatible-with=8\"\n\n    /**\n     * Exposes the [ElasticsearchClient] for the given [ElasticsearchSystem].\n     * Use this for advanced Elasticsearch operations not covered by the DSL.\n     */\n    @Suppress(\"unused\")\n    fun ElasticsearchSystem.client(): ElasticsearchClient = this.esClient\n  }\n}\n"
  },
  {
    "path": "lib/stove-elasticsearch/src/main/kotlin/com/trendyol/stove/elasticsearch/Extensions.kt",
    "content": "package com.trendyol.stove.elasticsearch\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.containers.withProvidedRegistry\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\n\ninternal fun Stove.withElasticsearch(\n  options: ElasticsearchSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(ElasticsearchSystem(this, ElasticsearchContext(runtime, options)))\n  return this\n}\n\ninternal fun Stove.withElasticsearch(\n  key: SystemKey,\n  options: ElasticsearchSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(key, ElasticsearchSystem(this, ElasticsearchContext(runtime, options, keyName = keyDisplayName(key))))\n  return this\n}\n\ninternal fun Stove.elasticsearch(): ElasticsearchSystem =\n  getOrNone<ElasticsearchSystem>().getOrElse {\n    throw SystemNotRegisteredException(ElasticsearchSystem::class)\n  }\n\ninternal fun Stove.elasticsearch(key: SystemKey): ElasticsearchSystem =\n  getOrNone<ElasticsearchSystem>(key).getOrElse {\n    throw SystemNotRegisteredException(ElasticsearchSystem::class, \"No ElasticsearchSystem registered with key '${keyDisplayName(key)}'\")\n  }\n\n/**\n * Configures Elasticsearch system.\n *\n * For container-based setup:\n * ```kotlin\n * elasticsearch {\n *   ElasticsearchSystemOptions(\n *     cleanup = { client -> client.indices().delete { it.index(\"*\") } },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * elasticsearch {\n *   ElasticsearchSystemOptions.provided(\n *     host = \"localhost\",\n *     port = 9200,\n *     password = \"password\",\n *     runMigrations = true,\n *     cleanup = { client -> client.indices().delete { it.index(\"*\") } },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n */\nfun WithDsl.elasticsearch(\n  configure: @StoveDsl () -> ElasticsearchSystemOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedElasticsearchSystemOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      imageName = options.container.imageWithTag,\n      registry = options.container.registry,\n      compatibleSubstitute = options.container.compatibleSubstitute\n    ) { dockerImageName -> StoveElasticSearchContainer(dockerImageName) }\n      .apply {\n        addExposedPorts(*options.container.exposedPorts.toIntArray())\n        withPassword(options.container.password)\n        if (options.container.disableSecurity) {\n          withEnv(\"xpack.security.enabled\", \"false\")\n        }\n        withReuse(stove.keepDependenciesRunning)\n        options.container.containerFn(this)\n      }\n  }\n  return stove.withElasticsearch(options, runtime)\n}\n\nfun WithDsl.elasticsearch(\n  key: SystemKey,\n  configure: @StoveDsl () -> ElasticsearchSystemOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedElasticsearchSystemOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      imageName = options.container.imageWithTag,\n      registry = options.container.registry,\n      compatibleSubstitute = options.container.compatibleSubstitute\n    ) { dockerImageName -> StoveElasticSearchContainer(dockerImageName) }\n      .apply {\n        addExposedPorts(*options.container.exposedPorts.toIntArray())\n        withPassword(options.container.password)\n        if (options.container.disableSecurity) {\n          withEnv(\"xpack.security.enabled\", \"false\")\n        }\n        withReuse(stove.keepDependenciesRunning)\n        options.container.containerFn(this)\n      }\n  }\n  return stove.withElasticsearch(key, options, runtime)\n}\n\nsuspend fun ValidationDsl.elasticsearch(validation: @ElasticDsl suspend ElasticsearchSystem.() -> Unit): Unit =\n  validation(this.stove.elasticsearch())\n\nsuspend fun ValidationDsl.elasticsearch(key: SystemKey, validation: @ElasticDsl suspend ElasticsearchSystem.() -> Unit): Unit =\n  validation(this.stove.elasticsearch(key))\n"
  },
  {
    "path": "lib/stove-elasticsearch/src/main/kotlin/com/trendyol/stove/elasticsearch/Options.kt",
    "content": "package com.trendyol.stove.elasticsearch\n\nimport arrow.core.*\nimport co.elastic.clients.elasticsearch.ElasticsearchClient\nimport co.elastic.clients.json.JsonpMapper\nimport co.elastic.clients.json.jackson.JacksonJsonpMapper\nimport com.trendyol.stove.containers.*\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.apache.http.client.config.RequestConfig\nimport org.apache.http.impl.nio.client.HttpAsyncClientBuilder\nimport org.elasticsearch.client.RestClient\nimport org.testcontainers.elasticsearch.ElasticsearchContainer\nimport org.testcontainers.utility.DockerImageName\nimport kotlin.time.Duration.Companion.minutes\n\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class ElasticDsl\n\n/**\n * Options for configuring the Elasticsearch system in container mode.\n */\n@StoveDsl\nopen class ElasticsearchSystemOptions(\n  open val clientConfigurer: ElasticClientConfigurer = ElasticClientConfigurer(),\n  open val container: ElasticContainerOptions = ElasticContainerOptions(),\n  open val jsonpMapper: JsonpMapper = JacksonJsonpMapper(StoveSerde.jackson.default),\n  open val cleanup: suspend (ElasticsearchClient) -> Unit = {},\n  override val configureExposedConfiguration: (ElasticSearchExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<ElasticSearchExposedConfiguration>,\n  SupportsMigrations<ElasticsearchClient, ElasticsearchSystemOptions> {\n  override val migrationCollection: MigrationCollection<ElasticsearchClient> = MigrationCollection()\n\n  companion object {\n    /**\n     * Creates options configured to use an externally provided Elasticsearch instance\n     * instead of a testcontainer.\n     *\n     * @param host The Elasticsearch host\n     * @param port The Elasticsearch port\n     * @param password The Elasticsearch password (for authentication)\n     * @param certificate Optional SSL certificate for secure connections\n     * @param clientConfigurer Client configuration\n     * @param jsonpMapper JSON mapper for serialization\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      host: String,\n      port: Int,\n      password: String = \"\",\n      certificate: ElasticsearchExposedCertificate? = null,\n      clientConfigurer: ElasticClientConfigurer = ElasticClientConfigurer(),\n      jsonpMapper: JsonpMapper = JacksonJsonpMapper(StoveSerde.jackson.default),\n      runMigrations: Boolean = true,\n      cleanup: suspend (ElasticsearchClient) -> Unit = {},\n      configureExposedConfiguration: (ElasticSearchExposedConfiguration) -> List<String>\n    ): ProvidedElasticsearchSystemOptions = ProvidedElasticsearchSystemOptions(\n      config = ElasticSearchExposedConfiguration(\n        host = host,\n        port = port,\n        password = password,\n        certificate = certificate\n      ),\n      clientConfigurer = clientConfigurer,\n      jsonpMapper = jsonpMapper,\n      runMigrations = runMigrations,\n      cleanup = cleanup,\n      configureExposedConfiguration = configureExposedConfiguration\n    )\n  }\n}\n\n/**\n * Options for using an externally provided Elasticsearch instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedElasticsearchSystemOptions(\n  /**\n   * The configuration for the provided Elasticsearch instance.\n   */\n  val config: ElasticSearchExposedConfiguration,\n  clientConfigurer: ElasticClientConfigurer = ElasticClientConfigurer(),\n  jsonpMapper: JsonpMapper = JacksonJsonpMapper(StoveSerde.jackson.default),\n  cleanup: suspend (ElasticsearchClient) -> Unit = {},\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  configureExposedConfiguration: (ElasticSearchExposedConfiguration) -> List<String>\n) : ElasticsearchSystemOptions(\n  clientConfigurer = clientConfigurer,\n  container = ElasticContainerOptions(),\n  jsonpMapper = jsonpMapper,\n  cleanup = cleanup,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<ElasticSearchExposedConfiguration> {\n  override val providedConfig: ElasticSearchExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n\n/**\n * Convenience type alias for Elasticsearch migrations.\n *\n * Instead of writing `DatabaseMigration<ElasticsearchClient>`, use `ElasticsearchMigration`:\n * ```kotlin\n * class MyMigration : ElasticsearchMigration {\n *   override val order: Int = 1\n *   override suspend fun execute(connection: ElasticsearchClient) { ... }\n * }\n * ```\n */\ntypealias ElasticsearchMigration = DatabaseMigration<ElasticsearchClient>\n\ndata class ElasticSearchExposedConfiguration(\n  val host: String,\n  val port: Int,\n  val password: String,\n  val certificate: ElasticsearchExposedCertificate?\n) : ExposedConfiguration\n\n@StoveDsl\ndata class ElasticsearchContext(\n  val runtime: SystemRuntime,\n  val options: ElasticsearchSystemOptions,\n  val keyName: String? = null\n)\n\nopen class StoveElasticSearchContainer(\n  override val imageNameAccess: DockerImageName\n) : ElasticsearchContainer(imageNameAccess),\n  StoveContainer\n\ndata class ElasticContainerOptions(\n  override val registry: String = \"docker.elastic.co/\",\n  override val tag: String = \"8.6.1\",\n  override val image: String = \"elasticsearch/elasticsearch\",\n  override val compatibleSubstitute: String? = null,\n  val exposedPorts: List<Int> = listOf(DEFAULT_ELASTIC_PORT),\n  val password: String = \"password\",\n  val disableSecurity: Boolean = true,\n  override val useContainerFn: UseContainerFn<StoveElasticSearchContainer> = { StoveElasticSearchContainer(it) },\n  override val containerFn: ContainerFn<StoveElasticSearchContainer> = { }\n) : ContainerOptions<StoveElasticSearchContainer> {\n  companion object {\n    const val DEFAULT_ELASTIC_PORT = 9200\n  }\n}\n\ndata class ElasticClientConfigurer(\n  val httpClientBuilder: HttpAsyncClientBuilder.() -> Unit = {\n    setDefaultRequestConfig(\n      RequestConfig\n        .custom()\n        .setSocketTimeout(5.minutes.inWholeMilliseconds.toInt())\n        .setConnectTimeout(5.minutes.inWholeMilliseconds.toInt())\n        .setConnectionRequestTimeout(5.minutes.inWholeMilliseconds.toInt())\n        .build()\n    )\n  },\n  val restClientOverrideFn: Option<(cfg: ElasticSearchExposedConfiguration) -> RestClient> = none()\n)\n"
  },
  {
    "path": "lib/stove-elasticsearch/src/main/kotlin/com/trendyol/stove/elasticsearch/util.kt",
    "content": "package com.trendyol.stove.elasticsearch\n\nimport co.elastic.clients.elasticsearch._types.query_dsl.QueryVariant\n\ninternal fun QueryVariant.asJsonString(): String = this._toQuery().toString().removePrefix(\"Query:\")\n"
  },
  {
    "path": "lib/stove-elasticsearch/src/test/kotlin/com/trendyol/stove/elasticsearch/ElasticsearchExposedCertificateTest.kt",
    "content": "package com.trendyol.stove.elasticsearch\n\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport com.trendyol.stove.functional.get\nimport com.trendyol.stove.serialization.*\nimport com.trendyol.stove.system.abstractions.StateWithProcess\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.*\nimport io.kotest.matchers.ints.shouldBeGreaterThan\n\nclass ElasticsearchExposedCertificateTest :\n  FunSpec({\n\n    test(\"equals should return true for same byte content\") {\n      val bytes1 = \"test-cert-bytes\".toByteArray()\n      val bytes2 = \"test-cert-bytes\".toByteArray()\n      val cert1 = ElasticsearchExposedCertificate(bytes1)\n      val cert2 = ElasticsearchExposedCertificate(bytes2)\n\n      (cert1 == cert2) shouldBe true\n    }\n\n    test(\"equals should return false for different byte content\") {\n      val cert1 = ElasticsearchExposedCertificate(\"cert-a\".toByteArray())\n      val cert2 = ElasticsearchExposedCertificate(\"cert-b\".toByteArray())\n\n      (cert1 == cert2) shouldBe false\n    }\n\n    test(\"equals should return true for same instance\") {\n      val cert = ElasticsearchExposedCertificate(\"test\".toByteArray())\n\n      (cert == cert) shouldBe true\n    }\n\n    test(\"equals should return false for different type\") {\n      val cert = ElasticsearchExposedCertificate(\"test\".toByteArray())\n\n      cert.equals(\"not a certificate\") shouldBe false\n    }\n\n    test(\"hashCode should be consistent for same content\") {\n      val bytes1 = \"test-cert\".toByteArray()\n      val bytes2 = \"test-cert\".toByteArray()\n      val cert1 = ElasticsearchExposedCertificate(bytes1)\n      val cert2 = ElasticsearchExposedCertificate(bytes2)\n\n      cert1.hashCode() shouldBe cert2.hashCode()\n    }\n\n    test(\"hashCode should differ for different content\") {\n      val cert1 = ElasticsearchExposedCertificate(\"cert-a\".toByteArray())\n      val cert2 = ElasticsearchExposedCertificate(\"cert-b\".toByteArray())\n\n      cert1.hashCode() shouldNotBe cert2.hashCode()\n    }\n\n    test(\"ser/de\") {\n      val state =\n        \"\"\"\n            {\n              \"state\": {\n                \"host\": \"localhost\",\n                \"port\": 50543,\n                \"password\": \"password\",\n                \"certificate\": {\n                  \"bytes\": \"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUZXakNDQTBLZ0F3SUJBZ0lWQU4wclloSXpaMS90Rmg5NmR3WGI1b1lWWnl4UU1BMEdDU3FHU0liM0RRRUIKQ3dVQU1Ed3hPakE0QmdOVkJBTVRNVVZzWVhOMGFXTnpaV0Z5WTJnZ2MyVmpkWEpwZEhrZ1lYVjBieTFqYjI1bQphV2QxY21GMGFXOXVJRWhVVkZBZ1EwRXdIaGNOTWpNd09ERTFNVGd5TWpJNFdoY05Nall3T0RFME1UZ3lNakk0CldqQThNVG93T0FZRFZRUURFekZGYkdGemRHbGpjMlZoY21Ob0lITmxZM1Z5YVhSNUlHRjFkRzh0WTI5dVptbG4KZFhKaGRHbHZiaUJJVkZSUUlFTkJNSUlDSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQWc4QU1JSUNDZ0tDQWdFQQppcDVGaWwySUMyRGhYbHkyV3RYTFliTnJUbHNkQklZQ3JvK0N1QU40djJHM3RuMkNQTmVoMnM2V0ovRUNrRitVCndJUVVKWEN0Mm43aEJDWkY4M1BlQ1JabWZyWkE0VXNtdzBYWS9OWWpTcnJRQXVtODYvamFZM0lMVGYzU1Jnei8KaWNFVEJCRVM1eTdmSFZlU0xmTjl1ME9hSC9tTnN5Q3FoMERMRFZrWXR5MHNJZXorb1paNmtxN2UrRE1OeHB5Sgp1cjRvUGJ5ekdmY1dnZDdnMll5T2RxNEd1TmFOck8ySjFVZG5BV3B5TVdnME5TSzd1TGlEZ0w5a25ZZlBnMmhQCisrWVpnVkJNNXJBMXJhY2x3c0U0NWJNVlErKzRyWEhhbDUwdS83VmN6a3M5QTREVDc3ZHVyOC9aM1hONWtpMm0KaWNTemJDTHlLdTZwdUhLQWhCdFMyWnlMMXBYN09RSVA2aWZLaVpaU1ZYUGZnMGk5cjVQL3ZFZTJoVXpTTDRVLwpsSWQvaWJUQWtIcmZGbEErN2FreFNzcFJoalMra1ZTQndyR05KQ3BDbWsraitxSnB5Sis5aTNWb0pkanVvemprClk2bS9EZG9kc21LdUZFYUdZNytBT1RVMjAwN0ZjZWdXUEJWejgzU051WmpwbURCczMzS25oeG5WM0RBb0QzUm4KbkNoV2ZQTGo4TUl1OG9tMll3RUpZMUtLR1hzUzU5TWtyclpuQnNjdzl0S0prMFQyRHlLM2dWckY5UnJpMk1mTwpKY3FCWUhBSHRQTHRQZU5STHJLUmxtYkh4NXJFMkNCWEJWQWJ1bU1EaGRIbE1lTWtwT1p3WnoyQWljWUV1anhlClU0TUl5LzczU1RHakhtcGpVT3dKcjNMdVdqVlBMNDlZeTZZWmNPbENsTThDQXdFQUFhTlRNRkV3SFFZRFZSME8KQkJZRUZNUTlobHVXN3VzdGQwZkZDNU5zSkNyTDhaTStNQjhHQTFVZEl3UVlNQmFBRk1ROWhsdVc3dXN0ZDBmRgpDNU5zSkNyTDhaTStNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnSUJBSEdOCjQ1Nm1iYXdKUHNLTVgvQlowanB2LytTbCtMTTB6U2gxeEF1YXlmbDk3WnBlbS80QkhGcU5vTUxGOEVqczhXcHoKUHU1Y3Y5VVFaZXNaWVVsNHE4ODY5TW03QnQ5UHVRcUJBR25VbTU3alhMRkRsdDRvVTFvZmVpalF1YkZ3M0wwMwpGa2NsQ3psZ2JhV21vb2ZKRTdKK2FEMGo5bHNOWllKem9tQlN6QnZGTC9uK0ptS0poQVk4SDNwTkNqdExtbXZjClZPbmluQWFScGxLQndSS1RRYm1ZVE53QXVTcEhvSUk4empqK3pGWm54MzVqSitJY0YwblQ1Q3FQT0tCcllxTmwKN21kTnU0OGs0eUpiY0JtYXNoa3BRdkQra2Q1RFJBWmZXZ2tjZzVZUk1RUnE3RnVpWkhxcmFVdWV2WmZ3dnB2UApqMmV5M0QwMG5aSUVIN3I0alVpVnl0SGNGejVQU29zRmIwZDlmWkRJYmJGanRQblpSTEVxbS8wd3N1V25VSVdRCnlSWTNvclNiMUZIYjdYQUlUdHlnZlZQZnlUV0lnemdtbjFCR3Z2eE5sYjIyVnB4TXcvaEpLWTU0WDRjc2s1RzkKbHZMNUVzT3BvYnZvWVJRNU9taHlJT1ZGSHUwcjRKZWkzcGJ3dTczWmlnLzNFanJLY0lRS0ttYzdhQUFkbGREeQpid0dRWDdvYzRLS1lra2JPNFNNQTRTZzUxQjJFZFEzVGYrSHJlUjFTcHN1TlB1U2p0aGY5MGY2eWYrU1d0NU04CnY2RmpVRy9sR0NGTndJTTd2N1o3SHhMVnIvbVg4MTRKVzBGREdLUmhHRHd3SDUzcTJYSmRaaEl5RlNaeWtuc1UKdmJLeW51Vm43czZrU1pYbnh2NnYyTTNsL09ZMjdpNHdUVnB6bzhXbgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==\"\n                }\n              },\n              \"processId\": 10496\n            }\n        \"\"\".trimIndent()\n      val j = StoveSerde.jackson.default\n      val stateWithProcess = j.readValue<StateWithProcess<ElasticSearchExposedConfiguration>>(state)\n      val serialize = j.writeValueAsString(stateWithProcess)\n      val stateWithProcess2 = j.readValue<StateWithProcess<ElasticSearchExposedConfiguration>>(serialize)\n\n      val cert = stateWithProcess2.state.certificate!!\n      cert.bytes.size shouldBeGreaterThan 0\n      cert.sslContext shouldNotBe null\n      cert.sslContext.protocol shouldBe \"TLSv1.3\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove-elasticsearch/src/test/kotlin/com/trendyol/stove/elasticsearch/ElasticsearchOptionsTest.kt",
    "content": "package com.trendyol.stove.elasticsearch\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\n\nclass ElasticsearchOptionsTest :\n  FunSpec({\n\n    test(\"ElasticSearchExposedConfiguration should hold connection details\") {\n      val cfg = ElasticSearchExposedConfiguration(\n        host = \"localhost\",\n        port = 9200,\n        password = \"secret\",\n        certificate = null\n      )\n\n      cfg.host shouldBe \"localhost\"\n      cfg.port shouldBe 9200\n      cfg.password shouldBe \"secret\"\n      cfg.certificate shouldBe null\n    }\n\n    test(\"ElasticsearchSystemOptions.provided should create ProvidedElasticsearchSystemOptions\") {\n      val options = ElasticsearchSystemOptions.provided(\n        host = \"es-host\",\n        port = 9200,\n        password = \"pass\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\"es.host=${cfg.host}\", \"es.port=${cfg.port}\")\n        }\n      )\n\n      options.providedConfig.host shouldBe \"es-host\"\n      options.providedConfig.port shouldBe 9200\n      options.providedConfig.password shouldBe \"pass\"\n      options.providedConfig.certificate shouldBe null\n      options.runMigrationsForProvided shouldBe true\n    }\n\n    test(\"ProvidedElasticsearchSystemOptions should expose correct properties\") {\n      val config = ElasticSearchExposedConfiguration(\n        host = \"remote-es\",\n        port = 9201,\n        password = \"p\",\n        certificate = null\n      )\n      val options = ProvidedElasticsearchSystemOptions(\n        config = config,\n        runMigrations = false,\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.config shouldBe config\n      options.providedConfig shouldBe config\n      options.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"ElasticClientConfigurer should have default configuration\") {\n      val configurer = ElasticClientConfigurer()\n      configurer.httpClientBuilder shouldNotBe null\n      configurer.restClientOverrideFn.isNone() shouldBe true\n    }\n\n    test(\"ElasticContainerOptions should have sensible defaults\") {\n      val opts = ElasticContainerOptions()\n      opts.password shouldBe \"password\"\n      opts.disableSecurity shouldBe true\n      opts.exposedPorts shouldBe listOf(ElasticContainerOptions.DEFAULT_ELASTIC_PORT)\n    }\n  })\n"
  },
  {
    "path": "lib/stove-elasticsearch/src/test/kotlin/com/trendyol/stove/elasticsearch/ElasticsearchTestSystemTests.kt",
    "content": "package com.trendyol.stove.elasticsearch\n\nimport arrow.core.Some\nimport co.elastic.clients.elasticsearch.ElasticsearchClient\nimport co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders\nimport co.elastic.clients.elasticsearch.indices.CreateIndexRequest\nimport com.fasterxml.jackson.annotation.JsonIgnoreProperties\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport org.apache.http.HttpHost\nimport org.elasticsearch.client.RestClient\nimport org.junit.jupiter.api.assertThrows\nimport org.slf4j.*\nimport org.testcontainers.elasticsearch.ElasticsearchContainer\nimport org.testcontainers.utility.DockerImageName\nimport java.util.*\n\n// ============================================================================\n// Shared components\n// ============================================================================\n\nconst val TEST_INDEX = \"stove-test-index\"\nconst val ANOTHER_INDEX = \"stove-another-index\"\nconst val DEFAULT_ELASTICSEARCH_TEST_TAG = \"8.9.0\"\n\nobject ElasticsearchTestRuntimeConfig {\n  val tag: String =\n    System.getenv(\"ELASTICSEARCH_TEST_TAG\")\n      ?: System.getProperty(\"elasticsearchTestTag\")\n      ?: DEFAULT_ELASTICSEARCH_TEST_TAG\n\n  val imageName: DockerImageName = DockerImageName.parse(\"docker.elastic.co/elasticsearch/elasticsearch:$tag\")\n}\n\nclass NoOpApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) = Unit\n\n  override suspend fun stop() = Unit\n}\n\nclass TestIndexMigrator : ElasticsearchMigration {\n  override val order: Int = MigrationPriority.HIGHEST.value\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun execute(connection: ElasticsearchClient) {\n    val createIndexRequest: CreateIndexRequest =\n      CreateIndexRequest\n        .Builder()\n        .index(TEST_INDEX)\n        .build()\n    connection.indices().create(createIndexRequest)\n    logger.info(\"$TEST_INDEX is created\")\n  }\n}\n\nclass AnotherIndexMigrator : ElasticsearchMigration {\n  override val order: Int = MigrationPriority.HIGHEST.value + 1\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun execute(connection: ElasticsearchClient) {\n    val createIndexRequest: CreateIndexRequest =\n      CreateIndexRequest\n        .Builder()\n        .index(ANOTHER_INDEX)\n        .build()\n    connection.indices().create(createIndexRequest)\n    logger.info(\"$ANOTHER_INDEX is created\")\n  }\n}\n\n// ============================================================================\n// Strategy interface\n// ============================================================================\n\nsealed interface ElasticsearchTestStrategy {\n  val logger: Logger\n\n  suspend fun start()\n\n  suspend fun stop()\n\n  companion object {\n    fun select(): ElasticsearchTestStrategy {\n      val useProvided = System.getenv(\"USE_PROVIDED\")?.toBoolean()\n        ?: System.getProperty(\"useProvided\")?.toBoolean()\n        ?: false\n\n      return if (useProvided) ProvidedElasticsearchStrategy() else ContainerElasticsearchStrategy()\n    }\n  }\n}\n\n// ============================================================================\n// Container-based strategy (default)\n// ============================================================================\n\nclass ContainerElasticsearchStrategy : ElasticsearchTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun start() {\n    logger.info(\"Starting Elasticsearch tests with container mode. tag=${ElasticsearchTestRuntimeConfig.tag}\")\n\n    val options = ElasticsearchSystemOptions(\n      clientConfigurer = ElasticClientConfigurer(\n        restClientOverrideFn = Some { cfg -> RestClient.builder(HttpHost(cfg.host, cfg.port)).build() }\n      ),\n      ElasticContainerOptions(tag = ElasticsearchTestRuntimeConfig.tag),\n      configureExposedConfiguration = { _ -> listOf() }\n    ).migrations {\n      register<TestIndexMigrator>()\n      register<AnotherIndexMigrator>()\n    }\n\n    Stove()\n      .with {\n        elasticsearch { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    logger.info(\"Elasticsearch container tests completed\")\n  }\n}\n\n// ============================================================================\n// Provided instance strategy\n// ============================================================================\n\nclass ProvidedElasticsearchStrategy : ElasticsearchTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  private lateinit var externalContainer: ElasticsearchContainer\n\n  override suspend fun start() {\n    logger.info(\"Starting Elasticsearch tests with provided mode. tag=${ElasticsearchTestRuntimeConfig.tag}\")\n\n    // Start an external container to simulate a provided instance\n    externalContainer = ElasticsearchContainer(ElasticsearchTestRuntimeConfig.imageName)\n      .withEnv(\"xpack.security.enabled\", \"false\")\n      .withEnv(\"discovery.type\", \"single-node\")\n      .apply { start() }\n\n    logger.info(\"External Elasticsearch container started at ${externalContainer.httpHostAddress}\")\n\n    val hostPort = externalContainer.httpHostAddress.split(\":\")\n    val options = ElasticsearchSystemOptions\n      .provided(\n        host = hostPort[0],\n        port = hostPort[1].toInt(),\n        runMigrations = true,\n        clientConfigurer = ElasticClientConfigurer(\n          restClientOverrideFn = Some { cfg -> RestClient.builder(HttpHost(cfg.host, cfg.port)).build() }\n        ),\n        cleanup = { client ->\n          logger.info(\"Running cleanup on provided instance\")\n          // Clean up test data if needed\n        },\n        configureExposedConfiguration = { _ -> listOf() }\n      ).migrations {\n        register<TestIndexMigrator>()\n        register<AnotherIndexMigrator>()\n      }\n\n    Stove()\n      .with {\n        elasticsearch { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    externalContainer.stop()\n    logger.info(\"Elasticsearch provided tests completed\")\n  }\n}\n\n// ============================================================================\n// Kotest project config - selects strategy based on environment\n// ============================================================================\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val strategy = ElasticsearchTestStrategy.select()\n\n  override suspend fun beforeProject() = strategy.start()\n\n  override suspend fun afterProject() = strategy.stop()\n}\n\n// ============================================================================\n// Tests\n// ============================================================================\n\nclass ElasticsearchTestSystemTests :\n  FunSpec({\n\n    @JsonIgnoreProperties\n    data class ExampleInstance(\n      val id: String,\n      val description: String\n    )\n    test(\"should save and get\") {\n      val exampleInstance = ExampleInstance(\"1\", \"1312\")\n      stove {\n        elasticsearch {\n          save(exampleInstance.id, exampleInstance, TEST_INDEX)\n          shouldGet<ExampleInstance>(key = exampleInstance.id, index = TEST_INDEX) {\n            it.description shouldBe exampleInstance.description\n          }\n        }\n      }\n    }\n\n    test(\"should save and get from another index\") {\n      val exampleInstance = ExampleInstance(\"1\", \"1312\")\n      stove {\n        elasticsearch {\n          save(exampleInstance.id, exampleInstance, ANOTHER_INDEX)\n          shouldGet<ExampleInstance>(ANOTHER_INDEX, exampleInstance.id) {\n            it.description shouldBe exampleInstance.description\n          }\n        }\n      }\n    }\n\n    test(\"should save 2 documents with the same description, then delete first one and query by description\") {\n      val desc = \"some description\"\n      val exampleInstance1 = ExampleInstance(\"1\", desc)\n      val exampleInstance2 = ExampleInstance(\"2\", desc)\n      val queryByDesc = QueryBuilders\n        .term()\n        .field(\"description.keyword\")\n        .value(desc)\n        .queryName(\"query_name\")\n        .build()\n      val queryAsString = queryByDesc.asJsonString()\n      stove {\n        elasticsearch {\n          save(exampleInstance1.id, exampleInstance1, TEST_INDEX)\n          save(exampleInstance2.id, exampleInstance2, TEST_INDEX)\n          shouldQuery<ExampleInstance>(queryByDesc._toQuery()) {\n            it.size shouldBe 2\n          }\n          shouldDelete(exampleInstance1.id, TEST_INDEX)\n          shouldGet<ExampleInstance>(key = exampleInstance2.id, index = TEST_INDEX) {}\n          shouldQuery<ExampleInstance>(queryAsString, TEST_INDEX) {\n            it.size shouldBe 1\n          }\n        }\n      }\n    }\n\n    test(\"should throw assertion error when document does exist\") {\n      val existDocId = UUID.randomUUID().toString()\n      val exampleInstance = ExampleInstance(existDocId, \"1312\")\n      stove {\n        elasticsearch {\n          save(exampleInstance.id, exampleInstance, TEST_INDEX)\n          shouldGet<ExampleInstance>(key = exampleInstance.id, index = TEST_INDEX) {\n            it.description shouldBe exampleInstance.description\n          }\n\n          assertThrows<AssertionError> { shouldNotExist(existDocId, index = TEST_INDEX) }\n        }\n      }\n    }\n\n    test(\"should does not throw exception when given does not exist id\") {\n      val notExistDocId = UUID.randomUUID().toString()\n      stove {\n        elasticsearch {\n          shouldNotExist(notExistDocId, index = TEST_INDEX)\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-elasticsearch/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.elasticsearch.StoveConfig\n"
  },
  {
    "path": "lib/stove-grpc/api/stove-grpc.api",
    "content": "public abstract interface annotation class com/trendyol/stove/grpc/GrpcDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/grpc/GrpcSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/PluggedSystem {\n\tpublic static final field Companion Lcom/trendyol/stove/grpc/GrpcSystem$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/grpc/GrpcSystemOptions;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/grpc/GrpcSystemOptions;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun channelWithMetadata (Ljava/util/Map;)Lio/grpc/Channel;\n\tpublic fun close ()V\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun getGrpcChannel ()Lio/grpc/ManagedChannel;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/grpc/GrpcSystemOptions;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun getWireClientResources ()Lcom/trendyol/stove/grpc/WireClientResources;\n\tpublic final fun grpcClient ()Lcom/squareup/wire/GrpcClient;\n\tpublic final fun managedChannel ()Lio/grpc/ManagedChannel;\n\tpublic final fun rawChannel (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/grpc/GrpcSystem;\n\tpublic final fun rawWireClient (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/grpc/GrpcSystem;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun withEndpoint (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/grpc/GrpcSystem;\n}\n\npublic final class com/trendyol/stove/grpc/GrpcSystem$Companion {\n}\n\npublic final class com/trendyol/stove/grpc/GrpcSystemKt {\n\tpublic static final fun grpc-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun grpc-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun grpc-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun grpc-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/grpc/GrpcSystemOptions : com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic synthetic fun <init> (Ljava/lang/String;IZJLjava/util/List;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;IZJLjava/util/List;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()I\n\tpublic final fun component3 ()Z\n\tpublic final fun component4-UwyO8pc ()J\n\tpublic final fun component5 ()Ljava/util/List;\n\tpublic final fun component6 ()Ljava/util/Map;\n\tpublic final fun component7 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun component8 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun copy-mGOUYlo (Ljava/lang/String;IZJLjava/util/List;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)Lcom/trendyol/stove/grpc/GrpcSystemOptions;\n\tpublic static synthetic fun copy-mGOUYlo$default (Lcom/trendyol/stove/grpc/GrpcSystemOptions;Ljava/lang/String;IZJLjava/util/List;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/trendyol/stove/grpc/GrpcSystemOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getCreateChannel ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getCreateWireClient ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getHost ()Ljava/lang/String;\n\tpublic final fun getInterceptors ()Ljava/util/List;\n\tpublic final fun getMetadata ()Ljava/util/Map;\n\tpublic final fun getPort ()I\n\tpublic final fun getTimeout-UwyO8pc ()J\n\tpublic final fun getUsePlaintext ()Z\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/grpc/MetadataInterceptor : io/grpc/ClientInterceptor {\n\tpublic fun <init> (Ljava/util/Map;)V\n\tpublic fun interceptCall (Lio/grpc/MethodDescriptor;Lio/grpc/CallOptions;Lio/grpc/Channel;)Lio/grpc/ClientCall;\n}\n\npublic final class com/trendyol/stove/grpc/WireClientResources {\n\tpublic fun <init> (Lcom/squareup/wire/GrpcClient;Lokhttp3/OkHttpClient;)V\n\tpublic final fun close ()V\n\tpublic final fun component1 ()Lcom/squareup/wire/GrpcClient;\n\tpublic final fun component2 ()Lokhttp3/OkHttpClient;\n\tpublic final fun copy (Lcom/squareup/wire/GrpcClient;Lokhttp3/OkHttpClient;)Lcom/trendyol/stove/grpc/WireClientResources;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/grpc/WireClientResources;Lcom/squareup/wire/GrpcClient;Lokhttp3/OkHttpClient;ILjava/lang/Object;)Lcom/trendyol/stove/grpc/WireClientResources;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getGrpcClient ()Lcom/squareup/wire/GrpcClient;\n\tpublic final fun getOkHttpClient ()Lokhttp3/OkHttpClient;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/grpc/test/GrpcTestServiceClient : com/trendyol/stove/grpc/test/TestServiceClient {\n\tpublic fun <init> (Lcom/squareup/wire/GrpcClient;)V\n\tpublic fun AuthenticatedCall ()Lcom/squareup/wire/GrpcCall;\n\tpublic fun BidiStream ()Lcom/squareup/wire/GrpcStreamingCall;\n\tpublic fun ClientStream ()Lcom/squareup/wire/GrpcStreamingCall;\n\tpublic fun ServerStream ()Lcom/squareup/wire/GrpcStreamingCall;\n\tpublic fun Unary ()Lcom/squareup/wire/GrpcCall;\n}\n\npublic final class com/trendyol/stove/grpc/test/TestRequest : com/squareup/wire/Message {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/grpc/test/TestRequest$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;ILokio/ByteString;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;ILokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun copy (Ljava/lang/String;ILokio/ByteString;)Lcom/trendyol/stove/grpc/test/TestRequest;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/grpc/test/TestRequest;Ljava/lang/String;ILokio/ByteString;ILjava/lang/Object;)Lcom/trendyol/stove/grpc/test/TestRequest;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getCount ()I\n\tpublic final fun getMessage ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic synthetic fun newBuilder ()Lcom/squareup/wire/Message$Builder;\n\tpublic synthetic fun newBuilder ()Ljava/lang/Void;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/grpc/test/TestRequest$Companion {\n}\n\npublic final class com/trendyol/stove/grpc/test/TestResponse : com/squareup/wire/Message {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/grpc/test/TestResponse$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;IZLokio/ByteString;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;IZLokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun copy (Ljava/lang/String;IZLokio/ByteString;)Lcom/trendyol/stove/grpc/test/TestResponse;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/grpc/test/TestResponse;Ljava/lang/String;IZLokio/ByteString;ILjava/lang/Object;)Lcom/trendyol/stove/grpc/test/TestResponse;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getCount ()I\n\tpublic final fun getMessage ()Ljava/lang/String;\n\tpublic final fun getSuccess ()Z\n\tpublic fun hashCode ()I\n\tpublic synthetic fun newBuilder ()Lcom/squareup/wire/Message$Builder;\n\tpublic synthetic fun newBuilder ()Ljava/lang/Void;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/grpc/test/TestResponse$Companion {\n}\n\npublic abstract interface class com/trendyol/stove/grpc/test/TestServiceClient : com/squareup/wire/Service {\n\tpublic abstract fun AuthenticatedCall ()Lcom/squareup/wire/GrpcCall;\n\tpublic abstract fun BidiStream ()Lcom/squareup/wire/GrpcStreamingCall;\n\tpublic abstract fun ClientStream ()Lcom/squareup/wire/GrpcStreamingCall;\n\tpublic abstract fun ServerStream ()Lcom/squareup/wire/GrpcStreamingCall;\n\tpublic abstract fun Unary ()Lcom/squareup/wire/GrpcCall;\n}\n\npublic abstract interface class com/trendyol/stove/grpc/test/TestServiceServer : com/squareup/wire/Service {\n\tpublic abstract fun AuthenticatedCall (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun BidiStream (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlinx/coroutines/channels/SendChannel;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun ClientStream (Lkotlinx/coroutines/channels/ReceiveChannel;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun ServerStream (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlinx/coroutines/channels/SendChannel;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun Unary (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/grpc/test/TestServiceWireGrpc {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/grpc/test/TestServiceWireGrpc;\n\tpublic static final field SERVICE_NAME Ljava/lang/String;\n\tpublic final fun getAuthenticatedCallMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun getBidiStreamMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun getClientStreamMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun getServerStreamMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun getServiceDescriptor ()Lio/grpc/ServiceDescriptor;\n\tpublic final fun getUnaryMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun newStub (Lio/grpc/Channel;)Lcom/trendyol/stove/grpc/test/TestServiceWireGrpc$TestServiceStub;\n}\n\npublic final class com/trendyol/stove/grpc/test/TestServiceWireGrpc$BindableAdapter : com/trendyol/stove/grpc/test/TestServiceWireGrpc$TestServiceImplBase {\n\tpublic fun <init> (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;)V\n\tpublic synthetic fun <init> (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun <init> (Lkotlin/jvm/functions/Function0;)V\n\tpublic fun AuthenticatedCall (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun BidiStream (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;\n\tpublic fun ClientStream (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun ServerStream (Lcom/trendyol/stove/grpc/test/TestRequest;)Lkotlinx/coroutines/flow/Flow;\n\tpublic fun Unary (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract class com/trendyol/stove/grpc/test/TestServiceWireGrpc$TestServiceImplBase : com/squareup/wire/kotlin/grpcserver/WireBindableService {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lkotlin/coroutines/CoroutineContext;)V\n\tpublic synthetic fun <init> (Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun AuthenticatedCall (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun BidiStream (Lkotlinx/coroutines/flow/Flow;)Lkotlinx/coroutines/flow/Flow;\n\tpublic fun ClientStream (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun ServerStream (Lcom/trendyol/stove/grpc/test/TestRequest;)Lkotlinx/coroutines/flow/Flow;\n\tpublic fun Unary (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun bindService ()Lio/grpc/ServerServiceDefinition;\n\tprotected final fun getContext ()Lkotlin/coroutines/CoroutineContext;\n}\n\npublic final class com/trendyol/stove/grpc/test/TestServiceWireGrpc$TestServiceImplBase$TestRequestMarshaller : com/squareup/wire/kotlin/grpcserver/WireMethodMarshaller {\n\tpublic fun <init> ()V\n\tpublic fun marshalledClass ()Ljava/lang/Class;\n\tpublic fun parse (Ljava/io/InputStream;)Lcom/trendyol/stove/grpc/test/TestRequest;\n\tpublic synthetic fun parse (Ljava/io/InputStream;)Ljava/lang/Object;\n\tpublic fun stream (Lcom/trendyol/stove/grpc/test/TestRequest;)Ljava/io/InputStream;\n\tpublic synthetic fun stream (Ljava/lang/Object;)Ljava/io/InputStream;\n}\n\npublic final class com/trendyol/stove/grpc/test/TestServiceWireGrpc$TestServiceImplBase$TestResponseMarshaller : com/squareup/wire/kotlin/grpcserver/WireMethodMarshaller {\n\tpublic fun <init> ()V\n\tpublic fun marshalledClass ()Ljava/lang/Class;\n\tpublic fun parse (Ljava/io/InputStream;)Lcom/trendyol/stove/grpc/test/TestResponse;\n\tpublic synthetic fun parse (Ljava/io/InputStream;)Ljava/lang/Object;\n\tpublic fun stream (Lcom/trendyol/stove/grpc/test/TestResponse;)Ljava/io/InputStream;\n\tpublic synthetic fun stream (Ljava/lang/Object;)Ljava/io/InputStream;\n}\n\npublic final class com/trendyol/stove/grpc/test/TestServiceWireGrpc$TestServiceStub : io/grpc/kotlin/AbstractCoroutineStub {\n\tpublic final fun AuthenticatedCall (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun BidiStream (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun ClientStream (Lkotlinx/coroutines/flow/Flow;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun ServerStream (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun Unary (Lcom/trendyol/stove/grpc/test/TestRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic synthetic fun build (Lio/grpc/Channel;Lio/grpc/CallOptions;)Lio/grpc/stub/AbstractStub;\n}\n\n"
  },
  {
    "path": "lib/stove-grpc/build.gradle.kts",
    "content": "plugins {\n  alias(libs.plugins.wire)\n}\n\ndependencies {\n  api(projects.lib.stove)\n  api(libs.io.grpc)\n  api(libs.io.grpc.stub)\n  api(libs.io.grpc.kotlin)\n  api(libs.io.grpc.netty)\n  api(libs.io.grpc.protobuf)\n  api(libs.wire.grpc.client)\n  api(libs.wire.grpc.runtime)\n  api(libs.google.protobuf.kotlin)\n  implementation(libs.wire.grpc.server)\n  implementation(libs.kotlin.reflect)\n  implementation(libs.kotlinx.core)\n  implementation(libs.kotlinx.jdk8)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.logback.classic)\n  testImplementation(libs.kotest.runner.junit5)\n  testImplementation(testFixtures(projects.lib.stove))\n}\n\nbuildscript {\n  dependencies {\n    classpath(libs.wire.grpc.server.generator)\n  }\n}\n\nwire {\n  sourcePath(\"src/test/proto\")\n\n  kotlin {\n    rpcRole = \"client\"\n    rpcCallStyle = \"suspending\"\n    exclusive = false\n    javaInterop = false\n  }\n\n  kotlin {\n    custom {\n      schemaHandlerFactory = com.squareup.wire.kotlin.grpcserver.GrpcServerSchemaHandler.Factory()\n      options = mapOf(\n        \"singleMethodServices\" to \"false\",\n        \"rpcCallStyle\" to \"suspending\"\n      )\n    }\n    rpcRole = \"server\"\n    rpcCallStyle = \"suspending\"\n    exclusive = false\n    singleMethodServices = false\n    javaInterop = true\n    includes = listOf(\"com.trendyol.stove.grpc.test.TestService\")\n  }\n}\n"
  },
  {
    "path": "lib/stove-grpc/src/main/kotlin/com/trendyol/stove/grpc/GrpcDsl.kt",
    "content": "package com.trendyol.stove.grpc\n\n/**\n * DSL marker for gRPC testing operations.\n *\n * This annotation is used to scope the DSL functions and prevent\n * accidental nesting of incompatible DSL blocks.\n */\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class GrpcDsl\n"
  },
  {
    "path": "lib/stove-grpc/src/main/kotlin/com/trendyol/stove/grpc/GrpcSystem.kt",
    "content": "package com.trendyol.stove.grpc\n\nimport arrow.core.getOrElse\nimport com.squareup.wire.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport com.trendyol.stove.tracing.TraceContext\nimport io.grpc.*\nimport java.util.concurrent.TimeUnit\n\n/**\n * gRPC client system for testing gRPC APIs.\n *\n * Provides a fluent DSL for making gRPC requests and asserting responses.\n * Supports multiple gRPC providers through a provider-agnostic design.\n *\n * ## Typed Channel (Recommended)\n *\n * For any stub type with a Channel constructor (grpc-kotlin, Wire, etc.):\n *\n * ```kotlin\n * grpc {\n *     channel<GreeterServiceStub> {\n *         val response = sayHello(HelloRequest(name = \"World\"))\n *         response.message shouldBe \"Hello, World!\"\n *     }\n * }\n * ```\n *\n * ## Wire Clients\n *\n * For Wire-generated service clients:\n *\n * ```kotlin\n * grpc {\n *     wireClient<GreeterServiceClient> {\n *         val response = SayHello().execute(HelloRequest(name = \"World\"))\n *         response.message shouldBe \"Hello, World!\"\n *     }\n * }\n * ```\n *\n * ## Custom Providers\n *\n * For any other gRPC library:\n *\n * ```kotlin\n * grpc {\n *     withEndpoint({ host, port -> CustomGrpcLib.connect(host, port) }) {\n *         call(...) shouldBe expected\n *     }\n * }\n * ```\n *\n * ## Streaming\n *\n * All streaming types work naturally with Kotlin coroutines:\n *\n * ```kotlin\n * grpc {\n *     channel<StreamServiceStub> {\n *         // Server streaming\n *         serverStream(request).collect { response ->\n *             // assertions on each response\n *         }\n *\n *         // Client streaming\n *         val response = clientStream(flow { emit(request1); emit(request2) })\n *\n *         // Bidirectional streaming\n *         bidiStream(requestFlow).collect { response ->\n *             // assertions\n *         }\n *     }\n * }\n * ```\n *\n * ## Per-Call Metadata\n *\n * Add metadata (headers) to specific calls:\n *\n * ```kotlin\n * grpc {\n *     channel<GreeterServiceStub>(\n *         metadata = mapOf(\"authorization\" to \"Bearer $token\")\n *     ) {\n *         sayHello(request)\n *     }\n * }\n * ```\n *\n * @property stove The parent test system.\n * @property options gRPC client configuration options.\n * @see GrpcSystemOptions\n */\n@GrpcDsl\nclass GrpcSystem(\n  override val stove: Stove,\n  @PublishedApi internal val options: GrpcSystemOptions,\n  private val keyName: String? = null\n) : PluggedSystem, Reports {\n  private val lazyGrpcChannel = lazy { options.createChannel(options.host, options.port) }\n\n  override val reportSystemName: String = \"gRPC\" + (keyName?.let { \" [$it]\" } ?: \"\")\n\n  private val lazyWireClientResources = lazy { options.createWireClient(options.host, options.port) }\n\n  @PublishedApi\n  internal val grpcChannel: ManagedChannel\n    get() = lazyGrpcChannel.value\n\n  @PublishedApi\n  internal val wireClientResources: WireClientResources\n    get() = lazyWireClientResources.value\n\n  /**\n   * Execute gRPC calls using a Wire-generated client.\n   *\n   * The client is automatically created from the GrpcClient, and `this` in the block\n   * refers to the client instance.\n   *\n   * ```kotlin\n   * grpc {\n   *     wireClient<GreeterServiceClient> {\n   *         val response = SayHello().execute(HelloRequest(name = \"World\"))\n   *         response.message shouldBe \"Hello!\"\n   *     }\n   * }\n   * ```\n   *\n   * @param T The Wire service client type.\n   * @param block The block to execute with the client as receiver.\n   */\n  suspend inline fun <reified T : Service> wireClient(\n    crossinline block: @GrpcDsl suspend T.() -> Unit\n  ): GrpcSystem {\n    val serviceName = T::class.simpleName ?: \"Unknown\"\n    val client = wireClientResources.grpcClient.create(T::class)\n    report(\n      action = \"Wire client: $serviceName\",\n      metadata = mapOf(\"service\" to serviceName)\n    ) {\n      block(client)\n    }\n    return this\n  }\n\n  /**\n   * Execute gRPC calls using a custom client created by the provided factory.\n   *\n   * This allows integration with any gRPC library by providing a factory function\n   * that takes host and port and returns the client.\n   *\n   * ```kotlin\n   * grpc {\n   *     withEndpoint({ h, p -> CustomGrpcLib.connect(h, p) }) {\n   *         call(...) shouldBe expected\n   *     }\n   * }\n   * ```\n   *\n   * @param T The custom client type.\n   * @param factory Factory function that creates the client from host and port.\n   * @param block The block to execute with the client as receiver.\n   */\n  inline fun <T> withEndpoint(\n    factory: (host: String, port: Int) -> T,\n    block: @GrpcDsl T.() -> Unit\n  ): GrpcSystem {\n    val client = factory(options.host, options.port)\n    block(client)\n    return this\n  }\n\n  /**\n   * Execute gRPC calls using any stub type that has a Channel constructor.\n   *\n   * The stub is automatically created from the channel using reflection.\n   * Works with grpc-kotlin stubs, Wire-generated stubs, and any other\n   * stub that takes a Channel as constructor parameter.\n   *\n   * ```kotlin\n   * grpc {\n   *     channel<GreeterServiceStub> {\n   *         val response = sayHello(HelloRequest(name = \"World\"))\n   *         response.message shouldBe \"Hello!\"\n   *     }\n   * }\n   * ```\n   *\n   * @param T The stub type with a Channel constructor.\n   * @param metadata Optional per-call metadata to add to all requests in this block.\n   * @param block The block to execute with the stub as receiver.\n   */\n  suspend inline fun <reified T : Any> channel(\n    metadata: Map<String, String> = emptyMap(),\n    crossinline block: @GrpcDsl suspend T.() -> Unit\n  ): GrpcSystem {\n    val stubName = T::class.simpleName ?: \"Unknown\"\n    val stubInstance = createStubFromChannel<T>(metadata)\n    report(\n      action = \"Channel stub: $stubName\",\n      metadata = mapOf(\"stub\" to stubName, \"hasMetadata\" to metadata.isNotEmpty())\n    ) {\n      block(stubInstance)\n    }\n    return this\n  }\n\n  /**\n   * Execute operations with direct access to the ManagedChannel.\n   *\n   * Use this for advanced scenarios where you need full control over\n   * stub creation, custom interceptors, or channel operations.\n   *\n   * ```kotlin\n   * grpc {\n   *     rawChannel { ch ->\n   *         val interceptedChannel = ClientInterceptors.intercept(ch, myInterceptor)\n   *         val stub = GreeterGrpc.newBlockingStub(interceptedChannel)\n   *         // ... use stub\n   *     }\n   * }\n   * ```\n   *\n   * @param block The block to execute with the channel.\n   */\n  inline fun rawChannel(\n    block: @GrpcDsl (ManagedChannel) -> Unit\n  ): GrpcSystem {\n    block(grpcChannel)\n    return this\n  }\n\n  /**\n   * Execute operations with direct access to the Wire GrpcClient.\n   *\n   * Use this for advanced Wire scenarios where you need direct client access.\n   *\n   * ```kotlin\n   * grpc {\n   *     rawWireClient { client ->\n   *         val service = client.create(MyServiceClient::class)\n   *         // ... use service\n   *     }\n   * }\n   * ```\n   *\n   * @param block The block to execute with the Wire GrpcClient.\n   */\n  inline fun rawWireClient(\n    block: @GrpcDsl (GrpcClient) -> Unit\n  ): GrpcSystem {\n    block(wireClientResources.grpcClient)\n    return this\n  }\n\n  /**\n   * Exposes the [ManagedChannel] used by this system.\n   */\n  @Suppress(\"unused\")\n  fun managedChannel(): ManagedChannel = grpcChannel\n\n  /**\n   * Exposes the Wire [GrpcClient] used by this system.\n   */\n  @Suppress(\"unused\")\n  fun grpcClient(): GrpcClient = wireClientResources.grpcClient\n\n  override fun then(): Stove = stove\n\n  override fun close() {\n    if (lazyGrpcChannel.isInitialized()) {\n      grpcChannel.shutdown()\n      grpcChannel.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n    }\n    if (lazyWireClientResources.isInitialized()) {\n      wireClientResources.close()\n    }\n  }\n\n  companion object {\n    private const val SHUTDOWN_TIMEOUT_SECONDS = 5L\n  }\n\n  /**\n   * Returns a channel with optional metadata interceptor applied.\n   * Automatically injects trace context headers when a trace is active.\n   */\n  @PublishedApi\n  internal fun channelWithMetadata(metadata: Map<String, String>): Channel {\n    val metadataWithTrace = buildMap {\n      putAll(metadata)\n      TraceContext.current()?.let { ctx ->\n        put(TraceContext.TRACEPARENT_HEADER, ctx.toTraceparent())\n        put(TraceContext.STOVE_TEST_ID_HEADER, ctx.testId)\n      }\n    }\n    return if (metadataWithTrace.isNotEmpty()) {\n      ClientInterceptors.intercept(grpcChannel, MetadataInterceptor(metadataWithTrace))\n    } else {\n      grpcChannel\n    }\n  }\n\n  /**\n   * Creates a stub instance from a Channel using Java reflection.\n   *\n   * This method handles internal constructors (like Wire-generated stubs) by\n   * using setAccessible(true). It looks for constructors that take:\n   * - Just a Channel\n   * - Channel and CallOptions\n   */\n  @PublishedApi\n  internal inline fun <reified T : Any> createStubFromChannel(\n    metadata: Map<String, String>\n  ): T {\n    val stubClass = T::class.java\n    val channelToUse = channelWithMetadata(metadata)\n\n    // Find a constructor that takes Channel (or Channel + CallOptions)\n    val constructor = stubClass.declaredConstructors\n      .filter { ctor ->\n        val params = ctor.parameterTypes\n        params.isNotEmpty() && Channel::class.java.isAssignableFrom(params[0])\n      }\n      .minByOrNull { it.parameterCount }\n      ?: throw IllegalArgumentException(\n        \"Cannot find suitable constructor for stub ${stubClass.simpleName}. \" +\n          \"Expected a constructor with Channel parameter.\"\n      )\n\n    constructor.isAccessible = true\n    return when (constructor.parameterCount) {\n      1 -> constructor.newInstance(channelToUse) as T\n      2 -> constructor.newInstance(channelToUse, CallOptions.DEFAULT) as T\n      else -> throw IllegalArgumentException(\n        \"Unexpected constructor signature for stub ${stubClass.simpleName}\"\n      )\n    }\n  }\n}\n\ninternal fun Stove.withGrpc(options: GrpcSystemOptions): Stove {\n  this.getOrRegister(GrpcSystem(this, options))\n  return this\n}\n\ninternal fun Stove.withGrpc(key: SystemKey, options: GrpcSystemOptions): Stove {\n  this.getOrRegister(key, GrpcSystem(this, options, keyName = keyDisplayName(key)))\n  return this\n}\n\ninternal fun Stove.grpc(): GrpcSystem = getOrNone<GrpcSystem>().getOrElse {\n  throw SystemNotRegisteredException(GrpcSystem::class)\n}\n\ninternal fun Stove.grpc(key: SystemKey): GrpcSystem = getOrNone<GrpcSystem>(key).getOrElse {\n  throw SystemNotRegisteredException(GrpcSystem::class, \"No GrpcSystem registered with key '${keyDisplayName(key)}'\")\n}\n\n/**\n * Registers the gRPC client system with the test system.\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         grpc {\n *             GrpcSystemOptions(host = \"localhost\", port = 50051)\n *         }\n *     }\n * ```\n *\n * @param configure Configuration block returning [GrpcSystemOptions].\n * @return The test system for fluent chaining.\n */\nfun WithDsl.grpc(configure: @StoveDsl () -> GrpcSystemOptions): Stove =\n  this.stove.withGrpc(configure())\n\n/**\n * Registers a keyed gRPC client system for testing multiple gRPC services.\n *\n * ```kotlin\n * Stove().with {\n *     grpc(PaymentService) {\n *         GrpcSystemOptions(host = \"localhost\", port = 50051)\n *     }\n * }\n * ```\n *\n * @param key The [SystemKey] identifying this gRPC client instance.\n * @param configure Configuration block returning [GrpcSystemOptions].\n * @return The test system for fluent chaining.\n */\nfun WithDsl.grpc(key: SystemKey, configure: @StoveDsl () -> GrpcSystemOptions): Stove =\n  this.stove.withGrpc(key, configure())\n\n/**\n * Executes gRPC assertions within the validation DSL.\n *\n * ```kotlin\n * stove {\n *     grpc {\n *         channel<GreeterServiceStub> {\n *             sayHello(request).message shouldBe \"Hello!\"\n *         }\n *     }\n * }\n * ```\n *\n * @param validation The gRPC assertion block.\n */\nsuspend fun ValidationDsl.grpc(\n  validation: @GrpcDsl suspend GrpcSystem.() -> Unit\n): Unit = validation(this.stove.grpc())\n\n/**\n * Executes gRPC assertions against a keyed gRPC client within the validation DSL.\n *\n * ```kotlin\n * stove {\n *     grpc(PaymentService) {\n *         channel<PaymentServiceStub> {\n *             processPayment(request).status shouldBe \"OK\"\n *         }\n *     }\n * }\n * ```\n *\n * @param key The [SystemKey] identifying the gRPC client instance.\n * @param validation The gRPC assertion block.\n */\nsuspend fun ValidationDsl.grpc(\n  key: SystemKey,\n  validation: @GrpcDsl suspend GrpcSystem.() -> Unit\n): Unit = validation(this.stove.grpc(key))\n"
  },
  {
    "path": "lib/stove-grpc/src/main/kotlin/com/trendyol/stove/grpc/GrpcSystemOptions.kt",
    "content": "package com.trendyol.stove.grpc\n\nimport com.squareup.wire.GrpcClient\nimport com.trendyol.stove.system.abstractions.SystemOptions\nimport io.grpc.*\nimport okhttp3.Interceptor\nimport okhttp3.OkHttpClient\nimport okhttp3.Protocol\nimport java.util.concurrent.TimeUnit\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.toJavaDuration\n\n/**\n * Holds both the Wire GrpcClient and its underlying OkHttpClient for proper resource management.\n */\ndata class WireClientResources(\n  val grpcClient: GrpcClient,\n  val okHttpClient: OkHttpClient\n) {\n  fun close() {\n    okHttpClient.dispatcher.executorService.shutdown()\n    okHttpClient.connectionPool.evictAll()\n  }\n}\n\n/**\n * Configuration options for the gRPC client system.\n *\n * ## Basic Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         grpc {\n *             GrpcSystemOptions(\n *                 host = \"localhost\",\n *                 port = 50051\n *             )\n *         }\n *     }\n * ```\n *\n * ## With Authentication\n *\n * ```kotlin\n * grpc {\n *     GrpcSystemOptions(\n *         host = \"localhost\",\n *         port = 50051,\n *         metadata = mapOf(\"authorization\" to \"Bearer $token\"),\n *         interceptors = listOf(LoggingInterceptor())\n *     )\n * }\n * ```\n *\n * ## Custom Channel\n *\n * For advanced scenarios (custom TLS, load balancing, etc.):\n *\n * ```kotlin\n * grpc {\n *     GrpcSystemOptions(\n *         host = \"localhost\",\n *         port = 50051,\n *         createChannel = { host, port ->\n *             ManagedChannelBuilder.forAddress(host, port)\n *                 .usePlaintext()\n *                 .enableRetry()\n *                 .build()\n *         }\n *     )\n * }\n * ```\n *\n * @property host The gRPC server host.\n * @property port The gRPC server port.\n * @property usePlaintext Whether to use plaintext (no TLS). Default is true for testing.\n * @property timeout Request timeout duration (default: 30 seconds).\n * @property interceptors List of client interceptors for logging, auth, tracing, etc.\n * @property metadata Default metadata (headers) to send with every request.\n * @property createChannel Factory function for creating the underlying ManagedChannel.\n * @property createWireClient Factory function for creating Wire's GrpcClient with resources.\n */\n@GrpcDsl\ndata class GrpcSystemOptions(\n  val host: String,\n  val port: Int,\n  val usePlaintext: Boolean = true,\n  val timeout: Duration = 30.seconds,\n  val interceptors: List<ClientInterceptor> = emptyList(),\n  val metadata: Map<String, String> = emptyMap(),\n  val createChannel: (host: String, port: Int) -> ManagedChannel = { h, p ->\n    defaultChannelBuilder(h, p, usePlaintext, timeout, interceptors, metadata)\n  },\n  val createWireClient: (host: String, port: Int) -> WireClientResources = { h, p ->\n    defaultWireGrpcClient(h, p, timeout, metadata)\n  }\n) : SystemOptions\n\n/**\n * Creates a default ManagedChannel with standard configuration.\n */\ninternal fun defaultChannelBuilder(\n  host: String,\n  port: Int,\n  usePlaintext: Boolean,\n  timeout: Duration,\n  interceptors: List<ClientInterceptor>,\n  metadata: Map<String, String>\n): ManagedChannel {\n  val builder = ManagedChannelBuilder\n    .forAddress(host, port)\n    .keepAliveTime(timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS)\n    .keepAliveTimeout(timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS)\n\n  if (usePlaintext) {\n    builder.usePlaintext()\n  }\n\n  // Add metadata interceptor if metadata is provided\n  if (metadata.isNotEmpty()) {\n    builder.intercept(MetadataInterceptor(metadata))\n  }\n\n  // Add user-provided interceptors\n  if (interceptors.isNotEmpty()) {\n    builder.intercept(interceptors)\n  }\n\n  return builder.build()\n}\n\n/**\n * Creates a default Wire GrpcClient with standard configuration and metadata support.\n */\ninternal fun defaultWireGrpcClient(\n  host: String,\n  port: Int,\n  timeout: Duration,\n  metadata: Map<String, String>\n): WireClientResources {\n  val okHttpClientBuilder = OkHttpClient\n    .Builder()\n    .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE))\n    .callTimeout(timeout.toJavaDuration())\n    .readTimeout(timeout.toJavaDuration())\n    .writeTimeout(timeout.toJavaDuration())\n    .connectTimeout(timeout.toJavaDuration())\n\n  // Add metadata headers via OkHttp interceptor\n  if (metadata.isNotEmpty()) {\n    okHttpClientBuilder.addInterceptor(\n      Interceptor { chain ->\n        val requestBuilder = chain.request().newBuilder()\n        metadata.forEach { (key, value) ->\n          requestBuilder.addHeader(key, value)\n        }\n        chain.proceed(requestBuilder.build())\n      }\n    )\n  }\n\n  val okHttpClient = okHttpClientBuilder.build()\n\n  val grpcClient = GrpcClient\n    .Builder()\n    .client(okHttpClient)\n    .baseUrl(\"http://$host:$port\")\n    .build()\n\n  return WireClientResources(grpcClient, okHttpClient)\n}\n\n/**\n * Interceptor that adds metadata (headers) to every gRPC call.\n */\n@PublishedApi\ninternal class MetadataInterceptor(\n  private val headers: Map<String, String>\n) : ClientInterceptor {\n  override fun <ReqT, RespT> interceptCall(\n    method: MethodDescriptor<ReqT, RespT>,\n    callOptions: CallOptions,\n    next: Channel\n  ): ClientCall<ReqT, RespT> = object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(\n    next.newCall(method, callOptions)\n  ) {\n    override fun start(responseListener: Listener<RespT>, metadata: Metadata) {\n      headers.forEach { (key, value) ->\n        metadata.put(Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER), value)\n      }\n      super.start(responseListener, metadata)\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove-grpc/src/test/kotlin/com/trendyol/stove/grpc/GrpcAuthInterceptorTest.kt",
    "content": "package com.trendyol.stove.grpc\n\nimport com.squareup.wire.*\nimport com.trendyol.stove.grpc.test.*\nimport com.trendyol.stove.system.stove\nimport io.grpc.*\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport org.slf4j.LoggerFactory\n\n/**\n * Tests for authentication and interceptor functionality in GrpcSystem.\n */\nclass GrpcAuthInterceptorTest :\n  FunSpec({\n\n    test(\"authenticated call should fail without authorization header (Wire client)\") {\n      // The default setup has auth headers, but we can verify the error handling\n      // by testing a fresh gRPC client without the headers\n      stove {\n        grpc {\n          // Create a wire client without auth headers\n          withEndpoint({ host, port ->\n            val okHttpClient = okhttp3.OkHttpClient\n              .Builder()\n              .protocols(listOf(okhttp3.Protocol.H2_PRIOR_KNOWLEDGE))\n              .build()\n            com.squareup.wire.GrpcClient\n              .Builder()\n              .client(okHttpClient)\n              .baseUrl(\"http://$host:$port\")\n              .build()\n              .create(TestServiceClient::class)\n          }) {\n            val exception = shouldThrow<GrpcException> {\n              AuthenticatedCall().execute(TestRequest(message = \"Hello\", count = 1))\n            }\n            exception.grpcStatus shouldBe GrpcStatus.UNAUTHENTICATED\n            exception.grpcMessage shouldContain \"authorization\"\n          }\n        }\n      }\n    }\n\n    test(\"wire client with auth header should succeed\") {\n      stove {\n        grpc {\n          withEndpoint({ host, port ->\n            val okHttpClient = okhttp3.OkHttpClient\n              .Builder()\n              .protocols(listOf(okhttp3.Protocol.H2_PRIOR_KNOWLEDGE))\n              .addInterceptor { chain ->\n                val request = chain\n                  .request()\n                  .newBuilder()\n                  .addHeader(\"authorization\", \"Bearer my-token\")\n                  .build()\n                chain.proceed(request)\n              }.build()\n            com.squareup.wire.GrpcClient\n              .Builder()\n              .client(okHttpClient)\n              .baseUrl(\"http://$host:$port\")\n              .build()\n              .create(TestServiceClient::class)\n          }) {\n            val response = AuthenticatedCall().execute(TestRequest(message = \"Secure\", count = 42))\n            response.message shouldBe \"Authenticated: Secure\"\n            response.success shouldBe true\n          }\n        }\n      }\n    }\n  })\n\n/**\n * A logging interceptor for testing purposes.\n */\nclass TestLoggingInterceptor : ClientInterceptor {\n  private val logger = LoggerFactory.getLogger(javaClass)\n\n  override fun <ReqT, RespT> interceptCall(\n    method: MethodDescriptor<ReqT, RespT>,\n    callOptions: CallOptions,\n    next: Channel\n  ): ClientCall<ReqT, RespT> {\n    logger.info(\"gRPC call: ${method.fullMethodName}\")\n    return next.newCall(method, callOptions)\n  }\n}\n"
  },
  {
    "path": "lib/stove-grpc/src/test/kotlin/com/trendyol/stove/grpc/GrpcSystemStubTest.kt",
    "content": "package com.trendyol.stove.grpc\n\nimport com.trendyol.stove.grpc.test.*\nimport com.trendyol.stove.system.stove\nimport io.grpc.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.*\nimport io.kotest.matchers.collections.shouldContain\nimport kotlinx.coroutines.flow.*\nimport java.util.concurrent.CopyOnWriteArrayList\n\n/**\n * Tests for typed channel DSL functionality in GrpcSystem.\n *\n * These tests verify the `channel<T>` DSL method which creates stubs\n * automatically from the managed channel, hiding the boilerplate.\n */\nclass GrpcSystemStubTest :\n  FunSpec({\n\n    test(\"channel<T> should execute unary call successfully\") {\n      stove {\n        grpc {\n          channel<TestServiceWireGrpc.TestServiceStub> {\n            val response = Unary(TestRequest(message = \"Hello Stub\", count = 42))\n            response.message shouldBe \"Echo: Hello Stub\"\n            response.count shouldBe 42\n            response.success shouldBe true\n          }\n        }\n      }\n    }\n\n    test(\"channel<T> should handle server streaming\") {\n      stove {\n        grpc {\n          channel<TestServiceWireGrpc.TestServiceStub> {\n            val responses = ServerStream(TestRequest(message = \"Stream\", count = 3)).toList()\n\n            responses.size shouldBe 3\n            responses[0].message shouldBe \"Stream - Item 0\"\n            responses[1].message shouldBe \"Stream - Item 1\"\n            responses[2].message shouldBe \"Stream - Item 2\"\n          }\n        }\n      }\n    }\n\n    test(\"channel<T> should handle client streaming\") {\n      stove {\n        grpc {\n          channel<TestServiceWireGrpc.TestServiceStub> {\n            val requestFlow = flow {\n              emit(TestRequest(message = \"First\", count = 1))\n              emit(TestRequest(message = \"Second\", count = 2))\n              emit(TestRequest(message = \"Third\", count = 3))\n            }\n\n            val response = ClientStream(requestFlow)\n            response.message shouldBe \"Received: First, Second, Third\"\n            response.count shouldBe 6\n            response.success shouldBe true\n          }\n        }\n      }\n    }\n\n    test(\"channel<T> should handle bidirectional streaming\") {\n      stove {\n        grpc {\n          channel<TestServiceWireGrpc.TestServiceStub> {\n            val requestFlow = flow {\n              emit(TestRequest(message = \"A\", count = 1))\n              emit(TestRequest(message = \"B\", count = 2))\n            }\n\n            val responses = BidiStream(requestFlow).toList()\n            responses.size shouldBe 2\n            responses[0].message shouldBe \"Echo: A\"\n            responses[1].message shouldBe \"Echo: B\"\n          }\n        }\n      }\n    }\n\n    test(\"channel<T> should support multiple sequential calls\") {\n      stove {\n        grpc {\n          channel<TestServiceWireGrpc.TestServiceStub> {\n            val response1 = Unary(TestRequest(message = \"Call 1\", count = 1))\n            response1.message shouldBe \"Echo: Call 1\"\n\n            val response2 = Unary(TestRequest(message = \"Call 2\", count = 2))\n            response2.message shouldBe \"Echo: Call 2\"\n\n            val response3 = Unary(TestRequest(message = \"Call 3\", count = 3))\n            response3.message shouldBe \"Echo: Call 3\"\n          }\n        }\n      }\n    }\n\n    test(\"channel<T> with per-call metadata should work\") {\n      stove {\n        grpc {\n          channel<TestServiceWireGrpc.TestServiceStub>(\n            metadata = mapOf(\"authorization\" to \"Bearer custom-token\")\n          ) {\n            val response = AuthenticatedCall(TestRequest(message = \"Authenticated\", count = 1))\n            response.message shouldBe \"Authenticated: Authenticated\"\n            response.success shouldBe true\n          }\n        }\n      }\n    }\n\n    test(\"rawChannel should provide direct access to ManagedChannel\") {\n      stove {\n        grpc {\n          rawChannel { ch ->\n            ch shouldNotBe null\n          }\n        }\n      }\n    }\n\n    test(\"rawChannel with custom interceptor should intercept calls\") {\n      val interceptedMethods = CopyOnWriteArrayList<String>()\n\n      stove {\n        grpc {\n          rawChannel { ch ->\n            // Create a logging interceptor\n            val loggingInterceptor = object : ClientInterceptor {\n              override fun <ReqT, RespT> interceptCall(\n                method: MethodDescriptor<ReqT, RespT>,\n                callOptions: CallOptions,\n                next: Channel\n              ): ClientCall<ReqT, RespT> {\n                interceptedMethods.add(method.fullMethodName)\n                return next.newCall(method, callOptions)\n              }\n            }\n\n            val interceptedChannel = ClientInterceptors.intercept(ch, loggingInterceptor)\n            val stub = TestServiceWireGrpc.TestServiceStub(interceptedChannel)\n\n            stub.Unary(TestRequest(message = \"Intercepted\", count = 1))\n          }\n        }\n      }\n\n      interceptedMethods shouldContain \"com.trendyol.stove.grpc.test.TestService/Unary\"\n    }\n\n    test(\"rawChannel with auth interceptor should add headers\") {\n      stove {\n        grpc {\n          rawChannel { ch ->\n            // Create an auth interceptor that adds authorization header\n            val authInterceptor = object : ClientInterceptor {\n              override fun <ReqT, RespT> interceptCall(\n                method: MethodDescriptor<ReqT, RespT>,\n                callOptions: CallOptions,\n                next: Channel\n              ): ClientCall<ReqT, RespT> = object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(\n                next.newCall(method, callOptions)\n              ) {\n                override fun start(responseListener: Listener<RespT>, headers: Metadata) {\n                  headers.put(\n                    Metadata.Key.of(\"authorization\", Metadata.ASCII_STRING_MARSHALLER),\n                    \"Bearer raw-channel-token\"\n                  )\n                  super.start(responseListener, headers)\n                }\n              }\n            }\n\n            val interceptedChannel = ClientInterceptors.intercept(ch, authInterceptor)\n            val stub = TestServiceWireGrpc.TestServiceStub(interceptedChannel)\n\n            // This should succeed because we added the auth header\n            val response = stub.AuthenticatedCall(TestRequest(message = \"RawAuth\", count = 1))\n            response.message shouldBe \"Authenticated: RawAuth\"\n            response.success shouldBe true\n          }\n        }\n      }\n    }\n\n    test(\"rawChannel with multiple interceptors should chain them\") {\n      val interceptorOrder = CopyOnWriteArrayList<String>()\n\n      stove {\n        grpc {\n          rawChannel { ch ->\n            val firstInterceptor = object : ClientInterceptor {\n              override fun <ReqT, RespT> interceptCall(\n                method: MethodDescriptor<ReqT, RespT>,\n                callOptions: CallOptions,\n                next: Channel\n              ): ClientCall<ReqT, RespT> {\n                interceptorOrder.add(\"first\")\n                return next.newCall(method, callOptions)\n              }\n            }\n\n            val secondInterceptor = object : ClientInterceptor {\n              override fun <ReqT, RespT> interceptCall(\n                method: MethodDescriptor<ReqT, RespT>,\n                callOptions: CallOptions,\n                next: Channel\n              ): ClientCall<ReqT, RespT> {\n                interceptorOrder.add(\"second\")\n                return next.newCall(method, callOptions)\n              }\n            }\n\n            // Interceptors are applied in reverse order (last added runs first)\n            val interceptedChannel = ClientInterceptors.intercept(ch, firstInterceptor, secondInterceptor)\n            val stub = TestServiceWireGrpc.TestServiceStub(interceptedChannel)\n\n            stub.Unary(TestRequest(message = \"Chained\", count = 1))\n          }\n        }\n      }\n\n      // ClientInterceptors.intercept applies in reverse order\n      interceptorOrder shouldBe listOf(\"second\", \"first\")\n    }\n\n    test(\"rawChannel should allow manual stub creation with custom call options\") {\n      stove {\n        grpc {\n          rawChannel { ch ->\n            val stub = TestServiceWireGrpc.TestServiceStub(ch)\n\n            // Execute a call\n            val response = stub.Unary(TestRequest(message = \"Manual\", count = 99))\n            response.message shouldBe \"Echo: Manual\"\n            response.count shouldBe 99\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-grpc/src/test/kotlin/com/trendyol/stove/grpc/GrpcSystemWireTest.kt",
    "content": "package com.trendyol.stove.grpc\n\nimport com.trendyol.stove.grpc.test.*\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldStartWith\n\n/**\n * Tests for Wire client functionality in GrpcSystem.\n */\nclass GrpcSystemWireTest :\n  FunSpec({\n\n    test(\"wireClient should execute unary call successfully\") {\n      stove {\n        grpc {\n          wireClient<TestServiceClient> {\n            val response = Unary().execute(TestRequest(message = \"Hello Wire\", count = 42))\n            response.message shouldBe \"Echo: Hello Wire\"\n            response.count shouldBe 42\n            response.success shouldBe true\n          }\n        }\n      }\n    }\n\n    test(\"rawWireClient should provide direct access to GrpcClient\") {\n      stove {\n        grpc {\n          rawWireClient { client ->\n            val service = client.create(TestServiceClient::class)\n            val response = service.Unary().execute(TestRequest(message = \"Direct\", count = 1))\n            response.message shouldStartWith \"Echo:\"\n          }\n        }\n      }\n    }\n\n    test(\"withEndpoint should work with Wire client factory\") {\n      stove {\n        grpc {\n          withEndpoint({ host, port ->\n            val okHttpClient = okhttp3.OkHttpClient\n              .Builder()\n              .protocols(listOf(okhttp3.Protocol.H2_PRIOR_KNOWLEDGE))\n              .build()\n            com.squareup.wire.GrpcClient\n              .Builder()\n              .client(okHttpClient)\n              .baseUrl(\"http://$host:$port\")\n              .build()\n              .create(TestServiceClient::class)\n          }) {\n            val response = Unary().execute(TestRequest(message = \"Custom\", count = 99))\n            response.message shouldBe \"Echo: Custom\"\n            response.count shouldBe 99\n          }\n        }\n      }\n    }\n\n    test(\"multiple sequential calls should work\") {\n      stove {\n        grpc {\n          wireClient<TestServiceClient> {\n            val response1 = Unary().execute(TestRequest(message = \"First\", count = 1))\n            response1.message shouldBe \"Echo: First\"\n\n            val response2 = Unary().execute(TestRequest(message = \"Second\", count = 2))\n            response2.message shouldBe \"Echo: Second\"\n\n            val response3 = Unary().execute(TestRequest(message = \"Third\", count = 3))\n            response3.message shouldBe \"Echo: Third\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-grpc/src/test/kotlin/com/trendyol/stove/grpc/StoveConfig.kt",
    "content": "package com.trendyol.stove.grpc\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.PortFinder\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\n\n/**\n * Test application that wraps the [TestGrpcServer].\n * This integrates the gRPC server lifecycle with Stove's test system.\n */\nclass TestGrpcApp(\n  private val port: Int\n) : ApplicationUnderTest<TestGrpcServer> {\n  private lateinit var server: TestGrpcServer\n\n  override suspend fun start(configurations: List<String>): TestGrpcServer {\n    server = TestGrpcServer(port).start()\n    return server\n  }\n\n  override suspend fun stop() {\n    if (::server.isInitialized) {\n      server.close()\n    }\n  }\n}\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        grpc {\n          GrpcSystemOptions(\n            host = TEST_HOST,\n            port = TEST_PORT,\n            metadata = mapOf(\"authorization\" to \"Bearer test-token\")\n          )\n        }\n\n        applicationUnderTest(TestGrpcApp(TEST_PORT))\n      }.run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n\n  companion object {\n    const val TEST_HOST = \"localhost\"\n    val TEST_PORT = PortFinder.findAvailablePort()\n  }\n}\n"
  },
  {
    "path": "lib/stove-grpc/src/test/kotlin/com/trendyol/stove/grpc/TestGrpcServer.kt",
    "content": "package com.trendyol.stove.grpc\n\nimport com.trendyol.stove.grpc.test.*\nimport io.grpc.*\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport org.slf4j.LoggerFactory\nimport java.util.concurrent.TimeUnit\n\n/**\n * Test gRPC server for testing the GrpcSystem.\n *\n * Implements all RPC types: unary, server streaming, client streaming, and bidirectional streaming.\n */\nclass TestGrpcServer(\n  private val port: Int\n) : AutoCloseable {\n  private val logger = LoggerFactory.getLogger(javaClass)\n  private lateinit var server: Server\n\n  fun start(): TestGrpcServer {\n    server = ServerBuilder\n      .forPort(port)\n      .addService(TestServiceImpl())\n      .intercept(AuthorizationServerInterceptor())\n      .build()\n      .start()\n\n    logger.info(\"Test gRPC server started on port $port\")\n    return this\n  }\n\n  fun awaitTermination() {\n    server.awaitTermination()\n  }\n\n  override fun close() {\n    if (::server.isInitialized) {\n      logger.info(\"Shutting down test gRPC server\")\n      server.shutdown()\n      server.awaitTermination(5, TimeUnit.SECONDS)\n    }\n  }\n}\n\n/**\n * Implementation of the TestService for testing purposes.\n */\nclass TestServiceImpl : TestServiceWireGrpc.TestServiceImplBase() {\n  override suspend fun Unary(request: TestRequest): TestResponse = TestResponse(\n    message = \"Echo: ${request.message}\",\n    count = request.count,\n    success = true\n  )\n\n  override fun ServerStream(request: TestRequest): Flow<TestResponse> = flow {\n    repeat(request.count) { i ->\n      emit(\n        TestResponse(\n          message = \"${request.message} - Item $i\",\n          count = i,\n          success = true\n        )\n      )\n      delay(10) // Small delay to simulate streaming\n    }\n  }\n\n  override suspend fun ClientStream(request: Flow<TestRequest>): TestResponse {\n    var totalCount = 0\n    val messages = mutableListOf<String>()\n\n    request.collect { req ->\n      totalCount += req.count\n      messages.add(req.message)\n    }\n\n    return TestResponse(\n      message = \"Received: ${messages.joinToString(\", \")}\",\n      count = totalCount,\n      success = true\n    )\n  }\n\n  override fun BidiStream(request: Flow<TestRequest>): Flow<TestResponse> = flow {\n    request.collect { req ->\n      emit(\n        TestResponse(\n          message = \"Echo: ${req.message}\",\n          count = req.count,\n          success = true\n        )\n      )\n    }\n  }\n\n  override suspend fun AuthenticatedCall(request: TestRequest): TestResponse {\n    val authToken = AUTH_TOKEN_KEY.get()\n    return if (authToken != null && authToken.startsWith(\"Bearer \")) {\n      TestResponse(\n        message = \"Authenticated: ${request.message}\",\n        count = request.count,\n        success = true\n      )\n    } else {\n      throw StatusException(Status.UNAUTHENTICATED.withDescription(\"Missing or invalid authorization\"))\n    }\n  }\n\n  companion object {\n    val AUTH_TOKEN_KEY: Context.Key<String> = Context.key(\"auth-token\")\n  }\n}\n\n/**\n * Server interceptor that extracts authorization header and stores in context.\n */\nclass AuthorizationServerInterceptor : ServerInterceptor {\n  override fun <ReqT, RespT> interceptCall(\n    call: ServerCall<ReqT, RespT>,\n    headers: Metadata,\n    next: ServerCallHandler<ReqT, RespT>\n  ): ServerCall.Listener<ReqT> {\n    val authHeader = headers.get(Metadata.Key.of(\"authorization\", Metadata.ASCII_STRING_MARSHALLER))\n    val context = if (authHeader != null) {\n      Context.current().withValue(TestServiceImpl.AUTH_TOKEN_KEY, authHeader)\n    } else {\n      Context.current()\n    }\n    return Contexts.interceptCall(context, call, headers, next)\n  }\n}\n"
  },
  {
    "path": "lib/stove-grpc/src/test/proto/test_service.proto",
    "content": "syntax = \"proto3\";\n\npackage com.trendyol.stove.grpc.test;\n\noption java_multiple_files = true;\noption java_package = \"com.trendyol.stove.grpc.test\";\n\n// Request message for test service\nmessage TestRequest {\n  string message = 1;\n  int32 count = 2;\n}\n\n// Response message for test service\nmessage TestResponse {\n  string message = 1;\n  int32 count = 2;\n  bool success = 3;\n}\n\n// Test service with all RPC types\nservice TestService {\n  // Unary RPC - single request, single response\n  rpc Unary(TestRequest) returns (TestResponse);\n\n  // Server streaming RPC - single request, stream of responses\n  rpc ServerStream(TestRequest) returns (stream TestResponse);\n\n  // Client streaming RPC - stream of requests, single response\n  rpc ClientStream(stream TestRequest) returns (TestResponse);\n\n  // Bidirectional streaming RPC - stream of requests, stream of responses\n  rpc BidiStream(stream TestRequest) returns (stream TestResponse);\n\n  // Authenticated endpoint that checks for authorization header\n  rpc AuthenticatedCall(TestRequest) returns (TestResponse);\n}\n"
  },
  {
    "path": "lib/stove-grpc/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.grpc.StoveConfig\n"
  },
  {
    "path": "lib/stove-grpc/src/test/resources/logback-test.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n    <encoder>\n      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n    </encoder>\n  </appender>\n\n  <logger name=\"io.grpc\" level=\"WARN\"/>\n  <logger name=\"io.netty\" level=\"WARN\"/>\n\n  <root level=\"INFO\">\n    <appender-ref ref=\"STDOUT\"/>\n  </root>\n</configuration>\n"
  },
  {
    "path": "lib/stove-grpc-mock/api/stove-grpc-mock.api",
    "content": "public final class com/trendyol/stove/testing/grpcmock/GrpcMetadataKeys {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/testing/grpcmock/GrpcMetadataKeys;\n\tpublic final fun ascii (Ljava/lang/String;)Lio/grpc/Metadata$Key;\n\tpublic final fun binary (Ljava/lang/String;)Lio/grpc/Metadata$Key;\n\tpublic final fun getAUTHORIZATION ()Lio/grpc/Metadata$Key;\n\tpublic final fun getCONTENT_TYPE ()Lio/grpc/Metadata$Key;\n}\n\npublic abstract interface annotation class com/trendyol/stove/testing/grpcmock/GrpcMockDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/GrpcMockExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;I)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()I\n\tpublic final fun copy (Ljava/lang/String;I)Lcom/trendyol/stove/testing/grpcmock/GrpcMockExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/GrpcMockExposedConfiguration;Ljava/lang/String;IILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/GrpcMockExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getHost ()Ljava/lang/String;\n\tpublic final fun getPort ()I\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/GrpcMockSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware, com/trendyol/stove/system/abstractions/ValidatedSystem {\n\tpublic static final field Companion Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystem$Companion;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun mockBidiStream (Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockBidiStream$default (Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystem;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockClientStream (Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockClientStream$default (Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystem;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockError (Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lio/grpc/Status$Code;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockError$default (Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystem;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lio/grpc/Status$Code;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockServerStream (Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockServerStream$default (Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystem;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Ljava/util/List;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockUnary (Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockUnary$default (Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystem;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic fun validate (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/GrpcMockSystem$Companion {\n\tpublic final fun server (Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystem;)Lio/grpc/Server;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/GrpcMockSystemOptions : com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()Z\n\tpublic final fun component3 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun component4 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystemOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystemOptions;IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/GrpcMockSystemOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAfterStubMatched ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getOnRequestReceived ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getPort ()I\n\tpublic final fun getRemoveStubAfterRequestMatched ()Z\n\tpublic final fun getServerBuilder ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/GrpcMockSystemOptionsKt {\n\tpublic static final fun grpcMock-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun grpcMock-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun grpcMock-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun grpcMock-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic abstract class com/trendyol/stove/testing/grpcmock/MetadataMatcher {\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/MetadataMatcher$All : com/trendyol/stove/testing/grpcmock/MetadataMatcher {\n\tpublic fun <init> (Ljava/util/List;)V\n\tpublic fun <init> ([Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;)V\n\tpublic final fun component1 ()Ljava/util/List;\n\tpublic final fun copy (Ljava/util/List;)Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$All;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$All;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$All;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getMatchers ()Ljava/util/List;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/MetadataMatcher$Any : com/trendyol/stove/testing/grpcmock/MetadataMatcher {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$Any;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/MetadataMatcher$BearerToken : com/trendyol/stove/testing/grpcmock/MetadataMatcher {\n\tpublic fun <init> (Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;)Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$BearerToken;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$BearerToken;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$BearerToken;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getToken ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/MetadataMatcher$Custom : com/trendyol/stove/testing/grpcmock/MetadataMatcher {\n\tpublic fun <init> (Lkotlin/jvm/functions/Function1;)V\n\tpublic final fun component1 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$Custom;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$Custom;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$Custom;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getMatcher ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/MetadataMatcher$HasHeader : com/trendyol/stove/testing/grpcmock/MetadataMatcher {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$HasHeader;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$HasHeader;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$HasHeader;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getKey ()Ljava/lang/String;\n\tpublic final fun getValue ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/MetadataMatcher$RequiresAuth : com/trendyol/stove/testing/grpcmock/MetadataMatcher {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher$RequiresAuth;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/ReceivedRequest {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/grpcmock/StubKey;[BLio/grpc/Metadata;JZLjava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/testing/grpcmock/StubKey;[BLio/grpc/Metadata;JZLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/testing/grpcmock/StubKey;\n\tpublic final fun component2 ()[B\n\tpublic final fun component3 ()Lio/grpc/Metadata;\n\tpublic final fun component4 ()J\n\tpublic final fun component5 ()Z\n\tpublic final fun component6 ()Ljava/lang/String;\n\tpublic final fun copy (Lcom/trendyol/stove/testing/grpcmock/StubKey;[BLio/grpc/Metadata;JZLjava/lang/String;)Lcom/trendyol/stove/testing/grpcmock/ReceivedRequest;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/ReceivedRequest;Lcom/trendyol/stove/testing/grpcmock/StubKey;[BLio/grpc/Metadata;JZLjava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/ReceivedRequest;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAuthorizationHeader ()Ljava/lang/String;\n\tpublic final fun getBearerToken ()Ljava/lang/String;\n\tpublic final fun getMatched ()Z\n\tpublic final fun getMetadata ()Lio/grpc/Metadata;\n\tpublic final fun getRequestBytes ()[B\n\tpublic final fun getStubId ()Ljava/lang/String;\n\tpublic final fun getStubKey ()Lcom/trendyol/stove/testing/grpcmock/StubKey;\n\tpublic final fun getTimestamp ()J\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract class com/trendyol/stove/testing/grpcmock/RequestMatcher {\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/RequestMatcher$Any : com/trendyol/stove/testing/grpcmock/RequestMatcher {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$Any;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/RequestMatcher$Custom : com/trendyol/stove/testing/grpcmock/RequestMatcher {\n\tpublic fun <init> (Lkotlin/jvm/functions/Function1;)V\n\tpublic final fun component1 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$Custom;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$Custom;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$Custom;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getMatcher ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/RequestMatcher$ExactBytes : com/trendyol/stove/testing/grpcmock/RequestMatcher {\n\tpublic fun <init> ([B)V\n\tpublic final fun component1 ()[B\n\tpublic final fun copy ([B)Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$ExactBytes;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$ExactBytes;[BILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$ExactBytes;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBytes ()[B\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/RequestMatcher$ExactMessage : com/trendyol/stove/testing/grpcmock/RequestMatcher {\n\tpublic fun <init> (Lcom/google/protobuf/Message;)V\n\tpublic final fun component1 ()Lcom/google/protobuf/Message;\n\tpublic final fun copy (Lcom/google/protobuf/Message;)Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$ExactMessage;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$ExactMessage;Lcom/google/protobuf/Message;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/RequestMatcher$ExactMessage;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getMessage ()Lcom/google/protobuf/Message;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract class com/trendyol/stove/testing/grpcmock/StubDefinition {\n\tpublic abstract fun getMetadataMatcher ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic abstract fun getRequestMatcher ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/StubDefinition$BidiStream : com/trendyol/stove/testing/grpcmock/StubDefinition {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lkotlin/jvm/functions/Function2;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic final fun component2 ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic final fun component3 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun copy (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lkotlin/jvm/functions/Function2;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$BidiStream;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/StubDefinition$BidiStream;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$BidiStream;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getHandler ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getMetadataMatcher ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic fun getRequestMatcher ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/StubDefinition$ClientStream : com/trendyol/stove/testing/grpcmock/StubDefinition {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic final fun component2 ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic final fun component3 ()Lcom/google/protobuf/Message;\n\tpublic final fun copy (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$ClientStream;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/StubDefinition$ClientStream;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$ClientStream;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getMetadataMatcher ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic fun getRequestMatcher ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic final fun getResponse ()Lcom/google/protobuf/Message;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/StubDefinition$Error : com/trendyol/stove/testing/grpcmock/StubDefinition {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lio/grpc/Status;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lio/grpc/Status;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic final fun component2 ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic final fun component3 ()Lio/grpc/Status;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun copy (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lio/grpc/Status;Ljava/lang/String;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$Error;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/StubDefinition$Error;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lio/grpc/Status;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$Error;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getMessage ()Ljava/lang/String;\n\tpublic fun getMetadataMatcher ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic fun getRequestMatcher ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic final fun getStatus ()Lio/grpc/Status;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/StubDefinition$ServerStream : com/trendyol/stove/testing/grpcmock/StubDefinition {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Ljava/util/List;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic final fun component2 ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic final fun component3 ()Ljava/util/List;\n\tpublic final fun copy (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Ljava/util/List;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$ServerStream;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/StubDefinition$ServerStream;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$ServerStream;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getMetadataMatcher ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic fun getRequestMatcher ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic final fun getResponses ()Ljava/util/List;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/StubDefinition$Unary : com/trendyol/stove/testing/grpcmock/StubDefinition {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic final fun component2 ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic final fun component3 ()Lcom/google/protobuf/Message;\n\tpublic final fun copy (Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$Unary;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/StubDefinition$Unary;Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;Lcom/google/protobuf/Message;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/StubDefinition$Unary;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getMetadataMatcher ()Lcom/trendyol/stove/testing/grpcmock/MetadataMatcher;\n\tpublic fun getRequestMatcher ()Lcom/trendyol/stove/testing/grpcmock/RequestMatcher;\n\tpublic final fun getResponse ()Lcom/google/protobuf/Message;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/grpcmock/StubKey {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/testing/grpcmock/StubKey;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/grpcmock/StubKey;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/testing/grpcmock/StubKey;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getFullMethodName ()Ljava/lang/String;\n\tpublic final fun getId ()Ljava/lang/String;\n\tpublic final fun getMethodName ()Ljava/lang/String;\n\tpublic final fun getServiceName ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\n"
  },
  {
    "path": "lib/stove-grpc-mock/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(libs.io.grpc)\n  api(libs.io.grpc.stub)\n  api(libs.io.grpc.protobuf)\n  api(libs.google.protobuf.util)\n  api(libs.caffeine)\n  implementation(libs.kotlinx.core)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.logback.classic)\n  testImplementation(libs.kotest.runner.junit5)\n  testImplementation(testFixtures(projects.lib.stove))\n  testImplementation(projects.lib.stoveGrpc)\n  testImplementation(libs.io.grpc.netty)\n  testImplementation(libs.io.grpc.kotlin)\n  testImplementation(libs.google.protobuf.kotlin)\n}\n\nplugins {\n  alias(libs.plugins.protobuf)\n}\n\nprotobuf {\n  protoc {\n    artifact = libs.protoc.get().toString()\n  }\n\n  plugins {\n    create(\"grpc\").apply {\n      artifact = libs.grpc.protoc.gen.java.get().toString()\n    }\n    create(\"grpckt\").apply {\n      artifact = \"${libs.grpc.protoc.gen.kotlin.get()}:jdk8@jar\"\n    }\n  }\n\n  generateProtoTasks {\n    all().forEach { task ->\n      task.plugins {\n        create(\"grpc\")\n        create(\"grpckt\")\n      }\n      task.builtins {\n        create(\"kotlin\")\n      }\n      // Generate descriptor set for tests\n      task.generateDescriptorSet = true\n      task.descriptorSetOptions.includeImports = true\n    }\n  }\n}\n\ntasks.named(\"compileTestKotlin\") {\n  dependsOn(\"generateTestProto\")\n}\n"
  },
  {
    "path": "lib/stove-grpc-mock/src/main/kotlin/com/trendyol/stove/testing/grpcmock/GrpcMockDsl.kt",
    "content": "package com.trendyol.stove.testing.grpcmock\n\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class GrpcMockDsl\n"
  },
  {
    "path": "lib/stove-grpc-mock/src/main/kotlin/com/trendyol/stove/testing/grpcmock/GrpcMockSystem.kt",
    "content": "@file:Suppress(\"unused\", \"MagicNumber\", \"TooManyFunctions\")\n\npackage com.trendyol.stove.testing.grpcmock\n\nimport arrow.core.*\nimport com.github.benmanes.caffeine.cache.*\nimport com.google.protobuf.Message\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport io.grpc.*\nimport io.grpc.stub.ServerCalls\nimport io.grpc.stub.StreamObserver\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.*\nimport org.slf4j.LoggerFactory\nimport java.io.ByteArrayInputStream\nimport java.io.InputStream\nimport java.util.*\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.TimeUnit\n\n/**\n * Native gRPC mock server for testing gRPC service integrations.\n *\n * This implementation provides full support for all gRPC RPC types:\n * - Unary (request-response)\n * - Server streaming (single request, stream of responses)\n * - Client streaming (stream of requests, single response)\n * - Bidirectional streaming (stream of requests, stream of responses)\n *\n * ## Configuration\n *\n * ```kotlin\n * Stove()\n *   .with {\n *     grpcMock {\n *       GrpcMockSystemOptions(port = 9090)\n *     }\n *     grpc {\n *       GrpcSystemOptions(host = \"localhost\", port = 9090)\n *     }\n *   }\n * ```\n *\n * ## Mocking Unary Calls\n *\n * ```kotlin\n * stove {\n *   grpcMock {\n *     mockUnary(\n *       serviceName = \"greeting.GreeterService\",\n *       methodName = \"SayHello\",\n *       response = HelloResponse.newBuilder().setMessage(\"Hello!\").build()\n *     )\n *   }\n * }\n * ```\n *\n * ## Mocking Authenticated Calls\n *\n * ```kotlin\n * stove {\n *   grpcMock {\n *     // Require specific bearer token\n *     mockUnary(\n *       serviceName = \"secure.SecureService\",\n *       methodName = \"GetSecret\",\n *       metadataMatcher = MetadataMatcher.BearerToken(\"valid-token\"),\n *       response = SecretResponse.newBuilder().setData(\"secret\").build()\n *     )\n *\n *     // Or use custom header matching\n *     mockUnary(\n *       serviceName = \"secure.SecureService\",\n *       methodName = \"GetSecret\",\n *       metadataMatcher = MetadataMatcher.HasHeader(\"x-api-key\", \"my-api-key\"),\n *       response = SecretResponse.newBuilder().setData(\"secret\").build()\n *     )\n *   }\n * }\n * ```\n */\n@GrpcMockDsl\nclass GrpcMockSystem internal constructor(\n  override val stove: Stove,\n  private val ctx: GrpcMockContext\n) : PluggedSystem,\n  ValidatedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  private val logger = LoggerFactory.getLogger(javaClass)\n  override val reportSystemName: String = \"gRPC Mock\" + (ctx.keyName?.let { \" [$it]\" } ?: \"\")\n\n  private val stubs = ConcurrentHashMap<String, MutableList<Pair<StubKey, StubDefinition>>>()\n  private val requestLog: Cache<String, ReceivedRequest> = Caffeine\n    .newBuilder()\n    .maximumSize(10_000)\n    .build()\n\n  private lateinit var server: Server\n  private lateinit var exposedConfiguration: GrpcMockExposedConfiguration\n\n  override fun configuration(): List<String> = ctx.configureExposedConfiguration(exposedConfiguration)\n\n  // ==================== Lifecycle ====================\n\n  override suspend fun run() {\n    server = ctx\n      .serverBuilder(ServerBuilder.forPort(ctx.port))\n      .intercept(MetadataCapturingInterceptor)\n      .fallbackHandlerRegistry(DynamicHandlerRegistry())\n      .build()\n      .also { it.start() }\n\n    exposedConfiguration = GrpcMockExposedConfiguration(\n      host = \"localhost\",\n      port = server.port\n    )\n    logger.info(\"gRPC Mock server started on port ${server.port}\")\n  }\n\n  override suspend fun stop() {\n    if (::server.isInitialized) {\n      server.shutdown().awaitTermination(5, TimeUnit.SECONDS)\n      logger.info(\"gRPC Mock server stopped\")\n    }\n  }\n\n  override fun close(): Unit = runBlocking {\n    Try { stop() }.recover { logger.warn(\"Error stopping gRPC mock: ${it.message}\") }\n  }\n\n  // ==================== Stub Registration ====================\n\n  /**\n   * Mocks a unary RPC (single request → single response).\n   *\n   * @param serviceName The fully qualified gRPC service name (e.g., \"greeting.GreeterService\")\n   * @param methodName The RPC method name (e.g., \"SayHello\")\n   * @param requestMatcher Optional matcher for filtering requests. Defaults to [RequestMatcher.Any].\n   * @param metadataMatcher Optional matcher for filtering by gRPC metadata/headers. Defaults to [MetadataMatcher.Any].\n   *   Use [MetadataMatcher.BearerToken] for Bearer token auth, [MetadataMatcher.HasHeader] for custom headers.\n   * @param response The protobuf [Message] to return when this stub is matched\n   * @return This [GrpcMockSystem] instance for chaining\n   *\n   * @sample\n   * ```kotlin\n   * grpcMock {\n   *   mockUnary(\n   *     serviceName = \"greeting.GreeterService\",\n   *     methodName = \"SayHello\",\n   *     response = HelloResponse.newBuilder().setMessage(\"Hello!\").build()\n   *   )\n   * }\n   * ```\n   */\n  suspend fun mockUnary(\n    serviceName: String,\n    methodName: String,\n    requestMatcher: RequestMatcher = RequestMatcher.Any,\n    metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    response: Message\n  ): GrpcMockSystem = registerStub(\n    serviceName,\n    methodName,\n    StubDefinition.Unary(requestMatcher, metadataMatcher, response),\n    \"unary\"\n  ) { response.toString().take(200).some() }\n\n  /**\n   * Mocks a server streaming RPC (single request → stream of responses).\n   *\n   * The mock will send all responses in sequence when a matching request is received.\n   *\n   * @param serviceName The fully qualified gRPC service name (e.g., \"streaming.ItemService\")\n   * @param methodName The RPC method name (e.g., \"ListItems\")\n   * @param requestMatcher Optional matcher for filtering requests. Defaults to [RequestMatcher.Any].\n   * @param metadataMatcher Optional matcher for filtering by gRPC metadata/headers. Defaults to [MetadataMatcher.Any].\n   * @param responses The list of protobuf [Message]s to stream back. Must not be empty.\n   * @return This [GrpcMockSystem] instance for chaining\n   * @throws IllegalArgumentException if [responses] is empty\n   *\n   * @sample\n   * ```kotlin\n   * grpcMock {\n   *   mockServerStream(\n   *     serviceName = \"streaming.ItemService\",\n   *     methodName = \"ListItems\",\n   *     responses = listOf(\n   *       Item.newBuilder().setId(\"1\").build(),\n   *       Item.newBuilder().setId(\"2\").build()\n   *     )\n   *   )\n   * }\n   * ```\n   */\n  suspend fun mockServerStream(\n    serviceName: String,\n    methodName: String,\n    requestMatcher: RequestMatcher = RequestMatcher.Any,\n    metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    responses: List<Message>\n  ): GrpcMockSystem {\n    require(responses.isNotEmpty()) { \"responses must not be empty\" }\n    return registerStub(\n      serviceName,\n      methodName,\n      StubDefinition.ServerStream(requestMatcher, metadataMatcher, responses),\n      \"server stream\",\n      metadata = mapOf(\"responseCount\" to responses.size)\n    )\n  }\n\n  /**\n   * Mocks a client streaming RPC (stream of requests → single response).\n   *\n   * The mock will collect all incoming requests and return the configured response\n   * when the client completes the stream.\n   *\n   * **Note:** The [requestMatcher] is evaluated against **only the first request** in the stream,\n   * because stub matching happens before the full stream is received. If you need to validate\n   * all requests in a client stream, consider using [mockBidiStream] with a custom handler.\n   *\n   * @param serviceName The fully qualified gRPC service name (e.g., \"upload.UploadService\")\n   * @param methodName The RPC method name (e.g., \"UploadChunks\")\n   * @param requestMatcher Optional matcher for the first request in the stream. Defaults to [RequestMatcher.Any].\n   * @param metadataMatcher Optional matcher for filtering by gRPC metadata/headers. Defaults to [MetadataMatcher.Any].\n   * @param response The protobuf [Message] to return after the client completes streaming\n   * @return This [GrpcMockSystem] instance for chaining\n   *\n   * @sample\n   * ```kotlin\n   * grpcMock {\n   *   mockClientStream(\n   *     serviceName = \"upload.UploadService\",\n   *     methodName = \"UploadChunks\",\n   *     response = UploadResponse.newBuilder().setSuccess(true).build()\n   *   )\n   * }\n   * ```\n   */\n  suspend fun mockClientStream(\n    serviceName: String,\n    methodName: String,\n    requestMatcher: RequestMatcher = RequestMatcher.Any,\n    metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    response: Message\n  ): GrpcMockSystem = registerStub(\n    serviceName,\n    methodName,\n    StubDefinition.ClientStream(requestMatcher, metadataMatcher, response),\n    \"client stream\"\n  ) { response.toString().take(200).some() }\n\n  /**\n   * Mocks a bidirectional streaming RPC (stream of requests ↔ stream of responses).\n   *\n   * The [handler] receives a flow of raw request bytes and should return a flow of response messages.\n   * This allows full control over the streaming behavior, including transforming requests into responses.\n   *\n   * @param serviceName The fully qualified gRPC service name (e.g., \"chat.ChatService\")\n   * @param methodName The RPC method name (e.g., \"Chat\")\n   * @param requestMatcher Optional matcher. Currently not used for bidi streams as matching happens dynamically.\n   * @param metadataMatcher Optional matcher for filtering by gRPC metadata/headers. Defaults to [MetadataMatcher.Any].\n   * @param handler A suspending function that transforms the incoming request flow into a response flow.\n   *   The handler receives raw [ByteArray] request bytes which can be parsed using protobuf's `parseFrom`.\n   * @return This [GrpcMockSystem] instance for chaining\n   *\n   * @sample\n   * ```kotlin\n   * grpcMock {\n   *   mockBidiStream(\n   *     serviceName = \"chat.ChatService\",\n   *     methodName = \"Chat\"\n   *   ) { requestFlow ->\n   *     requestFlow.map { bytes ->\n   *       val request = ChatMessage.parseFrom(bytes)\n   *       ChatMessage.newBuilder()\n   *         .setMessage(\"Echo: ${request.message}\")\n   *         .build()\n   *     }\n   *   }\n   * }\n   * ```\n   */\n  suspend fun mockBidiStream(\n    serviceName: String,\n    methodName: String,\n    requestMatcher: RequestMatcher = RequestMatcher.Any,\n    metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    handler: suspend (Flow<ByteArray>) -> Flow<Message>\n  ): GrpcMockSystem = registerStub(\n    serviceName,\n    methodName,\n    StubDefinition.BidiStream(requestMatcher, metadataMatcher, handler),\n    \"bidi stream\"\n  )\n\n  /**\n   * Mocks a gRPC error response for any RPC type.\n   *\n   * When a matching request is received, the mock will respond with the specified gRPC error status.\n   *\n   * @param serviceName The fully qualified gRPC service name (e.g., \"users.UserService\")\n   * @param methodName The RPC method name (e.g., \"GetUser\")\n   * @param requestMatcher Optional matcher for filtering requests. Defaults to [RequestMatcher.Any].\n   * @param metadataMatcher Optional matcher for filtering by gRPC metadata/headers. Defaults to [MetadataMatcher.Any].\n   * @param status The gRPC [Status.Code] to return (e.g., NOT_FOUND, UNAUTHENTICATED, PERMISSION_DENIED)\n   * @param message Optional error message description. Defaults to the status code name.\n   * @return This [GrpcMockSystem] instance for chaining\n   *\n   * @sample\n   * ```kotlin\n   * grpcMock {\n   *   mockError(\n   *     serviceName = \"users.UserService\",\n   *     methodName = \"GetUser\",\n   *     status = Status.Code.NOT_FOUND,\n   *     message = \"User not found\"\n   *   )\n   * }\n   * ```\n   */\n  suspend fun mockError(\n    serviceName: String,\n    methodName: String,\n    requestMatcher: RequestMatcher = RequestMatcher.Any,\n    metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    status: Status.Code,\n    message: String = status.name\n  ): GrpcMockSystem = registerStub(\n    serviceName,\n    methodName,\n    StubDefinition.Error(requestMatcher, metadataMatcher, Status.fromCode(status), message),\n    \"error\",\n    metadata = mapOf(\"status\" to status.name, \"message\" to message)\n  )\n\n  // ==================== Validation & Reporting ====================\n\n  override suspend fun validate() {\n    val unmatched = requestLog.asMap().values.filter { !it.matched }\n\n    if (unmatched.isNotEmpty()) {\n      val error = AssertionError(\n        \"There are ${unmatched.size} unmatched gRPC requests:\\n\" +\n          unmatched.joinToString(\"\\n\") { \"  - ${it.stubKey.fullMethodName}\" }\n      )\n      reporter.record(\n        ReportEntry.failure(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = \"Validate: All gRPC requests should match registered stubs\",\n          error = error.message.orEmpty(),\n          expected = \"0 unmatched requests\".some(),\n          actual = \"${unmatched.size} unmatched request(s)\".some()\n        )\n      )\n      throw error\n    }\n\n    reporter.record(\n      ReportEntry.success(\n        system = reportSystemName,\n        testId = reporter.currentTestId(),\n        action = \"Validate: All gRPC requests matched registered stubs\"\n      )\n    )\n  }\n\n  override fun then(): Stove = stove\n\n  override fun snapshot(): SystemSnapshot {\n    val allStubs = stubs.values.flatten()\n    val allRequests = requestLog.asMap().values\n\n    return SystemSnapshot(\n      system = reportSystemName,\n      state = mapOf(\n        \"registeredStubs\" to allStubs.map { (key, def) ->\n          mapOf(\"id\" to key.id, \"service\" to key.serviceName, \"method\" to key.methodName, \"type\" to def::class.simpleName)\n        },\n        \"receivedRequests\" to allRequests.map { req ->\n          mapOf(\n            \"method\" to req.stubKey.fullMethodName,\n            \"matched\" to req.matched,\n            \"timestamp\" to req.timestamp,\n            \"hasAuth\" to (req.authorizationHeader != null)\n          )\n        }\n      ),\n      summary = \"\"\"\n        |Registered stubs: ${allStubs.size}\n        |Received requests: ${allRequests.size}\n        |Matched requests: ${allRequests.count { it.matched }}\n        |Unmatched requests: ${allRequests.count { !it.matched }}\n        |Authenticated requests: ${allRequests.count { it.authorizationHeader != null }}\n      \"\"\".trimMargin()\n    )\n  }\n\n  // ==================== Internal: Registration ====================\n\n  private suspend fun registerStub(\n    serviceName: String,\n    methodName: String,\n    stub: StubDefinition,\n    stubType: String,\n    metadata: Map<String, Any> = emptyMap(),\n    outputProvider: () -> Option<String> = { None }\n  ): GrpcMockSystem {\n    val key = StubKey(serviceName, methodName)\n    val authInfo = when (val matcher = stub.metadataMatcher) {\n      is MetadataMatcher.BearerToken -> \" (authenticated)\"\n      is MetadataMatcher.RequiresAuth -> \" (requires auth)\"\n      is MetadataMatcher.HasHeader -> \" (header: ${matcher.key})\"\n      else -> \"\"\n    }\n    report(\n      action = \"Register $stubType stub: $serviceName/$methodName$authInfo\",\n      output = outputProvider(),\n      metadata = metadata\n    ) {\n      stubs.computeIfAbsent(key.fullMethodName) { mutableListOf() }.add(key to stub)\n      logger.debug(\"Registered stub for ${key.fullMethodName} (id: ${key.id})\")\n    }\n    return this\n  }\n\n  // ==================== Internal: Stub Lookup ====================\n\n  private fun findAndProcessStub(\n    fullMethodName: String,\n    requestBytes: ByteArray,\n    metadata: Metadata\n  ): Option<Pair<StubKey, StubDefinition>> {\n    val stubKey = fullMethodName.toStubKey()\n    ctx.onRequestReceived(stubKey, requestBytes)\n\n    return stubs[fullMethodName]\n      ?.find { (_, stub) ->\n        stub.requestMatcher.matches(requestBytes) && stub.metadataMatcher.matches(metadata)\n      }?.also { (key, stub) ->\n        logRequest(key, requestBytes, metadata, matched = true)\n        removeStubIfNeeded(key, fullMethodName)\n        ctx.afterStubMatched(key, stub)\n      }.toOption()\n      .onNone { logRequest(stubKey, requestBytes, metadata, matched = false) }\n  }\n\n  private fun removeStubIfNeeded(key: StubKey, fullMethodName: String) {\n    if (ctx.removeStubAfterRequestMatched) {\n      stubs[fullMethodName]?.removeIf { it.first.id == key.id }\n      logger.debug(\"Removed stub ${key.id} after match\")\n    }\n  }\n\n  private fun logRequest(key: StubKey, requestBytes: ByteArray, metadata: Metadata, matched: Boolean) {\n    requestLog.put(\n      UUID.randomUUID().toString(),\n      ReceivedRequest(key, requestBytes, metadata, matched = matched, stubId = if (matched) key.id else null)\n    )\n  }\n\n  // ==================== Internal: Handler Registry ====================\n\n  private inner class DynamicHandlerRegistry : HandlerRegistry() {\n    override fun lookupMethod(methodName: String, authority: String?): ServerMethodDefinition<*, *>? {\n      logger.debug(\"Looking up method: $methodName\")\n      return stubs[methodName]?.firstOrNull()?.let { (_, stub) ->\n        createHandler(methodName, stub.methodType)\n      } ?: run {\n        logger.warn(\"No stub registered for method: $methodName\")\n        null\n      }\n    }\n  }\n\n  private fun createHandler(\n    fullMethodName: String,\n    methodType: MethodDescriptor.MethodType\n  ): ServerMethodDefinition<ByteArray, ByteArray> {\n    val method = MethodDescriptor\n      .newBuilder<ByteArray, ByteArray>()\n      .setType(methodType)\n      .setFullMethodName(fullMethodName)\n      .setRequestMarshaller(ByteArrayMarshaller)\n      .setResponseMarshaller(ByteArrayMarshaller)\n      .build()\n\n    val handler = when (methodType) {\n      MethodDescriptor.MethodType.UNARY -> unaryHandler(fullMethodName)\n      MethodDescriptor.MethodType.SERVER_STREAMING -> serverStreamHandler(fullMethodName)\n      MethodDescriptor.MethodType.CLIENT_STREAMING -> clientStreamHandler(fullMethodName)\n      MethodDescriptor.MethodType.BIDI_STREAMING -> bidiStreamHandler(fullMethodName)\n      else -> unaryHandler(fullMethodName)\n    }\n\n    return ServerMethodDefinition.create(method, handler)\n  }\n\n  // ==================== Internal: Call Handlers ====================\n\n  private fun unaryHandler(fullMethodName: String): ServerCallHandler<ByteArray, ByteArray> =\n    ServerCalls.asyncUnaryCall { request, observer ->\n      val metadata = MetadataCapturingInterceptor.currentMetadata()\n      findAndProcessStub(fullMethodName, request, metadata).fold(\n        ifEmpty = { observer.sendUnimplemented(fullMethodName) },\n        ifSome = { (_, stub) -> stub.sendResponse(observer) }\n      )\n    }\n\n  private fun serverStreamHandler(fullMethodName: String): ServerCallHandler<ByteArray, ByteArray> =\n    ServerCalls.asyncServerStreamingCall { request, observer ->\n      val metadata = MetadataCapturingInterceptor.currentMetadata()\n      findAndProcessStub(fullMethodName, request, metadata).fold(\n        ifEmpty = { observer.sendUnimplemented(fullMethodName) },\n        ifSome = { (_, stub) -> stub.sendResponse(observer) }\n      )\n    }\n\n  private fun clientStreamHandler(fullMethodName: String): ServerCallHandler<ByteArray, ByteArray> =\n    ServerCalls.asyncClientStreamingCall { responseObserver ->\n      val metadata = MetadataCapturingInterceptor.currentMetadata()\n      CollectingStreamObserver(\n        onComplete = { requests ->\n          val requestBytes = requests.firstOrNull() ?: ByteArray(0)\n          findAndProcessStub(fullMethodName, requestBytes, metadata).fold(\n            ifEmpty = { responseObserver.sendUnimplemented(fullMethodName) },\n            ifSome = { (_, stub) -> stub.sendResponse(responseObserver) }\n          )\n        },\n        onStreamError = { responseObserver.onError(it) }\n      )\n    }\n\n  private fun bidiStreamHandler(fullMethodName: String): ServerCallHandler<ByteArray, ByteArray> =\n    ServerCalls.asyncBidiStreamingCall { responseObserver ->\n      val metadata = MetadataCapturingInterceptor.currentMetadata()\n      val requestChannel = Channel<ByteArray>(Channel.UNLIMITED)\n\n      CoroutineScope(Dispatchers.IO).launch {\n        stubs[fullMethodName]?.find { (_, stub) -> stub.metadataMatcher.matches(metadata) }?.let { (_, stub) ->\n          when (stub) {\n            is StubDefinition.BidiStream -> runCatching {\n              stub.handler(requestChannel.consumeAsFlow()).collect { response ->\n                responseObserver.onNext(response.toByteArray())\n              }\n              responseObserver.onCompleted()\n            }.onFailure { e ->\n              responseObserver.onError(Status.INTERNAL.withCause(e).asException())\n            }\n\n            is StubDefinition.Error -> responseObserver.onError(stub.toStatusException())\n\n            else -> responseObserver.sendUnexpectedStubType(\"bidi stream\")\n          }\n        } ?: responseObserver.sendUnimplemented(fullMethodName)\n      }\n\n      ChannelForwardingObserver(requestChannel, CoroutineScope(Dispatchers.IO))\n    }\n\n  // ==================== Extension Functions ====================\n\n  private val StubDefinition.methodType: MethodDescriptor.MethodType\n    get() = when (this) {\n      is StubDefinition.Unary, is StubDefinition.Error -> MethodDescriptor.MethodType.UNARY\n      is StubDefinition.ServerStream -> MethodDescriptor.MethodType.SERVER_STREAMING\n      is StubDefinition.ClientStream -> MethodDescriptor.MethodType.CLIENT_STREAMING\n      is StubDefinition.BidiStream -> MethodDescriptor.MethodType.BIDI_STREAMING\n    }\n\n  private fun RequestMatcher.matches(requestBytes: ByteArray): Boolean = when (this) {\n    is RequestMatcher.Any -> true\n    is RequestMatcher.ExactBytes -> requestBytes.contentEquals(bytes)\n    is RequestMatcher.ExactMessage -> requestBytes.contentEquals(message.toByteArray())\n    is RequestMatcher.Custom -> matcher(requestBytes)\n  }\n\n  private fun MetadataMatcher.matches(metadata: Metadata): Boolean = when (this) {\n    is MetadataMatcher.Any -> {\n      true\n    }\n\n    is MetadataMatcher.HasHeader -> {\n      val headerKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER)\n      metadata.get(headerKey) == value\n    }\n\n    is MetadataMatcher.BearerToken -> {\n      val auth = metadata.get(GrpcMetadataKeys.AUTHORIZATION)\n      auth == \"Bearer $token\"\n    }\n\n    is MetadataMatcher.RequiresAuth -> {\n      val auth = metadata.get(GrpcMetadataKeys.AUTHORIZATION)\n      !auth.isNullOrBlank()\n    }\n\n    is MetadataMatcher.Custom -> {\n      matcher(metadata)\n    }\n\n    is MetadataMatcher.All -> {\n      matchers.all { it.matches(metadata) }\n    }\n  }\n\n  private fun StubDefinition.sendResponse(observer: StreamObserver<ByteArray>) {\n    when (this) {\n      is StubDefinition.Unary -> observer.sendSingleAndComplete(response.toByteArray())\n      is StubDefinition.ServerStream -> observer.sendAllAndComplete(responses.map { it.toByteArray() })\n      is StubDefinition.ClientStream -> observer.sendSingleAndComplete(response.toByteArray())\n      is StubDefinition.Error -> observer.onError(toStatusException())\n      is StubDefinition.BidiStream -> observer.sendUnexpectedStubType(\"non-bidi call\")\n    }\n  }\n\n  private fun StubDefinition.Error.toStatusException(): StatusException =\n    (message?.let { status.withDescription(it) } ?: status).asException()\n\n  private fun StreamObserver<ByteArray>.sendSingleAndComplete(bytes: ByteArray) {\n    onNext(bytes)\n    onCompleted()\n  }\n\n  private fun StreamObserver<ByteArray>.sendAllAndComplete(bytesList: List<ByteArray>) {\n    bytesList.forEach { onNext(it) }\n    onCompleted()\n  }\n\n  private fun StreamObserver<*>.sendUnimplemented(fullMethodName: String) {\n    onError(Status.UNIMPLEMENTED.withDescription(\"No matching stub for $fullMethodName\").asException())\n  }\n\n  private fun StreamObserver<*>.sendUnexpectedStubType(context: String) {\n    onError(Status.INTERNAL.withDescription(\"Unexpected stub type for $context\").asException())\n  }\n\n  private fun String.toStubKey(): StubKey {\n    val parts = split(\"/\")\n    require(parts.size >= 2 && parts[0].isNotBlank() && parts[1].isNotBlank()) {\n      \"Invalid gRPC method name format: '$this'. Expected format: 'serviceName/methodName'\"\n    }\n    return StubKey(parts[0], parts[1])\n  }\n\n  // ==================== Helper Classes ====================\n\n  private class CollectingStreamObserver(\n    private val onComplete: (List<ByteArray>) -> Unit,\n    private val onStreamError: (Throwable) -> Unit\n  ) : StreamObserver<ByteArray> {\n    private val collected = mutableListOf<ByteArray>()\n\n    override fun onNext(value: ByteArray) {\n      collected.add(value)\n    }\n\n    override fun onError(t: Throwable) {\n      onStreamError(t)\n    }\n\n    override fun onCompleted() {\n      onComplete(collected)\n    }\n  }\n\n  private class ChannelForwardingObserver(\n    private val channel: Channel<ByteArray>,\n    private val scope: CoroutineScope\n  ) : StreamObserver<ByteArray> {\n    override fun onNext(value: ByteArray) {\n      // Use trySend for non-blocking operation, falling back to coroutine launch\n      // if the channel buffer is full (which shouldn't happen with UNLIMITED capacity)\n      val result = channel.trySend(value)\n      if (result.isFailure && !result.isClosed) {\n        scope.launch { channel.send(value) }\n      }\n    }\n\n    override fun onError(t: Throwable) {\n      channel.close(t)\n    }\n\n    override fun onCompleted() {\n      channel.close()\n    }\n  }\n\n  companion object {\n    fun GrpcMockSystem.server(): Server = server\n  }\n}\n\n/**\n * Interceptor that captures request metadata and makes it available via Context.\n */\nprivate object MetadataCapturingInterceptor : ServerInterceptor {\n  private val METADATA_KEY: Context.Key<Metadata> = Context.key(\"captured-metadata\")\n\n  fun currentMetadata(): Metadata = METADATA_KEY.get() ?: Metadata()\n\n  override fun <ReqT, RespT> interceptCall(\n    call: ServerCall<ReqT, RespT>,\n    headers: Metadata,\n    next: ServerCallHandler<ReqT, RespT>\n  ): ServerCall.Listener<ReqT> {\n    val context = Context.current().withValue(METADATA_KEY, headers)\n    return Contexts.interceptCall(context, call, headers, next)\n  }\n}\n\nprivate object ByteArrayMarshaller : MethodDescriptor.Marshaller<ByteArray> {\n  override fun stream(value: ByteArray): InputStream = ByteArrayInputStream(value)\n\n  override fun parse(stream: InputStream): ByteArray = stream.readBytes()\n}\n"
  },
  {
    "path": "lib/stove-grpc-mock/src/main/kotlin/com/trendyol/stove/testing/grpcmock/GrpcMockSystemOptions.kt",
    "content": "package com.trendyol.stove.testing.grpcmock\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport io.grpc.ServerBuilder\n\n/**\n * Callback invoked after a stub is matched and used.\n */\ntypealias AfterStubMatched = (StubKey, StubDefinition) -> Unit\n\n/**\n * Callback invoked for each request received.\n */\ntypealias OnRequestReceived = (StubKey, ByteArray) -> Unit\n\n/**\n * Configuration exposed by gRPC Mock after it starts.\n *\n * This allows the application under test to receive the actual gRPC mock URL,\n * which is especially useful when using dynamic ports (port = 0).\n *\n * @property host The host where gRPC mock is running.\n * @property port The actual port gRPC mock is listening on.\n */\ndata class GrpcMockExposedConfiguration(\n  val host: String,\n  val port: Int\n) : ExposedConfiguration\n\n/**\n * Configuration options for the native gRPC mock server.\n *\n * @property port The port to run the mock server on.\n *   Defaults to 0, which lets the system pick an available port automatically.\n *   This avoids port conflicts, especially in CI environments.\n * @property removeStubAfterRequestMatched If true, stubs are removed after being matched once.\n * @property afterStubMatched Callback invoked after a stub is matched.\n * @property onRequestReceived Callback invoked for each request received.\n * @property serverBuilder Optional custom server builder configuration.\n * @property configureExposedConfiguration Callback to expose the gRPC mock configuration to the application.\n */\ndata class GrpcMockSystemOptions(\n  val port: Int = 0,\n  val removeStubAfterRequestMatched: Boolean = false,\n  val afterStubMatched: AfterStubMatched = { _, _ -> },\n  val onRequestReceived: OnRequestReceived = { _, _ -> },\n  val serverBuilder: (ServerBuilder<*>) -> ServerBuilder<*> = { it },\n  override val configureExposedConfiguration: (GrpcMockExposedConfiguration) -> List<String> = { _ -> listOf() }\n) : SystemOptions,\n  ConfiguresExposedConfiguration<GrpcMockExposedConfiguration>\n\n/**\n * Internal context for the gRPC mock system.\n */\ninternal data class GrpcMockContext(\n  val port: Int,\n  val removeStubAfterRequestMatched: Boolean,\n  val afterStubMatched: AfterStubMatched,\n  val onRequestReceived: OnRequestReceived,\n  val serverBuilder: (ServerBuilder<*>) -> ServerBuilder<*>,\n  val configureExposedConfiguration: (GrpcMockExposedConfiguration) -> List<String>,\n  val keyName: String? = null\n)\n\ninternal fun Stove.withGrpcMock(options: GrpcMockSystemOptions): Stove =\n  GrpcMockSystem(\n    stove = this,\n    GrpcMockContext(\n      options.port,\n      options.removeStubAfterRequestMatched,\n      options.afterStubMatched,\n      options.onRequestReceived,\n      options.serverBuilder,\n      options.configureExposedConfiguration\n    )\n  ).also { getOrRegister(it) }\n    .let { this }\n\ninternal fun Stove.grpcMock(): GrpcMockSystem = getOrNone<GrpcMockSystem>().getOrElse {\n  throw SystemNotRegisteredException(GrpcMockSystem::class)\n}\n\ninternal fun Stove.withGrpcMock(key: SystemKey, options: GrpcMockSystemOptions): Stove =\n  GrpcMockSystem(\n    stove = this,\n    GrpcMockContext(\n      options.port,\n      options.removeStubAfterRequestMatched,\n      options.afterStubMatched,\n      options.onRequestReceived,\n      options.serverBuilder,\n      options.configureExposedConfiguration,\n      keyName = keyDisplayName(key)\n    )\n  ).also { getOrRegister(key, it) }\n    .let { this }\n\ninternal fun Stove.grpcMock(key: SystemKey): GrpcMockSystem = getOrNone<GrpcMockSystem>(key).getOrElse {\n  throw SystemNotRegisteredException(GrpcMockSystem::class, \"No GrpcMockSystem registered with key '${keyDisplayName(key)}'\")\n}\n\n/**\n * Registers the native gRPC mock system with Stove.\n *\n * ```kotlin\n * Stove()\n *   .with {\n *     grpcMock {\n *       GrpcMockSystemOptions(port = 9090)\n *     }\n *   }\n * ```\n */\nfun WithDsl.grpcMock(configure: @StoveDsl () -> GrpcMockSystemOptions): Stove =\n  this.stove.withGrpcMock(configure())\n\n/**\n * Registers a keyed gRPC mock system with Stove.\n */\nfun WithDsl.grpcMock(key: SystemKey, configure: @StoveDsl () -> GrpcMockSystemOptions): Stove =\n  this.stove.withGrpcMock(key, configure())\n\n/**\n * Access the gRPC mock system for stub configuration in tests.\n *\n * ```kotlin\n * stove {\n *   grpcMock {\n *     mockUnary(\n *       serviceName = \"greeting.GreeterService\",\n *       methodName = \"SayHello\",\n *       response = HelloResponse.newBuilder().setMessage(\"Hello!\").build()\n *     )\n *   }\n * }\n * ```\n */\nsuspend fun ValidationDsl.grpcMock(\n  validation: @GrpcMockDsl suspend GrpcMockSystem.() -> Unit\n): Unit = validation(stove.grpcMock())\n\n/**\n * Access a keyed gRPC mock system for stub configuration in tests.\n */\nsuspend fun ValidationDsl.grpcMock(\n  key: SystemKey,\n  validation: @GrpcMockDsl suspend GrpcMockSystem.() -> Unit\n): Unit = validation(stove.grpcMock(key))\n"
  },
  {
    "path": "lib/stove-grpc-mock/src/main/kotlin/com/trendyol/stove/testing/grpcmock/StubDefinition.kt",
    "content": "package com.trendyol.stove.testing.grpcmock\n\nimport com.google.protobuf.Message\nimport io.grpc.Metadata\nimport io.grpc.Status\nimport kotlinx.coroutines.flow.Flow\n\n/**\n * Key for identifying a stub in the registry.\n */\ndata class StubKey(\n  val serviceName: String,\n  val methodName: String,\n  val id: String = java.util.UUID\n    .randomUUID()\n    .toString()\n) {\n  val fullMethodName: String = \"$serviceName/$methodName\"\n}\n\n/**\n * Matcher for incoming requests.\n */\nsealed class RequestMatcher {\n  /** Matches any request */\n  data object Any : RequestMatcher()\n\n  /** Matches requests with exact message content */\n  data class ExactMessage(\n    val message: Message\n  ) : RequestMatcher()\n\n  /** Matches requests with exact byte content */\n  data class ExactBytes(\n    val bytes: ByteArray\n  ) : RequestMatcher() {\n    override fun equals(other: kotlin.Any?): Boolean {\n      if (this === other) return true\n      if (other !is ExactBytes) return false\n      return bytes.contentEquals(other.bytes)\n    }\n\n    override fun hashCode(): Int = bytes.contentHashCode()\n  }\n\n  /** Custom matcher function */\n  data class Custom(\n    val matcher: (ByteArray) -> Boolean\n  ) : RequestMatcher()\n}\n\n/**\n * Matcher for request metadata (headers).\n */\nsealed class MetadataMatcher {\n  /** Matches any metadata (no restrictions) */\n  data object Any : MetadataMatcher()\n\n  /** Requires specific header to be present with exact value */\n  data class HasHeader(\n    val key: String,\n    val value: String\n  ) : MetadataMatcher()\n\n  /** Requires Authorization header with specific Bearer token */\n  data class BearerToken(\n    val token: String\n  ) : MetadataMatcher()\n\n  /** Requires any valid Authorization header (non-empty) */\n  data object RequiresAuth : MetadataMatcher()\n\n  /** Custom metadata matcher */\n  data class Custom(\n    val matcher: (Metadata) -> Boolean\n  ) : MetadataMatcher()\n\n  /** Combines multiple matchers (all must match) */\n  data class All(\n    val matchers: List<MetadataMatcher>\n  ) : MetadataMatcher() {\n    constructor(vararg matchers: MetadataMatcher) : this(matchers.toList())\n  }\n}\n\n/**\n * Definition of a stub response.\n */\nsealed class StubDefinition {\n  abstract val requestMatcher: RequestMatcher\n  abstract val metadataMatcher: MetadataMatcher\n\n  /**\n   * Unary RPC: single request -> single response\n   */\n  data class Unary(\n    override val requestMatcher: RequestMatcher = RequestMatcher.Any,\n    override val metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    val response: Message\n  ) : StubDefinition()\n\n  /**\n   * Server streaming RPC: single request -> stream of responses\n   */\n  data class ServerStream(\n    override val requestMatcher: RequestMatcher = RequestMatcher.Any,\n    override val metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    val responses: List<Message>\n  ) : StubDefinition()\n\n  /**\n   * Client streaming RPC: stream of requests -> single response\n   */\n  data class ClientStream(\n    override val requestMatcher: RequestMatcher = RequestMatcher.Any,\n    override val metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    val response: Message\n  ) : StubDefinition()\n\n  /**\n   * Bidirectional streaming RPC: stream of requests <-> stream of responses\n   * The handler receives a flow of request bytes and returns a flow of response messages.\n   */\n  data class BidiStream(\n    override val requestMatcher: RequestMatcher = RequestMatcher.Any,\n    override val metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    val handler: suspend (Flow<ByteArray>) -> Flow<Message>\n  ) : StubDefinition()\n\n  /**\n   * Error response for any RPC type\n   */\n  data class Error(\n    override val requestMatcher: RequestMatcher = RequestMatcher.Any,\n    override val metadataMatcher: MetadataMatcher = MetadataMatcher.Any,\n    val status: Status,\n    val message: String? = null\n  ) : StubDefinition()\n}\n\n/**\n * Record of a request that was received by the mock server.\n */\ndata class ReceivedRequest(\n  val stubKey: StubKey,\n  val requestBytes: ByteArray,\n  val metadata: Metadata = Metadata(),\n  val timestamp: Long = System.currentTimeMillis(),\n  val matched: Boolean,\n  val stubId: String? = null\n) {\n  /** Get authorization header value if present */\n  val authorizationHeader: String?\n    get() = metadata.get(Metadata.Key.of(\"authorization\", Metadata.ASCII_STRING_MARSHALLER))\n\n  /** Get bearer token if present (strips \"Bearer \" prefix) */\n  val bearerToken: String?\n    get() = authorizationHeader?.takeIf { it.startsWith(\"Bearer \") }?.removePrefix(\"Bearer \")?.trim()\n\n  override fun equals(other: Any?): Boolean {\n    if (this === other) return true\n    if (other !is ReceivedRequest) return false\n    return stubKey == other.stubKey &&\n      requestBytes.contentEquals(other.requestBytes) &&\n      timestamp == other.timestamp &&\n      matched == other.matched\n  }\n\n  override fun hashCode(): Int {\n    var result = stubKey.hashCode()\n    result = 31 * result + requestBytes.contentHashCode()\n    result = 31 * result + timestamp.hashCode()\n    result = 31 * result + matched.hashCode()\n    return result\n  }\n}\n\n/**\n * Common metadata keys for gRPC.\n */\nobject GrpcMetadataKeys {\n  val AUTHORIZATION: Metadata.Key<String> = Metadata.Key.of(\"authorization\", Metadata.ASCII_STRING_MARSHALLER)\n  val CONTENT_TYPE: Metadata.Key<String> = Metadata.Key.of(\"content-type\", Metadata.ASCII_STRING_MARSHALLER)\n\n  /** Create a custom ASCII metadata key */\n  fun ascii(name: String): Metadata.Key<String> = Metadata.Key.of(name, Metadata.ASCII_STRING_MARSHALLER)\n\n  /** Create a custom binary metadata key */\n  fun binary(name: String): Metadata.Key<ByteArray> = Metadata.Key.of(\"$name-bin\", Metadata.BINARY_BYTE_MARSHALLER)\n}\n"
  },
  {
    "path": "lib/stove-grpc-mock/src/test/kotlin/com/trendyol/stove/testing/grpcmock/GrpcMockSystemTest.kt",
    "content": "package com.trendyol.stove.testing.grpcmock\n\nimport com.trendyol.stove.grpc.grpc\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.testing.grpcmock.test.*\nimport io.grpc.*\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.flow.*\n\n/**\n * Tests for the native gRPC mock system.\n */\nclass GrpcMockSystemTest :\n  FunSpec({\n    context(\"Unary RPC\") {\n      test(\"should mock unary call and receive response\") {\n        stove {\n          grpcMock {\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              response = TestResponse\n                .newBuilder()\n                .setMessage(\"Hello from mock!\")\n                .setCount(42)\n                .build()\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val request = testRequest {\n                message = \"Hello\"\n                count = 1\n              }\n              val response = unary(request)\n              response.message shouldBe \"Hello from mock!\"\n              response.count shouldBe 42\n            }\n          }\n        }\n      }\n\n      test(\"should mock unary call with request matching\") {\n        stove {\n          grpcMock {\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              requestMatcher = RequestMatcher.ExactMessage(\n                TestRequest\n                  .newBuilder()\n                  .setMessage(\"specific\")\n                  .setCount(100)\n                  .build()\n              ),\n              response = TestResponse\n                .newBuilder()\n                .setMessage(\"Matched specific request!\")\n                .build()\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val request = testRequest {\n                message = \"specific\"\n                count = 100\n              }\n              val response = unary(request)\n              response.message shouldBe \"Matched specific request!\"\n            }\n          }\n        }\n      }\n\n      test(\"should handle multiple sequential unary calls\") {\n        stove {\n          grpcMock {\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              requestMatcher = RequestMatcher.ExactMessage(\n                TestRequest.newBuilder().setMessage(\"first\").build()\n              ),\n              response = TestResponse.newBuilder().setMessage(\"First response\").build()\n            )\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              requestMatcher = RequestMatcher.ExactMessage(\n                TestRequest.newBuilder().setMessage(\"second\").build()\n              ),\n              response = TestResponse.newBuilder().setMessage(\"Second response\").build()\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val response1 = unary(testRequest { message = \"first\" })\n              response1.message shouldBe \"First response\"\n\n              val response2 = unary(testRequest { message = \"second\" })\n              response2.message shouldBe \"Second response\"\n            }\n          }\n        }\n      }\n    }\n\n    context(\"Error responses\") {\n      test(\"should mock NOT_FOUND error\") {\n        stove {\n          grpcMock {\n            mockError(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              status = Status.Code.NOT_FOUND,\n              message = \"Resource not found\"\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val exception = shouldThrow<StatusException> {\n                unary(testRequest { message = \"test\" })\n              }\n              exception.status.code shouldBe Status.Code.NOT_FOUND\n              exception.status.description shouldContain \"Resource not found\"\n            }\n          }\n        }\n      }\n\n      test(\"should mock UNAUTHENTICATED error\") {\n        stove {\n          grpcMock {\n            mockError(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              status = Status.Code.UNAUTHENTICATED,\n              message = \"Invalid credentials\"\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val exception = shouldThrow<StatusException> {\n                unary(testRequest { message = \"test\" })\n              }\n              exception.status.code shouldBe Status.Code.UNAUTHENTICATED\n            }\n          }\n        }\n      }\n\n      test(\"should mock INVALID_ARGUMENT error\") {\n        stove {\n          grpcMock {\n            mockError(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              status = Status.Code.INVALID_ARGUMENT,\n              message = \"Invalid input\"\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val exception = shouldThrow<StatusException> {\n                unary(testRequest { message = \"\" })\n              }\n              exception.status.code shouldBe Status.Code.INVALID_ARGUMENT\n            }\n          }\n        }\n      }\n    }\n\n    context(\"Server streaming RPC\") {\n      test(\"should mock server streaming call with multiple responses\") {\n        stove {\n          grpcMock {\n            mockServerStream(\n              serviceName = \"test.TestService\",\n              methodName = \"ServerStream\",\n              responses = listOf(\n                Item\n                  .newBuilder()\n                  .setId(\"1\")\n                  .setName(\"Item 1\")\n                  .setValue(100)\n                  .build(),\n                Item\n                  .newBuilder()\n                  .setId(\"2\")\n                  .setName(\"Item 2\")\n                  .setValue(200)\n                  .build(),\n                Item\n                  .newBuilder()\n                  .setId(\"3\")\n                  .setName(\"Item 3\")\n                  .setValue(300)\n                  .build()\n              )\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val request = testRequest {\n                message = \"stream\"\n                count = 3\n              }\n              val responses = serverStream(request).toList()\n\n              responses shouldHaveSize 3\n              responses[0].id shouldBe \"1\"\n              responses[0].name shouldBe \"Item 1\"\n              responses[1].id shouldBe \"2\"\n              responses[2].id shouldBe \"3\"\n            }\n          }\n        }\n      }\n    }\n\n    context(\"Client streaming RPC\") {\n      test(\"should mock client streaming call\") {\n        stove {\n          grpcMock {\n            mockClientStream(\n              serviceName = \"test.TestService\",\n              methodName = \"ClientStream\",\n              response = TestResponse\n                .newBuilder()\n                .setMessage(\"Received all items\")\n                .setCount(5)\n                .build()\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val requests = kotlinx.coroutines.flow.flowOf(\n                testRequest { message = \"item1\" },\n                testRequest { message = \"item2\" },\n                testRequest { message = \"item3\" }\n              )\n              val response = clientStream(requests)\n\n              response.message shouldBe \"Received all items\"\n              response.count shouldBe 5\n            }\n          }\n        }\n      }\n    }\n\n    context(\"Bidirectional streaming RPC\") {\n      test(\"should mock bidi streaming call\") {\n        stove {\n          grpcMock {\n            mockBidiStream(\n              serviceName = \"test.TestService\",\n              methodName = \"BidiStream\"\n            ) { requestFlow ->\n              requestFlow.map { _ ->\n                // Echo back with modified message\n                TestResponse\n                  .newBuilder()\n                  .setMessage(\"Echo response\")\n                  .setCount(1)\n                  .build()\n              }\n            }\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val requests = kotlinx.coroutines.flow.flowOf(\n                testRequest { message = \"hello1\" },\n                testRequest { message = \"hello2\" }\n              )\n              val responses = bidiStream(requests).toList()\n\n              responses shouldHaveSize 2\n              responses.forEach { it.message shouldBe \"Echo response\" }\n            }\n          }\n        }\n      }\n    }\n\n    context(\"Authenticated calls\") {\n      test(\"should mock authenticated unary call with bearer token\") {\n        stove {\n          grpcMock {\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              metadataMatcher = MetadataMatcher.BearerToken(\"valid-token-123\"),\n              response = TestResponse\n                .newBuilder()\n                .setMessage(\"Authenticated response!\")\n                .build()\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub>(\n              metadata = mapOf(\"authorization\" to \"Bearer valid-token-123\")\n            ) {\n              val response = unary(testRequest { message = \"secure request\" })\n              response.message shouldBe \"Authenticated response!\"\n            }\n          }\n        }\n      }\n\n      test(\"should reject unauthenticated request when token required\") {\n        stove {\n          grpcMock {\n            // Only match if token is provided\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              metadataMatcher = MetadataMatcher.BearerToken(\"required-token\"),\n              response = TestResponse.newBuilder().setMessage(\"success\").build()\n            )\n          }\n\n          grpc {\n            // Call without token should fail (no matching stub)\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub> {\n              val exception = shouldThrow<StatusException> {\n                unary(testRequest { message = \"no auth\" })\n              }\n              exception.status.code shouldBe Status.Code.UNIMPLEMENTED\n            }\n          }\n        }\n      }\n\n      test(\"should reject request with wrong token\") {\n        stove {\n          grpcMock {\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              metadataMatcher = MetadataMatcher.BearerToken(\"correct-token\"),\n              response = TestResponse.newBuilder().setMessage(\"success\").build()\n            )\n          }\n\n          grpc {\n            // Call with wrong token should fail\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub>(\n              metadata = mapOf(\"authorization\" to \"Bearer wrong-token\")\n            ) {\n              val exception = shouldThrow<StatusException> {\n                unary(testRequest { message = \"wrong auth\" })\n              }\n              exception.status.code shouldBe Status.Code.UNIMPLEMENTED\n            }\n          }\n        }\n      }\n\n      test(\"should match custom header\") {\n        stove {\n          grpcMock {\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              metadataMatcher = MetadataMatcher.HasHeader(\"x-api-key\", \"secret-key-abc\"),\n              response = TestResponse\n                .newBuilder()\n                .setMessage(\"API key verified!\")\n                .build()\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub>(\n              metadata = mapOf(\"x-api-key\" to \"secret-key-abc\")\n            ) {\n              val response = unary(testRequest { message = \"api request\" })\n              response.message shouldBe \"API key verified!\"\n            }\n          }\n        }\n      }\n\n      test(\"should support RequiresAuth matcher for any auth header\") {\n        stove {\n          grpcMock {\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              metadataMatcher = MetadataMatcher.RequiresAuth,\n              response = TestResponse\n                .newBuilder()\n                .setMessage(\"Some auth provided\")\n                .build()\n            )\n          }\n\n          grpc {\n            // Any authorization header should work\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub>(\n              metadata = mapOf(\"authorization\" to \"Basic dXNlcjpwYXNz\")\n            ) {\n              val response = unary(testRequest { message = \"basic auth\" })\n              response.message shouldBe \"Some auth provided\"\n            }\n          }\n        }\n      }\n\n      test(\"should support combined matchers\") {\n        stove {\n          grpcMock {\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              metadataMatcher = MetadataMatcher.All(\n                MetadataMatcher.BearerToken(\"valid-token\"),\n                MetadataMatcher.HasHeader(\"x-tenant-id\", \"tenant-123\")\n              ),\n              response = TestResponse\n                .newBuilder()\n                .setMessage(\"Multi-header match!\")\n                .build()\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub>(\n              metadata = mapOf(\n                \"authorization\" to \"Bearer valid-token\",\n                \"x-tenant-id\" to \"tenant-123\"\n              )\n            ) {\n              val response = unary(testRequest { message = \"multi auth\" })\n              response.message shouldBe \"Multi-header match!\"\n            }\n          }\n        }\n      }\n\n      test(\"should mock authenticated server streaming\") {\n        stove {\n          grpcMock {\n            mockServerStream(\n              serviceName = \"test.TestService\",\n              methodName = \"ServerStream\",\n              metadataMatcher = MetadataMatcher.BearerToken(\"stream-token\"),\n              responses = listOf(\n                Item\n                  .newBuilder()\n                  .setId(\"1\")\n                  .setName(\"Secure Item\")\n                  .build()\n              )\n            )\n          }\n\n          grpc {\n            channel<TestServiceGrpcKt.TestServiceCoroutineStub>(\n              metadata = mapOf(\"authorization\" to \"Bearer stream-token\")\n            ) {\n              val responses = serverStream(testRequest { message = \"stream\" }).toList()\n              responses shouldHaveSize 1\n              responses[0].name shouldBe \"Secure Item\"\n            }\n          }\n        }\n      }\n    }\n\n    context(\"System state\") {\n      test(\"snapshot should return system state\") {\n        stove {\n          grpcMock {\n            mockUnary(\n              serviceName = \"test.TestService\",\n              methodName = \"Unary\",\n              response = TestResponse.newBuilder().setMessage(\"test\").build()\n            )\n\n            val snapshot = snapshot()\n            snapshot.system shouldBe \"gRPC Mock\"\n            snapshot.summary shouldContain \"Registered stubs:\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-grpc-mock/src/test/kotlin/com/trendyol/stove/testing/grpcmock/StoveConfig.kt",
    "content": "package com.trendyol.stove.testing.grpcmock\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.grpc.GrpcSystemOptions\nimport com.trendyol.stove.grpc.grpc\nimport com.trendyol.stove.system.PortFinder\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\n\nprivate val GRPC_PORT = PortFinder.findAvailablePort()\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        grpcMock {\n          GrpcMockSystemOptions(\n            port = GRPC_PORT,\n            removeStubAfterRequestMatched = true\n          )\n        }\n        grpc {\n          GrpcSystemOptions(\n            host = \"localhost\",\n            port = GRPC_PORT\n          )\n        }\n        applicationUnderTest(\n          object : ApplicationUnderTest<Unit> {\n            override suspend fun start(configurations: List<String>) = Unit\n\n            override suspend fun stop() = Unit\n          }\n        )\n      }.run()\n  }\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n"
  },
  {
    "path": "lib/stove-grpc-mock/src/test/proto/test_service.proto",
    "content": "syntax = \"proto3\";\n\npackage test;\n\noption java_package = \"com.trendyol.stove.testing.grpcmock.test\";\noption java_multiple_files = true;\n\n// Request message\nmessage TestRequest {\n  string message = 1;\n  int32 count = 2;\n}\n\n// Response message\nmessage TestResponse {\n  string message = 1;\n  int32 count = 2;\n}\n\n// Item for streaming tests\nmessage Item {\n  string id = 1;\n  string name = 2;\n  int32 value = 3;\n}\n\n// Test service with all RPC types\nservice TestService {\n  // Unary RPC - single request, single response\n  rpc Unary(TestRequest) returns (TestResponse);\n\n  // Server streaming RPC - single request, stream of responses\n  rpc ServerStream(TestRequest) returns (stream Item);\n\n  // Client streaming RPC - stream of requests, single response\n  rpc ClientStream(stream TestRequest) returns (TestResponse);\n\n  // Bidirectional streaming RPC - stream of requests, stream of responses\n  rpc BidiStream(stream TestRequest) returns (stream TestResponse);\n}\n"
  },
  {
    "path": "lib/stove-grpc-mock/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.testing.grpcmock.StoveConfig\n"
  },
  {
    "path": "lib/stove-http/api/stove-http.api",
    "content": "public final class com/trendyol/stove/http/HttpClientSystemOptions : com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic synthetic fun <init> (Ljava/lang/String;Lio/ktor/serialization/ContentConverter;Lio/ktor/serialization/WebsocketContentConverter;JJLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Lio/ktor/serialization/ContentConverter;Lio/ktor/serialization/WebsocketContentConverter;JJLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Lio/ktor/serialization/ContentConverter;\n\tpublic final fun component3 ()Lio/ktor/serialization/WebsocketContentConverter;\n\tpublic final fun component4-UwyO8pc ()J\n\tpublic final fun component5-UwyO8pc ()J\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component7 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component8 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy-NxwtSZ4 (Ljava/lang/String;Lio/ktor/serialization/ContentConverter;Lio/ktor/serialization/WebsocketContentConverter;JJLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/http/HttpClientSystemOptions;\n\tpublic static synthetic fun copy-NxwtSZ4$default (Lcom/trendyol/stove/http/HttpClientSystemOptions;Ljava/lang/String;Lio/ktor/serialization/ContentConverter;Lio/ktor/serialization/WebsocketContentConverter;JJLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/http/HttpClientSystemOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBaseUrl ()Ljava/lang/String;\n\tpublic final fun getConfigureClient ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getConfigureWebSocket ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getContentConverter ()Lio/ktor/serialization/ContentConverter;\n\tpublic final fun getCreateClient ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getTimeout-UwyO8pc ()J\n\tpublic final fun getWebSocketContentConverter ()Lio/ktor/serialization/WebsocketContentConverter;\n\tpublic final fun getWsPingInterval-UwyO8pc ()J\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract interface annotation class com/trendyol/stove/http/HttpDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/http/HttpSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/PluggedSystem {\n\tpublic static final field Companion Lcom/trendyol/stove/http/HttpSystem$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/http/HttpClientSystemOptions;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/http/HttpClientSystemOptions;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun buildWebSocketUrl (Ljava/lang/String;)Ljava/lang/String;\n\tpublic fun close ()V\n\tpublic final fun configureRequest (Lio/ktor/client/request/HttpRequestBuilder;Ljava/lang/String;Ljava/util/Map;Larrow/core/Option;)V\n\tpublic final fun deleteAndExpectBodilessResponse (Ljava/lang/String;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun deleteAndExpectBodilessResponse$default (Lcom/trendyol/stove/http/HttpSystem;Ljava/lang/String;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun executeWithBody (Lio/ktor/http/HttpMethod;Ljava/lang/String;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun get (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Larrow/core/Option;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun getBodilessResponse (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Larrow/core/Option;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun getBodilessResponse$default (Lcom/trendyol/stove/http/HttpSystem;Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Larrow/core/Option;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun getKtorHttpClient ()Lio/ktor/client/HttpClient;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/http/HttpClientSystemOptions;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun headAndExpectBodilessResponse (Ljava/lang/String;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun headAndExpectBodilessResponse$default (Lcom/trendyol/stove/http/HttpSystem;Ljava/lang/String;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun patchAndExpectBodilessResponse (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun patchAndExpectBodilessResponse$default (Lcom/trendyol/stove/http/HttpSystem;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun postAndExpectBodilessResponse (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun postAndExpectBodilessResponse$default (Lcom/trendyol/stove/http/HttpSystem;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun putAndExpectBodilessResponse (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun putAndExpectBodilessResponse$default (Lcom/trendyol/stove/http/HttpSystem;Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun toBodilessResponse (Lio/ktor/client/statement/HttpResponse;)Lcom/trendyol/stove/http/StoveHttpResponse$Bodiless;\n\tpublic final fun toFormData (Ljava/util/List;)Ljava/util/List;\n\tpublic final fun webSocket (Ljava/lang/String;Ljava/util/Map;Larrow/core/Option;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun webSocket$default (Lcom/trendyol/stove/http/HttpSystem;Ljava/lang/String;Ljava/util/Map;Larrow/core/Option;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun webSocketExpect (Ljava/lang/String;Ljava/util/Map;Larrow/core/Option;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun webSocketExpect$default (Lcom/trendyol/stove/http/HttpSystem;Ljava/lang/String;Ljava/util/Map;Larrow/core/Option;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun webSocketRaw (Ljava/lang/String;Ljava/util/Map;Larrow/core/Option;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun webSocketRaw$default (Lcom/trendyol/stove/http/HttpSystem;Ljava/lang/String;Ljava/util/Map;Larrow/core/Option;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/http/HttpSystem$Companion {\n\tpublic final fun client (Lcom/trendyol/stove/http/HttpSystem;)Lio/ktor/client/HttpClient;\n\tpublic final fun client (Lcom/trendyol/stove/http/HttpSystem;Lkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/http/HttpSystem$Companion$HeaderConstants {\n\tpublic static final field AUTHORIZATION Ljava/lang/String;\n\tpublic static final field INSTANCE Lcom/trendyol/stove/http/HttpSystem$Companion$HeaderConstants;\n\tpublic final fun bearer (Ljava/lang/String;)Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/http/HttpSystemKt {\n\tpublic static final fun http-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun http-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun httpClient-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun httpClient-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic abstract class com/trendyol/stove/http/StoveMultiPartContent {\n}\n\npublic final class com/trendyol/stove/http/StoveMultiPartContent$Binary : com/trendyol/stove/http/StoveMultiPartContent {\n\tpublic fun <init> (Ljava/lang/String;[B)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()[B\n\tpublic final fun copy (Ljava/lang/String;[B)Lcom/trendyol/stove/http/StoveMultiPartContent$Binary;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/http/StoveMultiPartContent$Binary;Ljava/lang/String;[BILjava/lang/Object;)Lcom/trendyol/stove/http/StoveMultiPartContent$Binary;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getContent ()[B\n\tpublic final fun getParam ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/http/StoveMultiPartContent$File : com/trendyol/stove/http/StoveMultiPartContent {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;[BLjava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()[B\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;[BLjava/lang/String;)Lcom/trendyol/stove/http/StoveMultiPartContent$File;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/http/StoveMultiPartContent$File;Ljava/lang/String;Ljava/lang/String;[BLjava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/http/StoveMultiPartContent$File;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getContent ()[B\n\tpublic final fun getContentType ()Ljava/lang/String;\n\tpublic final fun getFileName ()Ljava/lang/String;\n\tpublic final fun getParam ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/http/StoveMultiPartContent$Text : com/trendyol/stove/http/StoveMultiPartContent {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/http/StoveMultiPartContent$Text;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/http/StoveMultiPartContent$Text;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/http/StoveMultiPartContent$Text;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getParam ()Ljava/lang/String;\n\tpublic final fun getValue ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract class com/trendyol/stove/http/StoveWebSocketMessage {\n}\n\npublic final class com/trendyol/stove/http/StoveWebSocketMessage$Binary : com/trendyol/stove/http/StoveWebSocketMessage {\n\tpublic fun <init> ([B)V\n\tpublic final fun component1 ()[B\n\tpublic final fun copy ([B)Lcom/trendyol/stove/http/StoveWebSocketMessage$Binary;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/http/StoveWebSocketMessage$Binary;[BILjava/lang/Object;)Lcom/trendyol/stove/http/StoveWebSocketMessage$Binary;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getContent ()[B\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/http/StoveWebSocketMessage$Text : com/trendyol/stove/http/StoveWebSocketMessage {\n\tpublic fun <init> (Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;)Lcom/trendyol/stove/http/StoveWebSocketMessage$Text;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/http/StoveWebSocketMessage$Text;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/http/StoveWebSocketMessage$Text;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getContent ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/http/StoveWebSocketSession {\n\tpublic fun <init> (Lio/ktor/client/plugins/websocket/DefaultClientWebSocketSession;)V\n\tpublic final fun close (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun close$default (Lcom/trendyol/stove/http/StoveWebSocketSession;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun collectBinaries-8Mi8wO0 (IJLkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun collectBinaries-8Mi8wO0$default (Lcom/trendyol/stove/http/StoveWebSocketSession;IJLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun collectTexts-8Mi8wO0 (IJLkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun collectTexts-8Mi8wO0$default (Lcom/trendyol/stove/http/StoveWebSocketSession;IJLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun getSession ()Lio/ktor/client/plugins/websocket/DefaultClientWebSocketSession;\n\tpublic final fun incoming ()Lkotlinx/coroutines/flow/Flow;\n\tpublic final fun incomingBinaries ()Lkotlinx/coroutines/flow/Flow;\n\tpublic final fun incomingTexts ()Lkotlinx/coroutines/flow/Flow;\n\tpublic final fun receive (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun receiveBinary (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun receiveBinaryWithTimeout-VtjQ1oo (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun receiveBinaryWithTimeout-VtjQ1oo$default (Lcom/trendyol/stove/http/StoveWebSocketSession;JLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun receiveText (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun receiveTextWithTimeout-VtjQ1oo (JLkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun receiveTextWithTimeout-VtjQ1oo$default (Lcom/trendyol/stove/http/StoveWebSocketSession;JLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun send (Lcom/trendyol/stove/http/StoveWebSocketMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun send (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun send ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun underlyingSession (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/http/StreamingKt {\n\tpublic static final fun readJsonContentStream (Lio/ktor/client/statement/HttpStatement;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;\n\tpublic static final fun readJsonTextStream (Lio/ktor/client/statement/HttpStatement;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow;\n\tpublic static final fun serializeToStreamJson (Lcom/trendyol/stove/serialization/StoveSerde;Ljava/util/List;)[B\n}\n\n"
  },
  {
    "path": "lib/stove-http/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(libs.ktor.client.core)\n  api(libs.ktor.client.okhttp)\n  api(libs.ktor.client.plugins.logging)\n  api(libs.ktor.client.content.negotiation)\n  api(libs.ktor.serialization.jackson.json)\n  api(libs.ktor.client.websockets)\n  implementation(libs.kotlinx.core)\n  implementation(libs.kotlinx.io.reactor)\n  implementation(libs.kotlinx.reactive)\n  implementation(libs.kotlinx.jdk8)\n}\n\ndependencies {\n  testImplementation(projects.lib.stoveWiremock)\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.jackson.jsr310)\n  testImplementation(testFixtures(projects.lib.stove))\n  testImplementation(libs.logback.classic)\n  testImplementation(libs.ktor.server.netty)\n  testImplementation(libs.ktor.server.websockets)\n}\n"
  },
  {
    "path": "lib/stove-http/src/main/kotlin/com/trendyol/stove/http/HttpClientFactory.kt",
    "content": "package com.trendyol.stove.http\n\nimport io.ktor.client.*\nimport io.ktor.client.engine.okhttp.*\nimport io.ktor.client.plugins.*\nimport io.ktor.client.plugins.contentnegotiation.*\nimport io.ktor.client.plugins.logging.*\nimport io.ktor.client.plugins.websocket.*\nimport io.ktor.client.request.*\nimport io.ktor.http.*\nimport io.ktor.serialization.*\nimport org.slf4j.LoggerFactory\nimport kotlin.time.*\n\nprivate val httpClientLogger = LoggerFactory.getLogger(\"com.trendyol.stove.http.HttpClient\")\n\n@Suppress(\"MagicNumber\")\ninternal fun jsonHttpClient(\n  baseUrl: String,\n  timeout: Duration,\n  converter: ContentConverter,\n  webSocketContentConverter: WebsocketContentConverter,\n  pingInterval: Duration,\n  configureWebSocket: WebSockets.Config.() -> Unit = {},\n  configureClient: HttpClientConfig<*>.() -> Unit = {}\n): HttpClient = HttpClient(OkHttp) {\n  engine {\n    config {\n      followRedirects(true)\n      followSslRedirects(true)\n      connectTimeout(timeout.toJavaDuration())\n      readTimeout(timeout.toJavaDuration())\n      callTimeout(timeout.toJavaDuration())\n      writeTimeout(timeout.toJavaDuration())\n    }\n  }\n\n  install(Logging) {\n    logger = object : Logger {\n      override fun log(message: String) {\n        httpClientLogger.info(message)\n      }\n    }\n  }\n\n  install(ContentNegotiation) {\n    register(ContentType.Application.Json, converter)\n    register(ContentType.Application.ProblemJson, converter)\n    register(ContentType.parse(\"application/x-ndjson\"), converter)\n  }\n\n  install(WebSockets) {\n    contentConverter = webSocketContentConverter\n    this.pingInterval = pingInterval\n    configureWebSocket(this)\n  }\n\n  defaultRequest {\n    url(baseUrl)\n    header(HttpHeaders.ContentType, ContentType.Application.Json)\n    header(HttpHeaders.Accept, ContentType.Application.Json)\n  }\n\n  configureClient(this)\n}\n"
  },
  {
    "path": "lib/stove-http/src/main/kotlin/com/trendyol/stove/http/HttpDsl.kt",
    "content": "package com.trendyol.stove.http\n\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class HttpDsl\n"
  },
  {
    "path": "lib/stove-http/src/main/kotlin/com/trendyol/stove/http/HttpSystem.kt",
    "content": "@file:Suppress(\"MemberVisibilityCanBePrivate\", \"unused\")\n\npackage com.trendyol.stove.http\n\nimport arrow.core.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport com.trendyol.stove.tracing.TraceContext\nimport io.ktor.client.call.*\nimport io.ktor.client.plugins.websocket.*\nimport io.ktor.client.request.*\nimport io.ktor.client.request.forms.*\nimport io.ktor.client.statement.*\nimport io.ktor.http.*\nimport io.ktor.serialization.*\nimport io.ktor.serialization.jackson.*\nimport io.ktor.util.*\nimport io.ktor.util.reflect.*\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.*\nimport java.nio.charset.Charset\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * Configuration options for the HTTP client system.\n *\n * ## Basic Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         httpClient {\n *             HttpClientSystemOptions(\n *                 baseUrl = \"http://localhost:8080\"\n *             )\n *         }\n *     }\n * ```\n *\n * ## Custom Serialization\n *\n * Match your application's JSON serialization:\n *\n * ```kotlin\n * httpClient {\n *     HttpClientSystemOptions(\n *         baseUrl = \"http://localhost:8080\",\n *         contentConverter = JacksonConverter(myObjectMapper),\n *         timeout = 60.seconds\n *     )\n * }\n * ```\n *\n * ## Custom HTTP Client\n *\n * For advanced scenarios (custom SSL, interceptors, etc.):\n *\n * ```kotlin\n * httpClient {\n *     HttpClientSystemOptions(\n *         baseUrl = \"http://localhost:8080\",\n *         createClient = { baseUrl ->\n *             HttpClient(CIO) {\n *                 install(ContentNegotiation) { jackson() }\n *                 install(Logging) { level = LogLevel.ALL }\n *                 defaultRequest { url(baseUrl) }\n *             }\n *         }\n *     )\n * }\n * ```\n *\n * @property baseUrl The base URL for all HTTP requests (e.g., \"http://localhost:8080\").\n * @property contentConverter The content converter for JSON serialization (default: Jackson).\n * @property timeout Request timeout duration (default: 30 seconds).\n * @property createClient Factory function for creating the underlying Ktor HTTP client.\n */\n@HttpDsl\ndata class HttpClientSystemOptions(\n  val baseUrl: String,\n  val contentConverter: ContentConverter = JacksonConverter(StoveSerde.jackson.default),\n  val webSocketContentConverter: WebsocketContentConverter = JacksonWebsocketContentConverter(\n    StoveSerde.jackson.default\n  ),\n  val timeout: kotlin.time.Duration = 30.seconds,\n  val wsPingInterval: kotlin.time.Duration = 20.seconds,\n  val configureClient: io.ktor.client.HttpClientConfig<*>.() -> Unit = {},\n  val configureWebSocket: WebSockets.Config.() -> Unit = {},\n  val createClient: (\n    baseUrl: String\n  ) -> io.ktor.client.HttpClient = { url ->\n    jsonHttpClient(\n      url,\n      timeout,\n      contentConverter,\n      webSocketContentConverter,\n      wsPingInterval,\n      configureWebSocket,\n      configureClient\n    )\n  }\n) : SystemOptions\n\ninternal fun Stove.withHttpClient(options: HttpClientSystemOptions): Stove {\n  this.getOrRegister(HttpSystem(this, options))\n  return this\n}\n\ninternal fun Stove.withHttpClient(key: SystemKey, options: HttpClientSystemOptions): Stove {\n  this.getOrRegister(key, HttpSystem(this, options, keyName = keyDisplayName(key)))\n  return this\n}\n\ninternal fun Stove.http(): HttpSystem = getOrNone<HttpSystem>().getOrElse {\n  throw SystemNotRegisteredException(HttpSystem::class)\n}\n\ninternal fun Stove.http(key: SystemKey): HttpSystem = getOrNone<HttpSystem>(key).getOrElse {\n  throw SystemNotRegisteredException(HttpSystem::class, \"No HttpSystem registered with key '${keyDisplayName(key)}'\")\n}\n\n/**\n * Registers the HTTP client system with the test system.\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         httpClient {\n *             HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n *         }\n *     }\n * ```\n *\n * @param configure Configuration block returning [HttpClientSystemOptions].\n * @return The test system for fluent chaining.\n */\nfun WithDsl.httpClient(configure: @StoveDsl () -> HttpClientSystemOptions): Stove =\n  this.stove.withHttpClient(configure())\n\n/**\n * Registers a keyed HTTP client system for testing multiple HTTP services.\n *\n * ```kotlin\n * Stove().with {\n *     httpClient(PaymentService) {\n *         HttpClientSystemOptions(baseUrl = \"https://payment.internal.com\")\n *     }\n * }\n * ```\n *\n * @param key The [SystemKey] identifying this HTTP client instance.\n * @param configure Configuration block returning [HttpClientSystemOptions].\n * @return The test system for fluent chaining.\n */\nfun WithDsl.httpClient(key: SystemKey, configure: @StoveDsl () -> HttpClientSystemOptions): Stove =\n  this.stove.withHttpClient(key, configure())\n\n/**\n * Executes HTTP assertions within the validation DSL.\n *\n * ```kotlin\n * stove {\n *     http {\n *         get<UserResponse>(\"/users/123\") { user ->\n *             user.name shouldBe \"John\"\n *         }\n *     }\n * }\n * ```\n *\n * @param validation The HTTP assertion block.\n */\nsuspend fun ValidationDsl.http(\n  validation: @HttpDsl suspend HttpSystem.() -> Unit\n): Unit = validation(this.stove.http())\n\n/**\n * Executes HTTP assertions against a keyed HTTP client within the validation DSL.\n *\n * ```kotlin\n * stove {\n *     http(PaymentService) {\n *         get<Payment>(\"/payments/123\") { payment ->\n *             payment.amount shouldBe 99.99\n *         }\n *     }\n * }\n * ```\n *\n * @param key The [SystemKey] identifying the HTTP client instance.\n * @param validation The HTTP assertion block.\n */\nsuspend fun ValidationDsl.http(\n  key: SystemKey,\n  validation: @HttpDsl suspend HttpSystem.() -> Unit\n): Unit = validation(this.stove.http(key))\n\n/**\n * HTTP client system for testing REST APIs.\n *\n * Provides a fluent DSL for making HTTP requests and asserting responses.\n * All methods return the system instance for chaining.\n *\n * ## GET Requests\n *\n * ```kotlin\n * http {\n *     // Get with typed response body\n *     get<UserResponse>(\"/users/123\") { user ->\n *         user.name shouldBe \"John\"\n *         user.email shouldBe \"john@example.com\"\n *     }\n *\n *     // Get with query parameters\n *     get<List<UserResponse>>(\"/users\", queryParams = mapOf(\"role\" to \"admin\")) { users ->\n *         users.size shouldBeGreaterThan 0\n *     }\n *\n *     // Get with full response access\n *     getResponse<UserResponse>(\"/users/123\") { response ->\n *         response.status shouldBe 200\n *         response.headers[\"Content-Type\"] shouldContain \"application/json\"\n *         response.body().name shouldBe \"John\"\n *     }\n *\n *     // Get with headers and auth token\n *     get<SecretData>(\n *         uri = \"/secrets\",\n *         headers = mapOf(\"X-Request-Id\" to \"123\"),\n *         token = \"jwt-token\".some()\n *     ) { data ->\n *         data shouldNotBe null\n *     }\n * }\n * ```\n *\n * ## POST Requests\n *\n * ```kotlin\n * http {\n *     // Post with JSON body and typed response\n *     postAndExpectJson<UserResponse>(\n *         uri = \"/users\",\n *         body = CreateUserRequest(name = \"John\", email = \"john@example.com\").some()\n *     ) { user ->\n *         user.id shouldNotBe null\n *     }\n *\n *     // Post expecting only status code\n *     postAndExpectBodilessResponse(\n *         uri = \"/users\",\n *         body = CreateUserRequest(name = \"John\").some()\n *     ) { response ->\n *         response.status shouldBe 201\n *     }\n *\n *     // Post with full response access\n *     postAndExpectBody<UserResponse>(\n *         uri = \"/users\",\n *         body = request.some()\n *     ) { response ->\n *         response.status shouldBe 201\n *         response.body().id shouldNotBe null\n *     }\n * }\n * ```\n *\n * ## PUT, PATCH, DELETE Requests\n *\n * ```kotlin\n * http {\n *     // PUT\n *     putAndExpectJson<UserResponse>(\n *         uri = \"/users/123\",\n *         body = UpdateUserRequest(name = \"Jane\").some()\n *     ) { user ->\n *         user.name shouldBe \"Jane\"\n *     }\n *\n *     // PATCH\n *     patchAndExpectBodilessResponse(\n *         uri = \"/users/123\",\n *         body = mapOf(\"status\" to \"active\").some()\n *     ) { response ->\n *         response.status shouldBe 200\n *     }\n *\n *     // DELETE\n *     deleteAndExpectBodilessResponse(\"/users/123\") { response ->\n *         response.status shouldBe 204\n *     }\n * }\n * ```\n *\n * ## Multipart/Form Requests\n *\n * ```kotlin\n * http {\n *     postMultipartAndExpectResponse<UploadResponse>(\n *         uri = \"/upload\",\n *         body = listOf(\n *             StoveMultiPartContent.Text(\"name\", \"document.pdf\"),\n *             StoveMultiPartContent.File(\n *                 param = \"file\",\n *                 fileName = \"document.pdf\",\n *                 content = fileBytes,\n *                 contentType = \"application/pdf\"\n *             )\n *         )\n *     ) { response ->\n *         response.body().fileId shouldNotBe null\n *     }\n * }\n * ```\n *\n * ## Streaming Responses\n *\n * ```kotlin\n * http {\n *     readJsonStream<LogEntry>(\n *         uri = \"/logs/stream\",\n *         headers = mapOf(\"Accept\" to \"application/x-ndjson\")\n *     ) { flow ->\n *         flow.collect { entry ->\n *             println(entry.message)\n *         }\n *     }\n * }\n * ```\n *\n * @property stove The parent test system.\n * @property options HTTP client configuration options.\n * @see HttpClientSystemOptions\n * @see StoveHttpResponse\n */\n@Suppress(\"TooManyFunctions\")\n@HttpDsl\nclass HttpSystem(\n  override val stove: Stove,\n  @PublishedApi internal val options: HttpClientSystemOptions,\n  private val keyName: String? = null\n) : PluggedSystem,\n  Reports {\n  @PublishedApi\n  internal val ktorHttpClient: io.ktor.client.HttpClient = options.createClient(options.baseUrl)\n\n  override val reportSystemName: String = \"HTTP\" + (keyName?.let { \" [$it]\" } ?: \"\")\n\n  /**\n   * Performs a GET request and asserts on the bodiless response.\n   */\n  suspend fun getBodilessResponse(\n    uri: String,\n    queryParams: Map<String, String> = mapOf(),\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    expect: suspend (StoveHttpResponse.Bodiless) -> Unit\n  ): HttpSystem {\n    val response = get(uri, headers, queryParams, token)\n    report(\n      action = \"GET $uri\",\n      input = queryParams.takeIf { it.isNotEmpty() }.toOption(),\n      output = \"Status: ${response.status.value}\".some(),\n      metadata = mapOf(\n        \"status\" to response.status.value,\n        \"headers\" to headers,\n        \"response\" to response.bodyAsText()\n      ),\n      expected = \"Response matching expectation\".some()\n    ) {\n      expect(response.toBodilessResponse())\n    }\n    return this\n  }\n\n  /**\n   * Performs a GET request and asserts on the typed response body.\n   */\n  suspend inline fun <reified T : Any> getResponse(\n    uri: String,\n    queryParams: Map<String, String> = mapOf(),\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: suspend (StoveHttpResponse.WithBody<T>) -> Unit\n  ): HttpSystem {\n    val response = get(uri, headers, queryParams, token)\n    report(\n      action = \"GET $uri\",\n      input = queryParams.takeIf { it.isNotEmpty() }.toOption(),\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"Response<${T::class.simpleName}> matching expectation\".some()\n    ) {\n      expect(response.toResponseWithBody())\n    }\n    return this\n  }\n\n  /**\n   * Performs a GET request and asserts on the deserialized response body.\n   */\n  suspend inline fun <reified TExpected : Any> get(\n    uri: String,\n    queryParams: Map<String, String> = mapOf(),\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: (TExpected) -> Unit\n  ): HttpSystem {\n    val response = get(uri, headers, queryParams, token)\n    report(\n      action = \"GET $uri\",\n      input = queryParams.takeIf { it.isNotEmpty() }.toOption(),\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"${TExpected::class.simpleName} matching expectation\".some()\n    ) {\n      response.expectSuccessBody(expect)\n    }\n    return this\n  }\n\n  /**\n   * Performs a GET request and asserts on a list response.\n   */\n  suspend inline fun <reified TExpected : Any> getMany(\n    uri: String,\n    queryParams: Map<String, String> = mapOf(),\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: (List<TExpected>) -> Unit\n  ): HttpSystem {\n    val response = get(uri, headers, queryParams, token)\n    report(\n      action = \"GET $uri\",\n      input = queryParams.takeIf { it.isNotEmpty() }.toOption(),\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"List<${TExpected::class.simpleName}> matching expectation\".some()\n    ) {\n      response.expectSuccessBody(expect)\n    }\n    return this\n  }\n\n  /**\n   * Performs a GET request for a JSON stream (NDJSON) and asserts on the flow.\n   */\n  suspend inline fun <reified TExpected : Any> readJsonStream(\n    uri: String,\n    queryParams: Map<String, String> = mapOf(),\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: suspend (Flow<TExpected>) -> Unit\n  ): HttpSystem {\n    report(\n      action = \"GET $uri (stream)\",\n      input = queryParams.takeIf { it.isNotEmpty() }.toOption(),\n      metadata = mapOf(\"headers\" to headers),\n      expected = \"Flow<${TExpected::class.simpleName}> stream\".some()\n    ) {\n      val flow = ktorHttpClient\n        .prepareGet {\n          url { appendEncodedPathSegments(uri) }\n          headers.forEach { (key, value) -> header(key, value) }\n          header(HttpHeaders.Accept, \"application/x-ndjson\")\n          queryParams.forEach { (key, value) -> parameter(key, value) }\n          token.map { header(HeaderConstants.AUTHORIZATION, HeaderConstants.bearer(it)) }\n        }.readJsonContentStream {\n          options.contentConverter.deserialize(Charset.defaultCharset(), typeInfo<TExpected>(), it) as TExpected\n        }\n      expect(flow.flowOn(Dispatchers.IO))\n    }\n    return this\n  }\n\n  /**\n   * Performs a POST request and asserts on the bodiless response.\n   */\n  suspend fun postAndExpectBodilessResponse(\n    uri: String,\n    body: Option<Any>,\n    token: Option<String> = None,\n    headers: Map<String, String> = mapOf(),\n    expect: suspend (StoveHttpResponse) -> Unit\n  ): HttpSystem {\n    val response = executeWithBody(HttpMethod.Post, uri, body, headers, token)\n    report(\n      action = \"POST $uri\",\n      input = body,\n      metadata = mapOf(\n        \"status\" to response.status.value,\n        \"headers\" to headers,\n        \"response\" to response.bodyAsText()\n      ),\n      expected = \"Response matching expectation\".some()\n    ) {\n      expect(response.toBodilessResponse())\n    }\n    return this\n  }\n\n  /**\n   * Performs a POST request and asserts on the deserialized response body.\n   */\n  suspend inline fun <reified TExpected : Any> postAndExpectJson(\n    uri: String,\n    body: Option<Any> = None,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: (actual: TExpected) -> Unit\n  ): HttpSystem {\n    val response = executeWithBody(HttpMethod.Post, uri, body, headers, token)\n    report(\n      action = \"POST $uri\",\n      input = body,\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"${TExpected::class.simpleName} matching expectation\".some()\n    ) {\n      response.expectSuccessBody(expect)\n    }\n    return this\n  }\n\n  /**\n   * Performs a POST request and asserts on the typed response with body access.\n   */\n  suspend inline fun <reified TExpected : Any> postAndExpectBody(\n    uri: String,\n    body: Option<Any> = None,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: suspend (actual: StoveHttpResponse.WithBody<TExpected>) -> Unit\n  ): HttpSystem {\n    val response = executeWithBody(HttpMethod.Post, uri, body, headers, token)\n    report(\n      action = \"POST $uri\",\n      input = body,\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"Response<${TExpected::class.simpleName}> matching expectation\".some()\n    ) {\n      expect(response.toResponseWithBody())\n    }\n    return this\n  }\n\n  /**\n   * Performs a PUT request and asserts on the bodiless response.\n   */\n  suspend fun putAndExpectBodilessResponse(\n    uri: String,\n    body: Option<Any>,\n    token: Option<String> = None,\n    headers: Map<String, String> = mapOf(),\n    expect: suspend (StoveHttpResponse) -> Unit\n  ): HttpSystem {\n    val response = executeWithBody(HttpMethod.Put, uri, body, headers, token)\n    report(\n      action = \"PUT $uri\",\n      input = body,\n      metadata = mapOf(\n        \"status\" to response.status.value,\n        \"headers\" to headers,\n        \"response\" to response.bodyAsText()\n      ),\n      expected = \"Response matching expectation\".some()\n    ) {\n      expect(response.toBodilessResponse())\n    }\n    return this\n  }\n\n  /**\n   * Performs a PUT request and asserts on the deserialized response body.\n   */\n  suspend inline fun <reified TExpected : Any> putAndExpectJson(\n    uri: String,\n    body: Option<Any> = None,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: (actual: TExpected) -> Unit\n  ): HttpSystem {\n    val response = executeWithBody(HttpMethod.Put, uri, body, headers, token)\n    report(\n      action = \"PUT $uri\",\n      input = body,\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"${TExpected::class.simpleName} matching expectation\".some()\n    ) {\n      response.expectSuccessBody(expect)\n    }\n    return this\n  }\n\n  /**\n   * Performs a PUT request and asserts on the typed response with body access.\n   */\n  suspend inline fun <reified TExpected : Any> putAndExpectBody(\n    uri: String,\n    body: Option<Any> = None,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: suspend (actual: StoveHttpResponse.WithBody<TExpected>) -> Unit\n  ): HttpSystem {\n    val response = executeWithBody(HttpMethod.Put, uri, body, headers, token)\n    report(\n      action = \"PUT $uri\",\n      input = body,\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"Response<${TExpected::class.simpleName}> matching expectation\".some()\n    ) {\n      expect(response.toResponseWithBody())\n    }\n    return this\n  }\n\n  /**\n   * Performs a PATCH request and asserts on the bodiless response.\n   */\n  suspend fun patchAndExpectBodilessResponse(\n    uri: String,\n    body: Option<Any>,\n    token: Option<String> = None,\n    headers: Map<String, String> = mapOf(),\n    expect: suspend (StoveHttpResponse) -> Unit\n  ): HttpSystem {\n    val response = executeWithBody(HttpMethod.Patch, uri, body, headers, token)\n    report(\n      action = \"PATCH $uri\",\n      input = body,\n      metadata = mapOf(\n        \"status\" to response.status.value,\n        \"headers\" to headers,\n        \"response\" to response.bodyAsText()\n      ),\n      expected = \"Response matching expectation\".some()\n    ) {\n      expect(response.toBodilessResponse())\n    }\n    return this\n  }\n\n  /**\n   * Performs a PATCH request and asserts on the deserialized response body.\n   */\n  suspend inline fun <reified TExpected : Any> patchAndExpectJson(\n    uri: String,\n    body: Option<Any> = None,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: (actual: TExpected) -> Unit\n  ): HttpSystem {\n    val response = executeWithBody(HttpMethod.Patch, uri, body, headers, token)\n    report(\n      action = \"PATCH $uri\",\n      input = body,\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"${TExpected::class.simpleName} matching expectation\".some()\n    ) {\n      response.expectSuccessBody(expect)\n    }\n    return this\n  }\n\n  /**\n   * Performs a PATCH request and asserts on the typed response with body access.\n   */\n  suspend inline fun <reified TExpected : Any> patchAndExpectBody(\n    uri: String,\n    body: Option<Any> = None,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: suspend (actual: StoveHttpResponse.WithBody<TExpected>) -> Unit\n  ): HttpSystem {\n    val response = executeWithBody(HttpMethod.Patch, uri, body, headers, token)\n    report(\n      action = \"PATCH $uri\",\n      input = body,\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"Response<${TExpected::class.simpleName}> matching expectation\".some()\n    ) {\n      expect(response.toResponseWithBody())\n    }\n    return this\n  }\n\n  /**\n   * Performs a DELETE request and asserts on the bodiless response.\n   */\n  suspend fun deleteAndExpectBodilessResponse(\n    uri: String,\n    token: Option<String> = None,\n    headers: Map<String, String> = mapOf(),\n    expect: suspend (StoveHttpResponse) -> Unit\n  ): HttpSystem {\n    val response = ktorHttpClient.delete {\n      configureRequest(uri, headers, token)\n    }\n    report(\n      action = \"DELETE $uri\",\n      metadata = mapOf(\n        \"status\" to response.status.value,\n        \"headers\" to headers,\n        \"response\" to response.bodyAsText()\n      ),\n      expected = \"Response matching expectation\".some()\n    ) {\n      expect(response.toBodilessResponse())\n    }\n    return this\n  }\n\n  /**\n   * Performs a DELETE request and asserts on the deserialized response body.\n   */\n  suspend inline fun <reified TExpected : Any> deleteAndExpectJson(\n    uri: String,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: (actual: TExpected) -> Unit\n  ): HttpSystem {\n    val response = ktorHttpClient.delete {\n      configureRequest(uri, headers, token)\n    }\n    report(\n      action = \"DELETE $uri\",\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"${TExpected::class.simpleName} matching expectation\".some()\n    ) {\n      response.expectSuccessBody(expect)\n    }\n    return this\n  }\n\n  /**\n   * Performs a HEAD request and asserts on the bodiless response.\n   */\n  suspend fun headAndExpectBodilessResponse(\n    uri: String,\n    token: Option<String> = None,\n    headers: Map<String, String> = mapOf(),\n    expect: suspend (StoveHttpResponse) -> Unit\n  ): HttpSystem {\n    val response = ktorHttpClient.head {\n      configureRequest(uri, headers, token)\n    }\n    report(\n      action = \"HEAD $uri\",\n      metadata = mapOf(\n        \"status\" to response.status.value,\n        \"headers\" to headers,\n        \"response\" to response.bodyAsText()\n      ),\n      expected = \"Response matching expectation\".some()\n    ) {\n      expect(response.toBodilessResponse())\n    }\n    return this\n  }\n\n  /**\n   * Performs a multipart POST request and asserts on the typed response.\n   */\n  suspend inline fun <reified TExpected : Any> postMultipartAndExpectResponse(\n    uri: String,\n    body: List<StoveMultiPartContent>,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    crossinline expect: suspend (StoveHttpResponse.WithBody<TExpected>) -> Unit\n  ): HttpSystem {\n    val response = ktorHttpClient.submitForm {\n      configureRequest(uri, headers, token)\n      setBody(MultiPartFormDataContent(toFormData(body)))\n    }\n    report(\n      action = \"POST $uri (multipart)\",\n      input = body.map { it::class.simpleName }.some(),\n      output = response.bodyAsText().some(),\n      metadata = mapOf(\"status\" to response.status.value, \"headers\" to headers),\n      expected = \"Response<${TExpected::class.simpleName}> matching expectation\".some()\n    ) {\n      expect(response.toResponseWithBody())\n    }\n    return this\n  }\n\n  override fun then(): Stove = stove\n\n  @PublishedApi\n  internal suspend fun get(\n    uri: String,\n    headers: Map<String, String>,\n    queryParams: Map<String, String>,\n    token: Option<String>\n  ) = ktorHttpClient.get {\n    configureRequest(uri, headers, token)\n    queryParams.forEach { (key, value) -> parameter(key, value) }\n  }\n\n  @PublishedApi\n  internal suspend fun executeWithBody(\n    method: HttpMethod,\n    uri: String,\n    body: Option<Any>,\n    headers: Map<String, String>,\n    token: Option<String>\n  ): HttpResponse = ktorHttpClient.request {\n    this.method = method\n    configureRequest(uri, headers, token)\n    body.map { setBody(it) }\n  }\n\n  @PublishedApi\n  internal fun HttpRequestBuilder.configureRequest(\n    uri: String,\n    headers: Map<String, String>,\n    token: Option<String>\n  ) {\n    url { appendEncodedPathSegments(uri) }\n    headers.forEach { (key, value) -> header(key, value) }\n    token.map { header(HeaderConstants.AUTHORIZATION, HeaderConstants.bearer(it)) }\n    injectTraceHeaders()\n  }\n\n  private fun HttpRequestBuilder.injectTraceHeaders() {\n    TraceContext.current()?.let { ctx ->\n      header(TraceContext.TRACEPARENT_HEADER, ctx.toTraceparent())\n      header(TraceContext.STOVE_TEST_ID_HEADER, ctx.testId)\n    }\n  }\n\n  @PublishedApi\n  internal fun HttpResponse.toBodilessResponse(): StoveHttpResponse.Bodiless =\n    StoveHttpResponse.Bodiless(status.value, headers.toMap())\n\n  @PublishedApi\n  internal inline fun <reified T : Any> HttpResponse.toResponseWithBody(): StoveHttpResponse.WithBody<T> =\n    StoveHttpResponse.WithBody(status.value, headers.toMap()) { body() }\n\n  @PublishedApi\n  internal suspend inline fun <reified T : Any> HttpResponse.expectSuccessBody(expect: (T) -> Unit) {\n    check(status.isSuccess()) { \"Expected a successful response, but got $status\" }\n    expect(body())\n  }\n\n  @PublishedApi\n  internal fun toFormData(\n    body: List<StoveMultiPartContent>\n  ) = formData {\n    body.forEach {\n      when (it) {\n        is StoveMultiPartContent.Text -> append(it.param, it.value)\n\n        is StoveMultiPartContent.Binary -> append(\n          it.param,\n          it.content,\n          Headers.build {\n            append(HttpHeaders.ContentType, ContentType.Application.OctetStream)\n          }\n        )\n\n        is StoveMultiPartContent.File -> append(\n          it.param,\n          it.content,\n          Headers.build {\n            append(HttpHeaders.ContentType, ContentType.parse(it.contentType))\n            append(HttpHeaders.ContentDisposition, \"filename=${it.fileName}\")\n          }\n        )\n      }\n    }\n  }\n\n  // region WebSocket Methods\n\n  /**\n   * Establishes a WebSocket connection and executes the provided block.\n   *\n   * ## Basic Usage\n   *\n   * ```kotlin\n   * http {\n   *     webSocket(\"/chat\") { session ->\n   *         session.send(\"Hello!\")\n   *         val response = session.receiveText()\n   *         response shouldBe \"Echo: Hello!\"\n   *     }\n   * }\n   * ```\n   *\n   * ## With Headers and Token\n   *\n   * ```kotlin\n   * http {\n   *     webSocket(\n   *         uri = \"/secure-chat\",\n   *         headers = mapOf(\"X-Custom-Header\" to \"value\"),\n   *         token = \"jwt-token\".some()\n   *     ) { session ->\n   *         session.send(\"Authenticated message\")\n   *     }\n   * }\n   * ```\n   *\n   * @param uri The WebSocket endpoint URI (e.g., \"/chat\").\n   * @param headers Optional HTTP headers to send with the upgrade request.\n   * @param token Optional bearer token for authentication.\n   * @param block The test block to execute with the WebSocket session.\n   * @return The [HttpSystem] for fluent chaining.\n   */\n  suspend fun webSocket(\n    uri: String,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    block: suspend StoveWebSocketSession.() -> Unit\n  ): HttpSystem {\n    ktorHttpClient.webSocket(\n      urlString = buildWebSocketUrl(uri),\n      request = {\n        headers.forEach { (key, value) -> this.headers.append(key, value) }\n        token.map {\n          this.headers.append(HeaderConstants.AUTHORIZATION, HeaderConstants.bearer(it))\n        }\n        injectWebSocketTraceHeaders()\n      }\n    ) {\n      val stoveSession = StoveWebSocketSession(this)\n      block(stoveSession)\n    }\n    return this\n  }\n\n  /**\n   * Establishes a WebSocket connection and executes assertions on the session.\n   *\n   * This is an alias for [webSocket] with a clearer intent for assertion-focused tests.\n   *\n   * @param uri The WebSocket endpoint URI.\n   * @param headers Optional HTTP headers.\n   * @param token Optional bearer token.\n   * @param expect The assertion block to execute.\n   * @return The [HttpSystem] for fluent chaining.\n   */\n  suspend fun webSocketExpect(\n    uri: String,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    expect: suspend StoveWebSocketSession.() -> Unit\n  ): HttpSystem = webSocket(uri, headers, token, expect)\n\n  /**\n   * Establishes a raw WebSocket connection for advanced use cases.\n   *\n   * This method provides direct access to the Ktor WebSocket session\n   * for scenarios where the simplified [StoveWebSocketSession] is not sufficient.\n   *\n   * @param uri The WebSocket endpoint URI.\n   * @param headers Optional HTTP headers.\n   * @param token Optional bearer token.\n   * @param block The block to execute with the raw Ktor WebSocket session.\n   * @return The [HttpSystem] for fluent chaining.\n   */\n  suspend fun webSocketRaw(\n    uri: String,\n    headers: Map<String, String> = mapOf(),\n    token: Option<String> = None,\n    block: suspend DefaultClientWebSocketSession.() -> Unit\n  ): HttpSystem {\n    ktorHttpClient.webSocket(\n      urlString = buildWebSocketUrl(uri),\n      request = {\n        headers.forEach { (key, value) -> this.headers.append(key, value) }\n        token.map {\n          this.headers.append(HeaderConstants.AUTHORIZATION, HeaderConstants.bearer(it))\n        }\n        injectWebSocketTraceHeaders()\n      }\n    ) {\n      block()\n    }\n    return this\n  }\n\n  @PublishedApi\n  internal fun buildWebSocketUrl(uri: String): String {\n    val baseUrl = options.baseUrl\n    val wsUrl = when {\n      baseUrl.startsWith(\"https://\") -> baseUrl.replace(\"https://\", \"wss://\")\n      baseUrl.startsWith(\"http://\") -> baseUrl.replace(\"http://\", \"ws://\")\n      else -> \"ws://$baseUrl\"\n    }\n    return \"$wsUrl${uri.ensureLeadingSlash()}\"\n  }\n\n  private fun String.ensureLeadingSlash(): String = if (startsWith(\"/\")) this else \"/$this\"\n\n  private fun HttpRequestBuilder.injectWebSocketTraceHeaders() {\n    TraceContext.current()?.let { ctx ->\n      headers.append(TraceContext.TRACEPARENT_HEADER, ctx.toTraceparent())\n      headers.append(TraceContext.STOVE_TEST_ID_HEADER, ctx.testId)\n    }\n  }\n\n  // endregion\n\n  override fun close() {\n    ktorHttpClient.close()\n  }\n\n  companion object {\n    object HeaderConstants {\n      const val AUTHORIZATION = \"Authorization\"\n\n      fun bearer(token: String) = \"Bearer $token\"\n    }\n\n    /**\n     * Exposes the [io.ktor.client.HttpClient] used by the [HttpSystem].\n     * Use this for advanced HTTP operations not covered by the DSL.\n     */\n    @Suppress(\"unused\")\n    fun HttpSystem.client(): io.ktor.client.HttpClient = this.ktorHttpClient\n\n    /**\n     * Exposes the [io.ktor.client.HttpClient] used by the [HttpSystem].\n     * Use this for advanced HTTP operations not covered by the DSL.\n     */\n    @Suppress(\"unused\")\n    suspend fun HttpSystem.client(\n      block: suspend io.ktor.client.HttpClient.(baseUrl: URLBuilder) -> Unit\n    ) {\n      report(\n        action = \"Custom HTTP Client Operation\",\n        metadata = mapOf(\"baseUrl\" to this.options.baseUrl),\n        expected = \"Custom operation completed\".some()\n      ) {\n        block(this.ktorHttpClient, URLBuilder(this.options.baseUrl))\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove-http/src/main/kotlin/com/trendyol/stove/http/StoveMultiPartContent.kt",
    "content": "@file:Suppress(\"ArrayInDataClass\")\n\npackage com.trendyol.stove.http\n\n/**\n * Represents a multi-part content for a HTTP request.\n */\nsealed class StoveMultiPartContent {\n  /**\n   * Represents a text content for a multi-part request.\n   */\n  data class Text(\n    val param: String,\n    val value: String\n  ) : StoveMultiPartContent()\n\n  /**\n   * Represents a file content for a multi-part request.\n   */\n  data class File(\n    val param: String,\n    val fileName: String,\n    val content: ByteArray,\n    val contentType: String\n  ) : StoveMultiPartContent()\n\n  /**\n   * Represents a binary content for a multi-part request.\n   */\n  data class Binary(\n    val param: String,\n    val content: ByteArray\n  ) : StoveMultiPartContent()\n}\n"
  },
  {
    "path": "lib/stove-http/src/main/kotlin/com/trendyol/stove/http/streaming.kt",
    "content": "package com.trendyol.stove.http\n\nimport arrow.core.toOption\nimport com.trendyol.stove.serialization.StoveSerde\nimport io.ktor.client.statement.*\nimport io.ktor.http.*\nimport io.ktor.utils.io.*\nimport kotlinx.coroutines.flow.*\n\n@OptIn(InternalAPI::class)\n@Suppress(\"unused\")\nfun <T> HttpStatement.readJsonTextStream(transform: suspend (line: String) -> T): Flow<T> = flow {\n  execute {\n    check(it.status.isSuccess()) { \"Request failed with status: ${it.status}\" }\n    while (!it.rawContent.isClosedForRead) {\n      it.rawContent.readUTF8LineNonEmpty { line -> emit(transform(line)) }\n    }\n  }\n}\n\n@OptIn(InternalAPI::class)\n@Suppress(\"unused\")\nfun <T> HttpStatement.readJsonContentStream(transform: suspend (line: ByteReadChannel) -> T): Flow<T> = flow {\n  execute {\n    check(it.status.isSuccess()) { \"Request failed with status: ${it.status}\" }\n    while (!it.rawContent.isClosedForRead) {\n      it.rawContent.readUTF8LineNonEmpty { line -> emit(transform(ByteReadChannel(line.toByteArray()))) }\n    }\n  }\n}\n\nprivate suspend fun ByteReadChannel.readUTF8LineNonEmpty(onRead: suspend (String) -> Unit) {\n  readLine().toOption().filter { it.isNotBlank() }.map { onRead(it) }\n}\n\n/**\n * Serializes the items to a stream of JSON strings.\n */\nfun <T : Any> StoveSerde<T, ByteArray>.serializeToStreamJson(items: List<T>): ByteArray = items\n  .joinToString(\"\\n\") { String(serialize(it)) }\n  .toByteArray()\n"
  },
  {
    "path": "lib/stove-http/src/main/kotlin/com/trendyol/stove/http/websocket.kt",
    "content": "@file:Suppress(\"TooManyFunctions\")\n\npackage com.trendyol.stove.http\n\nimport arrow.core.*\nimport io.ktor.client.plugins.websocket.*\nimport io.ktor.websocket.*\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * Represents a WebSocket message that can be sent or received.\n *\n * @see Text for text messages\n * @see Binary for binary messages\n */\n@HttpDsl\nsealed class StoveWebSocketMessage {\n  /**\n   * A text-based WebSocket message.\n   *\n   * @property content The text content of the message.\n   */\n  data class Text(\n    val content: String\n  ) : StoveWebSocketMessage()\n\n  /**\n   * A binary WebSocket message.\n   *\n   * @property content The binary content of the message.\n   */\n  data class Binary(\n    val content: ByteArray\n  ) : StoveWebSocketMessage() {\n    override fun equals(other: Any?): Boolean {\n      if (this === other) return true\n      if (javaClass != other?.javaClass) return false\n      other as Binary\n      return content.contentEquals(other.content)\n    }\n\n    override fun hashCode(): Int = content.contentHashCode()\n  }\n}\n\n/**\n * A test-friendly wrapper around a Ktor WebSocket session.\n *\n * Provides a simplified API for sending and receiving WebSocket messages\n * in e2e tests, including support for collecting messages with timeouts.\n *\n * ## Basic Usage\n *\n * ```kotlin\n * http {\n *     webSocket(\"/chat\") { session ->\n *         // Send a message\n *         session.send(\"Hello, World!\")\n *\n *         // Receive a single message\n *         val response = session.receiveText()\n *         response shouldBe \"Echo: Hello, World!\"\n *     }\n * }\n * ```\n *\n * ## Collecting Multiple Messages\n *\n * ```kotlin\n * http {\n *     webSocket(\"/events\") { session ->\n *         // Collect messages with a timeout\n *         val messages = session.collectTexts(\n *             count = 5,\n *             timeout = 10.seconds\n *         )\n *         messages.size shouldBe 5\n *     }\n * }\n * ```\n *\n * @property session The underlying Ktor WebSocket session.\n */\n@HttpDsl\nclass StoveWebSocketSession(\n  @PublishedApi internal val session: DefaultClientWebSocketSession\n) {\n  /**\n   * Sends a text message through the WebSocket connection.\n   *\n   * @param message The text message to send.\n   */\n  suspend fun send(message: String) {\n    session.send(Frame.Text(message))\n  }\n\n  /**\n   * Sends a binary message through the WebSocket connection.\n   *\n   * @param data The binary data to send.\n   */\n  suspend fun send(data: ByteArray) {\n    session.send(Frame.Binary(true, data))\n  }\n\n  /**\n   * Sends a [StoveWebSocketMessage] through the WebSocket connection.\n   *\n   * @param message The message to send (either Text or Binary).\n   */\n  suspend fun send(message: StoveWebSocketMessage) {\n    when (message) {\n      is StoveWebSocketMessage.Text -> send(message.content)\n      is StoveWebSocketMessage.Binary -> send(message.content)\n    }\n  }\n\n  /**\n   * Receives the next text message from the WebSocket connection.\n   *\n   * @return The received text message, or null if the connection is closed\n   *         or a non-text frame is received.\n   */\n  suspend fun receiveText(): String? = session.incoming.receive().let { frame ->\n    when (frame) {\n      is Frame.Text -> frame.readText()\n      else -> null\n    }\n  }\n\n  /**\n   * Receives the next binary message from the WebSocket connection.\n   *\n   * @return The received binary data, or null if the connection is closed\n   *         or a non-binary frame is received.\n   */\n  suspend fun receiveBinary(): ByteArray? = session.incoming.receive().let { frame ->\n    when (frame) {\n      is Frame.Binary -> frame.readBytes()\n      else -> null\n    }\n  }\n\n  /**\n   * Receives the next message from the WebSocket connection as a [StoveWebSocketMessage].\n   *\n   * @return The received message (Text or Binary), or null if the connection is closed\n   *         or an unsupported frame type is received.\n   */\n  suspend fun receive(): StoveWebSocketMessage? = session.incoming.receive().let { frame ->\n    when (frame) {\n      is Frame.Text -> StoveWebSocketMessage.Text(frame.readText())\n      is Frame.Binary -> StoveWebSocketMessage.Binary(frame.readBytes())\n      else -> null\n    }\n  }\n\n  /**\n   * Attempts to receive a text message with a timeout.\n   *\n   * @param timeout The maximum duration to wait for a message.\n   * @return An [Option] containing the received text message, or [None] if the timeout\n   *         is reached or the connection is closed.\n   */\n  suspend fun receiveTextWithTimeout(timeout: Duration = 5.seconds): Option<String> = try {\n    withTimeout(timeout) {\n      receiveText().toOption()\n    }\n  } catch (_: TimeoutCancellationException) {\n    None\n  }\n\n  /**\n   * Attempts to receive a binary message with a timeout.\n   *\n   * @param timeout The maximum duration to wait for a message.\n   * @return An [Option] containing the received binary data, or [None] if the timeout\n   *         is reached or the connection is closed.\n   */\n  suspend fun receiveBinaryWithTimeout(timeout: Duration = 5.seconds): Option<ByteArray> = try {\n    withTimeout(timeout) {\n      receiveBinary().toOption()\n    }\n  } catch (_: TimeoutCancellationException) {\n    None\n  }\n\n  /**\n   * Collects text messages from the WebSocket connection.\n   *\n   * @param count The number of messages to collect.\n   * @param timeout The maximum duration to wait for all messages.\n   * @return A list of received text messages.\n   */\n  suspend fun collectTexts(\n    count: Int,\n    timeout: Duration = 30.seconds\n  ): List<String> = withTimeout(timeout) {\n    val messages = mutableListOf<String>()\n    repeat(count) {\n      receiveText()?.let { messages.add(it) }\n    }\n    messages\n  }\n\n  /**\n   * Collects binary messages from the WebSocket connection.\n   *\n   * @param count The number of messages to collect.\n   * @param timeout The maximum duration to wait for all messages.\n   * @return A list of received binary data.\n   */\n  suspend fun collectBinaries(\n    count: Int,\n    timeout: Duration = 30.seconds\n  ): List<ByteArray> = withTimeout(timeout) {\n    val messages = mutableListOf<ByteArray>()\n    repeat(count) {\n      receiveBinary()?.let { messages.add(it) }\n    }\n    messages\n  }\n\n  /**\n   * Creates a Flow of incoming text messages.\n   *\n   * The flow will emit messages until the WebSocket connection is closed.\n   *\n   * @return A [Flow] of text messages.\n   */\n  fun incomingTexts(): Flow<String> = session.incoming\n    .receiveAsFlow()\n    .filterIsInstance<Frame.Text>()\n    .map { it.readText() }\n\n  /**\n   * Creates a Flow of incoming binary messages.\n   *\n   * The flow will emit messages until the WebSocket connection is closed.\n   *\n   * @return A [Flow] of binary data.\n   */\n  fun incomingBinaries(): Flow<ByteArray> = session.incoming\n    .receiveAsFlow()\n    .filterIsInstance<Frame.Binary>()\n    .map { it.readBytes() }\n\n  /**\n   * Creates a Flow of all incoming messages as [StoveWebSocketMessage].\n   *\n   * The flow will emit messages until the WebSocket connection is closed.\n   *\n   * @return A [Flow] of [StoveWebSocketMessage].\n   */\n  fun incoming(): Flow<StoveWebSocketMessage> = session.incoming\n    .receiveAsFlow()\n    .mapNotNull { frame ->\n      when (frame) {\n        is Frame.Text -> StoveWebSocketMessage.Text(frame.readText())\n        is Frame.Binary -> StoveWebSocketMessage.Binary(frame.readBytes())\n        else -> null\n      }\n    }\n\n  /**\n   * Closes the WebSocket connection gracefully.\n   *\n   * @param reason Optional close reason message.\n   */\n  suspend fun close(reason: String = \"Test completed\") {\n    session.close(CloseReason(CloseReason.Codes.NORMAL, reason))\n  }\n\n  /**\n   * Provides access to the underlying Ktor WebSocket session for advanced use cases.\n   *\n   * @param block The block to execute with the underlying session.\n   * @return The result of the block.\n   */\n  suspend fun <T> underlyingSession(\n    block: suspend DefaultClientWebSocketSession.() -> T\n  ): T = block(session)\n}\n"
  },
  {
    "path": "lib/stove-http/src/test/kotlin/com/trendyol/stove/http/HttpSystemTests.kt",
    "content": "package com.trendyol.stove.http\n\nimport arrow.core.*\nimport com.github.tomakehurst.wiremock.client.WireMock\nimport com.github.tomakehurst.wiremock.client.WireMock.*\nimport com.github.tomakehurst.wiremock.matching.MultipartValuePattern\nimport com.trendyol.stove.ConsoleSpec\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.http.HttpSystem.Companion.client\nimport com.trendyol.stove.system.PortFinder\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.*\nimport io.kotest.matchers.string.shouldContain\nimport io.ktor.client.request.*\nimport io.ktor.http.*\nimport kotlinx.coroutines.flow.toList\nimport java.time.Instant\nimport java.util.*\n\nclass NoApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) {\n    // do nothing\n  }\n\n  override suspend fun stop() {\n    // do nothing\n  }\n}\n\nprivate val WIREMOCK_PORT = PortFinder.findAvailablePort()\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:$WIREMOCK_PORT\"\n          )\n        }\n\n        wiremock {\n          WireMockSystemOptions(WIREMOCK_PORT)\n        }\n\n        applicationUnderTest(NoApplication())\n      }.run()\n\n  override suspend fun afterProject(): Unit = com.trendyol.stove.system.Stove\n    .stop()\n}\n\nclass HttpSystemTests :\n  FunSpec({\n    test(\"DELETE and expect bodiless response\") {\n      stove {\n        wiremock {\n          mockDelete(\"/delete-success\", statusCode = 200)\n          mockDelete(\"/delete-fail\", statusCode = 400)\n        }\n\n        http {\n          deleteAndExpectBodilessResponse(\"/delete-success\", None) { actual ->\n            actual.status shouldBe 200\n          }\n          deleteAndExpectBodilessResponse(\"/delete-fail\", None) { actual ->\n            actual.status shouldBe 400\n          }\n        }\n      }\n    }\n\n    test(\"PUT and expect bodiless/JSON response\") {\n      val expectedPutDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockPut(\"/put-with-response-body\", 200, None, responseBody = TestDto(expectedPutDtoName).some())\n          mockPut(\"/put-without-response-body\", 200, None, responseBody = None)\n        }\n\n        http {\n\n          putAndExpectBodilessResponse(\"/put-without-response-body\", None, None) { actual ->\n            actual.status shouldBe 200\n          }\n          putAndExpectJson<TestDto>(\"/put-with-response-body\") { actual ->\n            actual.name shouldBe expectedPutDtoName\n          }\n        }\n      }\n    }\n\n    test(\"POST and expect bodiless/JSON response\") {\n      val expectedPOSTDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockPost(\"/post-with-response-body\", 200, None, responseBody = TestDto(expectedPOSTDtoName).some())\n          mockPost(\"/post-without-response-body\", 200, None, responseBody = None)\n        }\n\n        http {\n          postAndExpectBodilessResponse(\"/post-without-response-body\", None, None) { actual ->\n            actual.status shouldBe 200\n          }\n          postAndExpectJson<TestDto>(\"/post-with-response-body\") { actual ->\n            actual.name shouldBe expectedPOSTDtoName\n          }\n        }\n      }\n    }\n\n    test(\"PATCH and expect bodiless/JSON response\") {\n      val expectedPatchDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockPatch(\"/patch-with-response-body\", 200, None, responseBody = TestDto(expectedPatchDtoName).some())\n          mockPatch(\"/patch-without-response-body\", 200, None, responseBody = None)\n        }\n\n        http {\n\n          patchAndExpectBodilessResponse(\"/patch-without-response-body\", None, None) { actual ->\n            actual.status shouldBe 200\n          }\n          patchAndExpectJson<TestDto>(\"/patch-with-response-body\") { actual ->\n            actual.name shouldBe expectedPatchDtoName\n          }\n        }\n      }\n    }\n\n    test(\"GET and expect JSON response\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockGet(\"/get\", 200, responseBody = TestDto(expectedGetDtoName).some())\n          mockGet(\"/get-many\", 200, responseBody = listOf(TestDto(expectedGetDtoName)).some())\n        }\n\n        http {\n          get<TestDto>(\"/get\") { actual ->\n            actual.name shouldBe expectedGetDtoName\n          }\n\n          getMany<TestDto>(\"/get-many\") { actual ->\n            actual[0] shouldBe TestDto(expectedGetDtoName)\n          }\n\n          get<List<TestDto>>(\"/get-many\") { actual ->\n            actual[0] shouldBe TestDto(expectedGetDtoName)\n          }\n        }\n      }\n    }\n\n    test(\"getResponse and expect body\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockGet(\"/get\", 200, responseBody = TestDto(expectedGetDtoName).some())\n        }\n\n        http {\n          getResponse<TestDto>(\"/get\") { actual ->\n            actual.body().name shouldBe expectedGetDtoName\n          }\n        }\n      }\n    }\n\n    test(\"getResponse and expect bodiless\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockGet(\"/get\", 200, responseBody = TestDto(expectedGetDtoName).some())\n        }\n\n        http {\n          getBodilessResponse(\"/get\") { actual ->\n            actual.status shouldBe 200\n            actual::class shouldBe StoveHttpResponse.Bodiless::class\n          }\n        }\n      }\n    }\n\n    test(\"put and expect body\") {\n      val expectedPutDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockPut(\"/put-with-response-body\", 200, None, responseBody = TestDto(expectedPutDtoName).some())\n        }\n\n        http {\n          putAndExpectBody<TestDto>(\"/put-with-response-body\") { actual ->\n            actual.body().name shouldBe expectedPutDtoName\n          }\n        }\n      }\n    }\n\n    test(\"post and expect body\") {\n      val expectedPostDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockPost(\"/post-with-response-body\", 200, None, responseBody = TestDto(expectedPostDtoName).some())\n        }\n\n        http {\n          postAndExpectBody<TestDto>(\"/post-with-response-body\") { actual ->\n            actual.body().name shouldBe expectedPostDtoName\n          }\n        }\n      }\n    }\n\n    test(\"patch and expect body\") {\n      val expectedPatchDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockPatch(\"/patch-with-response-body\", 200, None, responseBody = TestDto(expectedPatchDtoName).some())\n        }\n\n        http {\n          patchAndExpectBody<TestDto>(\"/patch-with-response-body\") { actual ->\n            actual.body().name shouldBe expectedPatchDtoName\n          }\n        }\n      }\n    }\n\n    test(\"get with query params should work\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockGet(\"/get?param=1\", 200, responseBody = TestDto(expectedGetDtoName).some())\n        }\n\n        http {\n          get<TestDto>(\"/get\", queryParams = mapOf(\"param\" to \"1\")) { actual ->\n            actual.name shouldBe expectedGetDtoName\n          }\n        }\n      }\n    }\n\n    test(\"multipart post should work\") {\n      val expectedPostDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockPostConfigure(\"/post-with-multipart\") { req, _ ->\n            req.withMultipartRequestBody(\n              aMultipart()\n                .matchingType(MultipartValuePattern.MatchingType.ANY)\n                .withHeader(\"Content-Disposition\", equalTo(\"form-data; name=name\"))\n                .withBody(equalTo(expectedPostDtoName))\n            )\n            req.withMultipartRequestBody(\n              aMultipart()\n                .matchingType(MultipartValuePattern.MatchingType.ANY)\n                .withHeader(\"Content-Disposition\", equalTo(\"form-data; name=file; filename=file.png\"))\n                .withBody(equalTo(\"file\"))\n            )\n            req.willReturn(aResponse().withStatus(200).withBody(\"hoi!\"))\n          }\n        }\n\n        http {\n          postMultipartAndExpectResponse<String>(\n            \"/post-with-multipart\",\n            body = listOf(\n              StoveMultiPartContent.Text(\"name\", expectedPostDtoName),\n              StoveMultiPartContent.File(\n                param = \"file\",\n                fileName = \"file.png\",\n                content = \"file\".toByteArray(),\n                contentType = \"application/octet-stream\"\n              )\n            )\n          ) { actual ->\n            actual.body() shouldBe \"hoi!\"\n            actual.status shouldBe 200\n          }\n        }\n      }\n    }\n\n    test(\"java time instant should work\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockGet(\"/get\", 200, responseBody = TestDtoWithInstant(expectedGetDtoName, Instant.now()).some())\n        }\n\n        http {\n          get<TestDtoWithInstant>(\"/get\") { actual ->\n            actual.name shouldBe expectedGetDtoName\n          }\n        }\n      }\n    }\n\n    test(\"keep path segments as is\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockGet(\"/get?path=1\", 200, responseBody = TestDto(expectedGetDtoName).some())\n        }\n\n        http {\n          get<TestDto>(\"/get?path=1\") { actual ->\n            actual.name shouldBe expectedGetDtoName\n          }\n\n          client() shouldNotBe null\n\n          client { baseUrl ->\n            val resp = get(\n              baseUrl\n                .apply {\n                  path(\"/get\")\n                  parameters.append(\"path\", \"1\")\n                }.build()\n            )\n            resp.status shouldBe HttpStatusCode.OK\n          }\n        }\n      }\n    }\n\n    test(\"behavioural tests\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          behaviourFor(\"/get-behaviour\", WireMock::get) {\n            initially {\n              aResponse()\n                .withStatus(503)\n                .withBody(\"Service unavailable\")\n            }\n            then {\n              aResponse()\n                .withHeader(\"Content-Type\", \"application/json\")\n                .withStatus(200)\n                .withBody(it.serialize(TestDto(expectedGetDtoName)))\n            }\n          }\n        }\n\n        http {\n          this.getResponse<Any>(\"/get-behaviour\") { actual ->\n            actual.status shouldBe 503\n          }\n\n          get<TestDto>(\"/get-behaviour\") { actual ->\n            actual.name shouldBe expectedGetDtoName\n          }\n        }\n      }\n    }\n\n    test(\"if there is no initial step, can not place `then`\") {\n      stove {\n        wiremock {\n          behaviourFor(\"/get-behaviour\", WireMock::get) {\n            shouldThrow<IllegalStateException> {\n              then {\n                aResponse()\n                  .withHeader(\"Content-Type\", \"application/json\")\n                  .withStatus(200)\n                  .withBody(it.serialize(TestDto(UUID.randomUUID().toString())))\n              }\n            }\n          }\n        }\n      }\n    }\n\n    test(\"should only call initially once\") {\n      stove {\n        wiremock {\n          behaviourFor(\"/get-behaviour\", WireMock::get) {\n            initially {\n              aResponse()\n                .withStatus(503)\n                .withBody(\"Service unavailable\")\n            }\n            shouldThrow<IllegalStateException> {\n              initially {\n                aResponse()\n                  .withStatus(503)\n                  .withBody(\"Service unavailable\")\n              }\n            }\n          }\n        }\n      }\n    }\n\n    test(\"serialize to application/x-ndjson\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      val items = (1..10).map { TestDto(expectedGetDtoName) }\n\n      stove {\n        wiremock {\n          mockGetConfigure(\"/get-ndjson\") { builder, serde ->\n            builder.willReturn(\n              aResponse()\n                .withHeader(\"Content-Type\", \"application/x-ndjson\")\n                .withBody(serde.serializeToStreamJson(items))\n            )\n          }\n        }\n\n        http {\n          readJsonStream<TestDto>(\"/get-ndjson\") { actual ->\n            val collected = actual.toList()\n            collected.size shouldBe 10\n            collected.forEach { it.name shouldBe expectedGetDtoName }\n          }\n        }\n      }\n    }\n\n    test(\"get with headers\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      val headers = mapOf(\"Custom-Header\" to \"CustomValue\")\n      stove {\n        wiremock {\n          mockGet(\"/get?param=1\", 200, responseBody = TestDto(expectedGetDtoName).some(), responseHeaders = headers)\n        }\n\n        http {\n          getResponse<TestDto>(\"/get\", queryParams = mapOf(\"param\" to \"1\")) { actual ->\n            actual.body().name shouldBe expectedGetDtoName\n            actual.headers[\"custom-header\"] shouldBe listOf(\"CustomValue\")\n          }\n        }\n      }\n    }\n  })\n\nclass HttpConsoleTesting :\n  ConsoleSpec({ capturedOutput ->\n    test(\"should return error when request bodies do not match\") {\n      val expectedGetDtoName = UUID.randomUUID().toString()\n      stove {\n        wiremock {\n          mockPost(\"/post-with-response-body\", 200, requestBody = TestDto(\"lol\").some(), responseBody = TestDto(expectedGetDtoName).some())\n        }\n\n        shouldThrow<Throwable> {\n          http {\n            postAndExpectJson<TestDto>(\"/post-with-response-body2\", body = TestDto(\"no-match\").some()) { actual ->\n              actual.name shouldBe expectedGetDtoName\n            }\n          }\n        }\n\n        capturedOutput.out shouldContain \"[equalToJson]                                              |                                                     <<<<< Body does not match\\n\"\n      }\n    }\n  })\n\ndata class TestDto(\n  val name: String\n)\n\ndata class TestDtoWithInstant(\n  val name: String,\n  val instant: Instant\n)\n"
  },
  {
    "path": "lib/stove-http/src/test/kotlin/com/trendyol/stove/http/WebSocketTests.kt",
    "content": "package com.trendyol.stove.http\n\nimport arrow.core.some\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.*\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.ktor.server.application.*\nimport io.ktor.server.engine.*\nimport io.ktor.server.netty.*\nimport io.ktor.server.routing.*\nimport io.ktor.server.websocket.*\nimport io.ktor.websocket.*\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.*\nimport kotlin.time.Duration.Companion.seconds\n\nprivate const val WS_PORT = 9877\n\n/**\n * Application under test that runs a simple WebSocket echo server.\n */\nclass WebSocketEchoServer : ApplicationUnderTest<Unit> {\n  private lateinit var server: EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>\n\n  override suspend fun start(configurations: List<String>) {\n    server = embeddedServer(Netty, port = WS_PORT) {\n      install(WebSockets) {\n        pingPeriodMillis = 15_000\n        timeoutMillis = 15_000\n      }\n      routing {\n        // Echo endpoint - echoes back whatever is sent\n        webSocket(\"/echo\") {\n          for (frame in incoming) {\n            when (frame) {\n              is Frame.Text -> {\n                val text = frame.readText()\n                send(Frame.Text(\"Echo: $text\"))\n              }\n\n              is Frame.Binary -> {\n                val bytes = frame.readBytes()\n                send(Frame.Binary(true, bytes))\n              }\n\n              else -> {}\n            }\n          }\n        }\n\n        // Broadcast endpoint - sends multiple messages\n        webSocket(\"/broadcast\") {\n          for (i in 1..5) {\n            send(Frame.Text(\"Message $i\"))\n            delay(10)\n          }\n          close(CloseReason(CloseReason.Codes.NORMAL, \"Broadcast complete\"))\n        }\n\n        // Auth endpoint - checks for authorization header\n        webSocket(\"/secure\") {\n          val token = call.request.headers[\"Authorization\"]\n          if (token != null && token.startsWith(\"Bearer \")) {\n            send(Frame.Text(\"Authenticated: ${token.substringAfter(\"Bearer \")}\"))\n          } else {\n            send(Frame.Text(\"Unauthorized\"))\n          }\n          close(CloseReason(CloseReason.Codes.NORMAL, \"Auth check complete\"))\n        }\n\n        // Binary endpoint - echoes binary data\n        webSocket(\"/binary\") {\n          for (frame in incoming) {\n            when (frame) {\n              is Frame.Binary -> {\n                val bytes = frame.readBytes()\n                send(Frame.Binary(true, bytes.reversedArray()))\n              }\n\n              else -> {}\n            }\n          }\n        }\n      }\n    }\n    server.start(wait = false)\n    delay(500)\n  }\n\n  override suspend fun stop() {\n    server.stop(1000, 2000)\n  }\n}\n\nclass WebSocketTests :\n  FunSpec({\n    lateinit var wsStove: Stove\n\n    beforeSpec {\n      wsStove = Stove()\n        .with {\n          httpClient {\n            HttpClientSystemOptions(\n              baseUrl = \"http://localhost:$WS_PORT\"\n            )\n          }\n          applicationUnderTest(WebSocketEchoServer())\n        }\n      wsStove.run()\n    }\n\n    afterSpec {\n      wsStove.close()\n    }\n\n    test(\"should send and receive text messages via WebSocket\") {\n      stove {\n        http {\n          webSocket(\"/echo\") {\n            send(\"Hello, WebSocket!\")\n            val response = receiveText()\n            response shouldBe \"Echo: Hello, WebSocket!\"\n          }\n        }\n      }\n    }\n\n    test(\"should send and receive multiple messages\") {\n      stove {\n        http {\n          webSocket(\"/echo\") {\n            send(\"First\")\n            receiveText() shouldBe \"Echo: First\"\n\n            send(\"Second\")\n            receiveText() shouldBe \"Echo: Second\"\n\n            send(\"Third\")\n            receiveText() shouldBe \"Echo: Third\"\n          }\n        }\n      }\n    }\n\n    test(\"should collect multiple messages from broadcast endpoint\") {\n      stove {\n        http {\n          webSocket(\"/broadcast\") {\n            val messages = collectTexts(count = 5, timeout = 10.seconds)\n            messages shouldHaveSize 5\n            messages[0] shouldBe \"Message 1\"\n            messages[4] shouldBe \"Message 5\"\n          }\n        }\n      }\n    }\n\n    test(\"should handle authentication via headers\") {\n      stove {\n        http {\n          webSocket(\n            uri = \"/secure\",\n            token = \"my-secret-token\".some()\n          ) {\n            val response = receiveText()\n            response shouldBe \"Authenticated: my-secret-token\"\n          }\n        }\n      }\n    }\n\n    test(\"should handle custom headers\") {\n      stove {\n        http {\n          webSocket(\n            uri = \"/secure\",\n            headers = mapOf(\"Authorization\" to \"Bearer custom-token\")\n          ) {\n            val response = receiveText()\n            response shouldBe \"Authenticated: custom-token\"\n          }\n        }\n      }\n    }\n\n    test(\"should send and receive binary data\") {\n      stove {\n        http {\n          webSocket(\"/binary\") {\n            val data = byteArrayOf(1, 2, 3, 4, 5)\n            send(data)\n\n            val response = receiveBinary()\n            response shouldBe byteArrayOf(5, 4, 3, 2, 1)\n          }\n        }\n      }\n    }\n\n    test(\"should use StoveWebSocketMessage sealed class\") {\n      stove {\n        http {\n          webSocket(\"/echo\") {\n            send(StoveWebSocketMessage.Text(\"Using sealed class\"))\n            val response = receive()\n            response shouldBe StoveWebSocketMessage.Text(\"Echo: Using sealed class\")\n          }\n        }\n      }\n    }\n\n    test(\"should use incoming flow for streaming messages\") {\n      stove {\n        http {\n          webSocket(\"/broadcast\") {\n            val messages = incomingTexts()\n              .take(3)\n              .toList()\n\n            messages shouldHaveSize 3\n            messages[0] shouldBe \"Message 1\"\n            messages[1] shouldBe \"Message 2\"\n            messages[2] shouldBe \"Message 3\"\n          }\n        }\n      }\n    }\n\n    test(\"should receive text with timeout\") {\n      stove {\n        http {\n          webSocket(\"/echo\") {\n            send(\"Quick message\")\n            val response = receiveTextWithTimeout(5.seconds)\n            response.isSome() shouldBe true\n            response.getOrNull() shouldBe \"Echo: Quick message\"\n          }\n        }\n      }\n    }\n\n    test(\"webSocketExpect should work as alias\") {\n      stove {\n        http {\n          webSocketExpect(\"/echo\") {\n            send(\"Test\")\n            receiveText() shouldBe \"Echo: Test\"\n          }\n        }\n      }\n    }\n\n    test(\"webSocketRaw should provide access to underlying session\") {\n      stove {\n        http {\n          webSocketRaw(\"/echo\") {\n            send(Frame.Text(\"Raw frame\"))\n            val frame = incoming.receive()\n            (frame as Frame.Text).readText() shouldBe \"Echo: Raw frame\"\n          }\n        }\n      }\n    }\n\n    test(\"should properly close connection\") {\n      stove {\n        http {\n          webSocket(\"/echo\") {\n            send(\"Before close\")\n            receiveText() shouldBe \"Echo: Before close\"\n            close(\"Test completed\")\n          }\n        }\n      }\n    }\n\n    test(\"should access underlying session for advanced operations\") {\n      stove {\n        http {\n          webSocket(\"/echo\") {\n            underlyingSession {\n              send(Frame.Text(\"Advanced\"))\n              val frame = incoming.receive()\n              (frame as Frame.Text).readText() shouldBe \"Echo: Advanced\"\n            }\n          }\n        }\n      }\n    }\n\n    test(\"should send StoveWebSocketMessage.Binary and receive binary response\") {\n      stove {\n        http {\n          webSocket(\"/binary\") {\n            val data = byteArrayOf(10, 20, 30)\n            send(StoveWebSocketMessage.Binary(data))\n            val response = receiveBinary()\n            response shouldBe byteArrayOf(30, 20, 10)\n          }\n        }\n      }\n    }\n\n    test(\"should receive binary with timeout\") {\n      stove {\n        http {\n          webSocket(\"/binary\") {\n            send(byteArrayOf(1, 2, 3))\n            val response = receiveBinaryWithTimeout(5.seconds)\n            response.isSome() shouldBe true\n            response.getOrNull() shouldBe byteArrayOf(3, 2, 1)\n          }\n        }\n      }\n    }\n\n    test(\"should collect multiple binary messages\") {\n      stove {\n        http {\n          webSocket(\"/binary\") {\n            repeat(3) { i ->\n              send(byteArrayOf((i + 1).toByte()))\n            }\n            val responses = collectBinaries(count = 3, timeout = 10.seconds)\n            responses shouldHaveSize 3\n            responses[0] shouldBe byteArrayOf(1)\n            responses[1] shouldBe byteArrayOf(2)\n            responses[2] shouldBe byteArrayOf(3)\n          }\n        }\n      }\n    }\n\n    test(\"should use incoming flow for binary streaming\") {\n      stove {\n        http {\n          webSocket(\"/binary\") {\n            repeat(3) { i ->\n              send(byteArrayOf((i + 10).toByte()))\n            }\n            val messages = incomingBinaries()\n              .take(3)\n              .toList()\n\n            messages shouldHaveSize 3\n          }\n        }\n      }\n    }\n\n    test(\"should use generic incoming flow for mixed messages\") {\n      stove {\n        http {\n          webSocket(\"/echo\") {\n            send(\"Test message\")\n            val messages = incoming()\n              .take(1)\n              .toList()\n\n            messages shouldHaveSize 1\n            (messages[0] is StoveWebSocketMessage.Text) shouldBe true\n            (messages[0] as StoveWebSocketMessage.Text).content shouldBe \"Echo: Test message\"\n          }\n        }\n      }\n    }\n  })\n\nclass StoveWebSocketMessageTests :\n  FunSpec({\n    test(\"Binary equals should return true for same content\") {\n      val a = StoveWebSocketMessage.Binary(byteArrayOf(1, 2, 3))\n      val b = StoveWebSocketMessage.Binary(byteArrayOf(1, 2, 3))\n      (a == b) shouldBe true\n    }\n\n    test(\"Binary equals should return false for different content\") {\n      val a = StoveWebSocketMessage.Binary(byteArrayOf(1, 2, 3))\n      val b = StoveWebSocketMessage.Binary(byteArrayOf(4, 5, 6))\n      (a == b) shouldBe false\n    }\n\n    test(\"Binary equals should return true for same instance\") {\n      val a = StoveWebSocketMessage.Binary(byteArrayOf(1, 2, 3))\n      (a == a) shouldBe true\n    }\n\n    @Suppress(\"EqualsNullCall\")\n    test(\"Binary equals should return false for different type\") {\n      val a = StoveWebSocketMessage.Binary(byteArrayOf(1, 2, 3))\n      a.equals(\"not binary\") shouldBe false\n    }\n\n    test(\"Binary hashCode should be consistent for same content\") {\n      val a = StoveWebSocketMessage.Binary(byteArrayOf(1, 2, 3))\n      val b = StoveWebSocketMessage.Binary(byteArrayOf(1, 2, 3))\n      a.hashCode() shouldBe b.hashCode()\n    }\n\n    test(\"Binary hashCode should differ for different content\") {\n      val a = StoveWebSocketMessage.Binary(byteArrayOf(1, 2, 3))\n      val b = StoveWebSocketMessage.Binary(byteArrayOf(4, 5, 6))\n      (a.hashCode() != b.hashCode()) shouldBe true\n    }\n\n    test(\"Text should have correct content\") {\n      val msg = StoveWebSocketMessage.Text(\"hello\")\n      msg.content shouldBe \"hello\"\n    }\n  })\n\nclass WebSocketUrlBuildingTests :\n  FunSpec({\n    test(\"should convert http to ws\") {\n      val testSystem = Stove()\n      testSystem.with {\n        httpClient {\n          HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n        }\n      }\n      val httpSystem = testSystem.getOrNone<HttpSystem>().getOrNull()!!\n      httpSystem.buildWebSocketUrl(\"/chat\") shouldBe \"ws://localhost:8080/chat\"\n    }\n\n    test(\"should convert https to wss\") {\n      val testSystem = Stove()\n      testSystem.with {\n        httpClient {\n          HttpClientSystemOptions(baseUrl = \"https://localhost:8080\")\n        }\n      }\n      val httpSystem = testSystem.getOrNone<HttpSystem>().getOrNull()!!\n      httpSystem.buildWebSocketUrl(\"/chat\") shouldBe \"wss://localhost:8080/chat\"\n    }\n\n    test(\"should handle uri without leading slash\") {\n      val testSystem = Stove()\n      testSystem.with {\n        httpClient {\n          HttpClientSystemOptions(baseUrl = \"http://localhost:8080\")\n        }\n      }\n      val httpSystem = testSystem.getOrNone<HttpSystem>().getOrNull()!!\n      httpSystem.buildWebSocketUrl(\"chat\") shouldBe \"ws://localhost:8080/chat\"\n    }\n\n    test(\"should handle baseUrl without protocol\") {\n      val testSystem = Stove()\n      testSystem.with {\n        httpClient {\n          HttpClientSystemOptions(baseUrl = \"localhost:8080\")\n        }\n      }\n      val httpSystem = testSystem.getOrNone<HttpSystem>().getOrNull()!!\n      httpSystem.buildWebSocketUrl(\"/chat\") shouldBe \"ws://localhost:8080/chat\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove-http/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.http.StoveConfig\n"
  },
  {
    "path": "lib/stove-http/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "lib/stove-kafka/api/stove-kafka.api",
    "content": "public final class com/trendyol/stove/kafka/AcknowledgedMessage : com/squareup/wire/Message {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/AcknowledgedMessage$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;IJLjava/lang/String;Lokio/ByteString;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;IJLjava/lang/String;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;IJLjava/lang/String;Lokio/ByteString;)Lcom/trendyol/stove/kafka/AcknowledgedMessage;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/AcknowledgedMessage;Ljava/lang/String;Ljava/lang/String;IJLjava/lang/String;Lokio/ByteString;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/AcknowledgedMessage;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getException ()Ljava/lang/String;\n\tpublic final fun getId ()Ljava/lang/String;\n\tpublic final fun getOffset ()J\n\tpublic final fun getPartition ()I\n\tpublic final fun getTopic ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic synthetic fun newBuilder ()Lcom/squareup/wire/Message$Builder;\n\tpublic synthetic fun newBuilder ()Ljava/lang/Void;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/AcknowledgedMessage$Companion {\n}\n\npublic final class com/trendyol/stove/kafka/Caching {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/kafka/Caching;\n\tpublic final fun of ()Lcom/github/benmanes/caffeine/cache/Cache;\n}\n\npublic final class com/trendyol/stove/kafka/CommittedMessage : com/squareup/wire/Message {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/CommittedMessage$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;IJLjava/lang/String;Lokio/ByteString;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;IJLjava/lang/String;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;IJLjava/lang/String;Lokio/ByteString;)Lcom/trendyol/stove/kafka/CommittedMessage;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/CommittedMessage;Ljava/lang/String;Ljava/lang/String;IJLjava/lang/String;Lokio/ByteString;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/CommittedMessage;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getId ()Ljava/lang/String;\n\tpublic final fun getMetadata ()Ljava/lang/String;\n\tpublic final fun getOffset ()J\n\tpublic final fun getPartition ()I\n\tpublic final fun getTopic ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic synthetic fun newBuilder ()Lcom/squareup/wire/Message$Builder;\n\tpublic synthetic fun newBuilder ()Ljava/lang/Void;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/CommittedMessage$Companion {\n}\n\npublic final class com/trendyol/stove/kafka/CommittedRecord {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;JI)V\n\tpublic final fun getMetadata ()Ljava/lang/String;\n\tpublic final fun getOffset ()J\n\tpublic final fun getPartition ()I\n\tpublic final fun getTopic ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/ConsumedMessage : com/squareup/wire/Message {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/ConsumedMessage$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Lokio/ByteString;Ljava/lang/String;IJLjava/lang/String;Ljava/util/Map;Lokio/ByteString;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Lokio/ByteString;Ljava/lang/String;IJLjava/lang/String;Ljava/util/Map;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun copy (Ljava/lang/String;Lokio/ByteString;Ljava/lang/String;IJLjava/lang/String;Ljava/util/Map;Lokio/ByteString;)Lcom/trendyol/stove/kafka/ConsumedMessage;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/ConsumedMessage;Ljava/lang/String;Lokio/ByteString;Ljava/lang/String;IJLjava/lang/String;Ljava/util/Map;Lokio/ByteString;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/ConsumedMessage;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getHeaders ()Ljava/util/Map;\n\tpublic final fun getId ()Ljava/lang/String;\n\tpublic final fun getKey ()Ljava/lang/String;\n\tpublic final fun getMessage ()Lokio/ByteString;\n\tpublic final fun getOffset ()J\n\tpublic final fun getPartition ()I\n\tpublic final fun getTopic ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic synthetic fun newBuilder ()Lcom/squareup/wire/Message$Builder;\n\tpublic synthetic fun newBuilder ()Ljava/lang/Void;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/ConsumedMessage$Companion {\n}\n\npublic final class com/trendyol/stove/kafka/ConsumedRecord {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;[BLjava/util/Map;JI)V\n\tpublic final fun getHeaders ()Ljava/util/Map;\n\tpublic final fun getKey ()Ljava/lang/String;\n\tpublic final fun getOffset ()J\n\tpublic final fun getPartition ()I\n\tpublic final fun getTopic ()Ljava/lang/String;\n\tpublic final fun getValue ()[B\n}\n\npublic final class com/trendyol/stove/kafka/CoroutinesKt {\n\tpublic static final fun getAsExecutor (Lkotlinx/coroutines/CoroutineScope;)Ljava/util/concurrent/Executor;\n\tpublic static final fun getAsExecutorService (Lkotlinx/coroutines/CoroutineScope;)Ljava/util/concurrent/ExecutorService;\n}\n\npublic final class com/trendyol/stove/kafka/EmbeddedKafkaRuntime : com/trendyol/stove/system/abstractions/SystemRuntime {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/kafka/EmbeddedKafkaRuntime;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/ExtensionsKt {\n\tpublic static final fun metadata (Lcom/trendyol/stove/kafka/ConsumedMessage;)Lcom/trendyol/stove/messaging/MessageMetadata;\n\tpublic static final fun metadata (Lcom/trendyol/stove/kafka/PublishedMessage;)Lcom/trendyol/stove/messaging/MessageMetadata;\n\tpublic static final fun toProperties (Ljava/util/Map;)Ljava/util/Properties;\n}\n\npublic final class com/trendyol/stove/kafka/GrpcStoveKafkaObserverServiceClient : com/trendyol/stove/kafka/StoveKafkaObserverServiceClient {\n\tpublic fun <init> (Lcom/squareup/wire/GrpcClient;)V\n\tpublic fun healthCheck ()Lcom/squareup/wire/GrpcCall;\n\tpublic fun onAcknowledgedMessage ()Lcom/squareup/wire/GrpcCall;\n\tpublic fun onCommittedMessage ()Lcom/squareup/wire/GrpcCall;\n\tpublic fun onConsumedMessage ()Lcom/squareup/wire/GrpcCall;\n\tpublic fun onPublishedMessage ()Lcom/squareup/wire/GrpcCall;\n}\n\npublic final class com/trendyol/stove/kafka/HealthCheckRequest : com/squareup/wire/Message {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/HealthCheckRequest$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Lokio/ByteString;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun copy (Ljava/lang/String;Lokio/ByteString;)Lcom/trendyol/stove/kafka/HealthCheckRequest;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/HealthCheckRequest;Ljava/lang/String;Lokio/ByteString;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/HealthCheckRequest;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getService ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic synthetic fun newBuilder ()Lcom/squareup/wire/Message$Builder;\n\tpublic synthetic fun newBuilder ()Ljava/lang/Void;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/HealthCheckRequest$Companion {\n}\n\npublic final class com/trendyol/stove/kafka/HealthCheckResponse : com/squareup/wire/Message {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/HealthCheckResponse$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;Lokio/ByteString;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun copy (Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;Lokio/ByteString;)Lcom/trendyol/stove/kafka/HealthCheckResponse;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/HealthCheckResponse;Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;Lokio/ByteString;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/HealthCheckResponse;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getStatus ()Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;\n\tpublic fun hashCode ()I\n\tpublic synthetic fun newBuilder ()Lcom/squareup/wire/Message$Builder;\n\tpublic synthetic fun newBuilder ()Ljava/lang/Void;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/HealthCheckResponse$Companion {\n}\n\npublic final class com/trendyol/stove/kafka/HealthCheckResponse$ServingStatus : java/lang/Enum, com/squareup/wire/WireEnum {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus$Companion;\n\tpublic static final field NOT_SERVING Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;\n\tpublic static final field SERVICE_UNKNOWN Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;\n\tpublic static final field SERVING Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;\n\tpublic static final field UNKNOWN Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;\n\tpublic static final fun fromValue (I)Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;\n\tpublic static fun getEntries ()Lkotlin/enums/EnumEntries;\n\tpublic fun getValue ()I\n\tpublic static fun valueOf (Ljava/lang/String;)Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;\n\tpublic static fun values ()[Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;\n}\n\npublic final class com/trendyol/stove/kafka/HealthCheckResponse$ServingStatus$Companion {\n\tpublic final fun fromValue (I)Lcom/trendyol/stove/kafka/HealthCheckResponse$ServingStatus;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/KafkaContainerOptions$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/util/List;\n\tpublic final fun component5 ()Ljava/lang/String;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component7 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/kafka/KafkaContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/KafkaContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/KafkaContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic final fun getPorts ()Ljava/util/List;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaContainerOptions$Companion {\n\tpublic final fun getDEFAULT_KAFKA_PORTS ()Ljava/util/List;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaContext {\n\tpublic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/kafka/KafkaSystemOptions;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/kafka/KafkaSystemOptions;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic final fun component2 ()Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/kafka/KafkaSystemOptions;Ljava/lang/String;)Lcom/trendyol/stove/kafka/KafkaContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/KafkaContext;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/kafka/KafkaSystemOptions;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/KafkaContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getKeyName ()Ljava/lang/String;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaContextKt {\n\tpublic static final fun kafka-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun kafka-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun kafka-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun kafka-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBootstrapServers ()Ljava/lang/String;\n\tpublic final fun getInterceptorClass ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaMigrationContext {\n\tpublic fun <init> (Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/kafka/KafkaSystemOptions;)V\n\tpublic final fun component1 ()Lorg/apache/kafka/clients/admin/Admin;\n\tpublic final fun component2 ()Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n\tpublic final fun copy (Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/kafka/KafkaSystemOptions;)Lcom/trendyol/stove/kafka/KafkaMigrationContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/KafkaMigrationContext;Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/kafka/KafkaSystemOptions;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/KafkaMigrationContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAdmin ()Lorg/apache/kafka/clients/admin/Admin;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/AfterRunAware, com/trendyol/stove/system/abstractions/BeforeRunAware, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/KafkaSystem$Companion;\n\tpublic field sink Lcom/trendyol/stove/kafka/intercepting/StoveMessageSink;\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/kafka/KafkaContext;)V\n\tpublic final fun adminOperations (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun afterRun (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun assertKafkaMessage-WPi__2c (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun beforeRun (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic final fun consumer-IY8X1Ik (Ljava/lang/String;ZLjava/lang/String;ZLkotlin/jvm/functions/Function1;Lorg/apache/kafka/common/serialization/Deserializer;Lorg/apache/kafka/common/serialization/Deserializer;JJLjava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun consumer-IY8X1Ik$default (Lcom/trendyol/stove/kafka/KafkaSystem;Ljava/lang/String;ZLjava/lang/String;ZLkotlin/jvm/functions/Function1;Lorg/apache/kafka/common/serialization/Deserializer;Lorg/apache/kafka/common/serialization/Deserializer;JJLjava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic final fun getSink ()Lcom/trendyol/stove/kafka/intercepting/StoveMessageSink;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun messageStore ()Lcom/trendyol/stove/kafka/intercepting/MessageStore;\n\tpublic final fun pause ()Lcom/trendyol/stove/kafka/KafkaSystem;\n\tpublic final fun peekCommittedMessages-rnQQ1Ag (JLjava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun peekCommittedMessages-rnQQ1Ag$default (Lcom/trendyol/stove/kafka/KafkaSystem;JLjava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun peekConsumedMessages-rnQQ1Ag (JLjava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun peekConsumedMessages-rnQQ1Ag$default (Lcom/trendyol/stove/kafka/KafkaSystem;JLjava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun peekPublishedMessages-rnQQ1Ag (JLjava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun peekPublishedMessages-rnQQ1Ag$default (Lcom/trendyol/stove/kafka/KafkaSystem;JLjava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun publish (Ljava/lang/String;Ljava/lang/Object;Larrow/core/Option;Ljava/util/Map;ILarrow/core/Option;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun publish$default (Lcom/trendyol/stove/kafka/KafkaSystem;Ljava/lang/String;Ljava/lang/Object;Larrow/core/Option;Ljava/util/Map;ILarrow/core/Option;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun setSink (Lcom/trendyol/stove/kafka/intercepting/StoveMessageSink;)V\n\tpublic final fun shouldBeConsumedInternal-dWUq8MI (Lkotlin/reflect/KClass;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldBeFailedInternal-dWUq8MI (Lkotlin/reflect/KClass;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldBePublishedInternal-dWUq8MI (Lkotlin/reflect/KClass;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldBeRetriedInternal-WPwdCS8 (Lkotlin/reflect/KClass;JILkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause ()Lcom/trendyol/stove/kafka/KafkaSystem;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaSystem$Companion {\n}\n\npublic final class com/trendyol/stove/kafka/KafkaSystemKt {\n\tpublic static final field STOVE_KAFKA_BRIDGE_PORT Ljava/lang/String;\n\tpublic static final fun getStoveKafkaBridgePortDefault ()Ljava/lang/String;\n\tpublic static final fun getStoveSerdeRef ()Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic static final fun setStoveKafkaBridgePortDefault (Ljava/lang/String;)V\n\tpublic static final fun setStoveSerdeRef (Lcom/trendyol/stove/serialization/StoveSerde;)V\n}\n\npublic class com/trendyol/stove/kafka/KafkaSystemOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/KafkaSystemOptions$Companion;\n\tpublic fun <init> (ZLcom/trendyol/stove/kafka/TopicSuffixes;ZILcom/trendyol/stove/serialization/StoveSerde;Lorg/apache/kafka/common/serialization/Serializer;Lcom/trendyol/stove/kafka/KafkaContainerOptions;Lkotlin/jvm/functions/Function2;Ljava/util/Map;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (ZLcom/trendyol/stove/kafka/TopicSuffixes;ZILcom/trendyol/stove/serialization/StoveSerde;Lorg/apache/kafka/common/serialization/Serializer;Lcom/trendyol/stove/kafka/KafkaContainerOptions;Lkotlin/jvm/functions/Function2;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getBridgeGrpcServerPort ()I\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainerOptions ()Lcom/trendyol/stove/kafka/KafkaContainerOptions;\n\tpublic fun getListenPublishedMessagesFromStove ()Z\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun getProperties ()Ljava/util/Map;\n\tpublic fun getSerde ()Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic fun getTopicSuffixes ()Lcom/trendyol/stove/kafka/TopicSuffixes;\n\tpublic fun getUseEmbeddedKafka ()Z\n\tpublic fun getValueSerializer ()Lorg/apache/kafka/common/serialization/Serializer;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaSystemOptions$Companion {\n\tpublic final fun provided (Ljava/lang/String;Lcom/trendyol/stove/kafka/TopicSuffixes;ZILcom/trendyol/stove/serialization/StoveSerde;Lorg/apache/kafka/common/serialization/Serializer;Ljava/util/Map;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/kafka/ProvidedKafkaSystemOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/kafka/KafkaSystemOptions$Companion;Ljava/lang/String;Lcom/trendyol/stove/kafka/TopicSuffixes;ZILcom/trendyol/stove/serialization/StoveSerde;Lorg/apache/kafka/common/serialization/Serializer;Ljava/util/Map;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/ProvidedKafkaSystemOptions;\n}\n\npublic final class com/trendyol/stove/kafka/ProvidedKafkaSystemOptions : com/trendyol/stove/kafka/KafkaSystemOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;Lcom/trendyol/stove/kafka/TopicSuffixes;ZILcom/trendyol/stove/serialization/StoveSerde;Lorg/apache/kafka/common/serialization/Serializer;Ljava/util/Map;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;Lcom/trendyol/stove/kafka/TopicSuffixes;ZILcom/trendyol/stove/serialization/StoveSerde;Lorg/apache/kafka/common/serialization/Serializer;Ljava/util/Map;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic final class com/trendyol/stove/kafka/PublishedMessage : com/squareup/wire/Message {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/PublishedMessage$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Lokio/ByteString;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lokio/ByteString;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Lokio/ByteString;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun copy (Ljava/lang/String;Lokio/ByteString;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lokio/ByteString;)Lcom/trendyol/stove/kafka/PublishedMessage;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/PublishedMessage;Ljava/lang/String;Lokio/ByteString;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lokio/ByteString;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/PublishedMessage;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getHeaders ()Ljava/util/Map;\n\tpublic final fun getId ()Ljava/lang/String;\n\tpublic final fun getKey ()Ljava/lang/String;\n\tpublic final fun getMessage ()Lokio/ByteString;\n\tpublic final fun getTopic ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic synthetic fun newBuilder ()Lcom/squareup/wire/Message$Builder;\n\tpublic synthetic fun newBuilder ()Ljava/lang/Void;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/PublishedMessage$Companion {\n}\n\npublic final class com/trendyol/stove/kafka/PublishedRecord {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;[BLjava/util/Map;)V\n\tpublic final fun getHeaders ()Ljava/util/Map;\n\tpublic final fun getKey ()Ljava/lang/String;\n\tpublic final fun getTopic ()Ljava/lang/String;\n\tpublic final fun getValue ()[B\n}\n\npublic final class com/trendyol/stove/kafka/Reply : com/squareup/wire/Message {\n\tpublic static final field ADAPTER Lcom/squareup/wire/ProtoAdapter;\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/Reply$Companion;\n\tpublic fun <init> ()V\n\tpublic fun <init> (ILokio/ByteString;)V\n\tpublic synthetic fun <init> (ILokio/ByteString;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun copy (ILokio/ByteString;)Lcom/trendyol/stove/kafka/Reply;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/Reply;ILokio/ByteString;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/Reply;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getStatus ()I\n\tpublic fun hashCode ()I\n\tpublic synthetic fun newBuilder ()Lcom/squareup/wire/Message$Builder;\n\tpublic synthetic fun newBuilder ()Ljava/lang/Void;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/Reply$Companion {\n}\n\npublic class com/trendyol/stove/kafka/StoveKafkaContainer : org/testcontainers/kafka/ConfluentKafkaContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\npublic abstract interface class com/trendyol/stove/kafka/StoveKafkaObserverServiceClient : com/squareup/wire/Service {\n\tpublic abstract fun healthCheck ()Lcom/squareup/wire/GrpcCall;\n\tpublic abstract fun onAcknowledgedMessage ()Lcom/squareup/wire/GrpcCall;\n\tpublic abstract fun onCommittedMessage ()Lcom/squareup/wire/GrpcCall;\n\tpublic abstract fun onConsumedMessage ()Lcom/squareup/wire/GrpcCall;\n\tpublic abstract fun onPublishedMessage ()Lcom/squareup/wire/GrpcCall;\n}\n\npublic abstract interface class com/trendyol/stove/kafka/StoveKafkaObserverServiceServer : com/squareup/wire/Service {\n\tpublic abstract fun healthCheck (Lcom/trendyol/stove/kafka/HealthCheckRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun onAcknowledgedMessage (Lcom/trendyol/stove/kafka/AcknowledgedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun onCommittedMessage (Lcom/trendyol/stove/kafka/CommittedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun onConsumedMessage (Lcom/trendyol/stove/kafka/ConsumedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic abstract fun onPublishedMessage (Lcom/trendyol/stove/kafka/PublishedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc;\n\tpublic static final field SERVICE_NAME Ljava/lang/String;\n\tpublic final fun getServiceDescriptor ()Lio/grpc/ServiceDescriptor;\n\tpublic final fun gethealthCheckMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun getonAcknowledgedMessageMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun getonCommittedMessageMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun getonConsumedMessageMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun getonPublishedMessageMethod ()Lio/grpc/MethodDescriptor;\n\tpublic final fun newStub (Lio/grpc/Channel;)Lcom/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceStub;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$BindableAdapter : com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase {\n\tpublic fun <init> (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;)V\n\tpublic synthetic fun <init> (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun <init> (Lkotlin/jvm/functions/Function0;)V\n\tpublic fun healthCheck (Lcom/trendyol/stove/kafka/HealthCheckRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onAcknowledgedMessage (Lcom/trendyol/stove/kafka/AcknowledgedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onCommittedMessage (Lcom/trendyol/stove/kafka/CommittedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onConsumedMessage (Lcom/trendyol/stove/kafka/ConsumedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onPublishedMessage (Lcom/trendyol/stove/kafka/PublishedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase : com/squareup/wire/kotlin/grpcserver/WireBindableService {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lkotlin/coroutines/CoroutineContext;)V\n\tpublic synthetic fun <init> (Lkotlin/coroutines/CoroutineContext;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun bindService ()Lio/grpc/ServerServiceDefinition;\n\tprotected final fun getContext ()Lkotlin/coroutines/CoroutineContext;\n\tpublic fun healthCheck (Lcom/trendyol/stove/kafka/HealthCheckRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onAcknowledgedMessage (Lcom/trendyol/stove/kafka/AcknowledgedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onCommittedMessage (Lcom/trendyol/stove/kafka/CommittedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onConsumedMessage (Lcom/trendyol/stove/kafka/ConsumedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onPublishedMessage (Lcom/trendyol/stove/kafka/PublishedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase$AcknowledgedMessageMarshaller : com/squareup/wire/kotlin/grpcserver/WireMethodMarshaller {\n\tpublic fun <init> ()V\n\tpublic fun marshalledClass ()Ljava/lang/Class;\n\tpublic fun parse (Ljava/io/InputStream;)Lcom/trendyol/stove/kafka/AcknowledgedMessage;\n\tpublic synthetic fun parse (Ljava/io/InputStream;)Ljava/lang/Object;\n\tpublic fun stream (Lcom/trendyol/stove/kafka/AcknowledgedMessage;)Ljava/io/InputStream;\n\tpublic synthetic fun stream (Ljava/lang/Object;)Ljava/io/InputStream;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase$CommittedMessageMarshaller : com/squareup/wire/kotlin/grpcserver/WireMethodMarshaller {\n\tpublic fun <init> ()V\n\tpublic fun marshalledClass ()Ljava/lang/Class;\n\tpublic fun parse (Ljava/io/InputStream;)Lcom/trendyol/stove/kafka/CommittedMessage;\n\tpublic synthetic fun parse (Ljava/io/InputStream;)Ljava/lang/Object;\n\tpublic fun stream (Lcom/trendyol/stove/kafka/CommittedMessage;)Ljava/io/InputStream;\n\tpublic synthetic fun stream (Ljava/lang/Object;)Ljava/io/InputStream;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase$ConsumedMessageMarshaller : com/squareup/wire/kotlin/grpcserver/WireMethodMarshaller {\n\tpublic fun <init> ()V\n\tpublic fun marshalledClass ()Ljava/lang/Class;\n\tpublic fun parse (Ljava/io/InputStream;)Lcom/trendyol/stove/kafka/ConsumedMessage;\n\tpublic synthetic fun parse (Ljava/io/InputStream;)Ljava/lang/Object;\n\tpublic fun stream (Lcom/trendyol/stove/kafka/ConsumedMessage;)Ljava/io/InputStream;\n\tpublic synthetic fun stream (Ljava/lang/Object;)Ljava/io/InputStream;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase$HealthCheckRequestMarshaller : com/squareup/wire/kotlin/grpcserver/WireMethodMarshaller {\n\tpublic fun <init> ()V\n\tpublic fun marshalledClass ()Ljava/lang/Class;\n\tpublic fun parse (Ljava/io/InputStream;)Lcom/trendyol/stove/kafka/HealthCheckRequest;\n\tpublic synthetic fun parse (Ljava/io/InputStream;)Ljava/lang/Object;\n\tpublic fun stream (Lcom/trendyol/stove/kafka/HealthCheckRequest;)Ljava/io/InputStream;\n\tpublic synthetic fun stream (Ljava/lang/Object;)Ljava/io/InputStream;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase$HealthCheckResponseMarshaller : com/squareup/wire/kotlin/grpcserver/WireMethodMarshaller {\n\tpublic fun <init> ()V\n\tpublic fun marshalledClass ()Ljava/lang/Class;\n\tpublic fun parse (Ljava/io/InputStream;)Lcom/trendyol/stove/kafka/HealthCheckResponse;\n\tpublic synthetic fun parse (Ljava/io/InputStream;)Ljava/lang/Object;\n\tpublic fun stream (Lcom/trendyol/stove/kafka/HealthCheckResponse;)Ljava/io/InputStream;\n\tpublic synthetic fun stream (Ljava/lang/Object;)Ljava/io/InputStream;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase$PublishedMessageMarshaller : com/squareup/wire/kotlin/grpcserver/WireMethodMarshaller {\n\tpublic fun <init> ()V\n\tpublic fun marshalledClass ()Ljava/lang/Class;\n\tpublic fun parse (Ljava/io/InputStream;)Lcom/trendyol/stove/kafka/PublishedMessage;\n\tpublic synthetic fun parse (Ljava/io/InputStream;)Ljava/lang/Object;\n\tpublic fun stream (Lcom/trendyol/stove/kafka/PublishedMessage;)Ljava/io/InputStream;\n\tpublic synthetic fun stream (Ljava/lang/Object;)Ljava/io/InputStream;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase$ReplyMarshaller : com/squareup/wire/kotlin/grpcserver/WireMethodMarshaller {\n\tpublic fun <init> ()V\n\tpublic fun marshalledClass ()Ljava/lang/Class;\n\tpublic fun parse (Ljava/io/InputStream;)Lcom/trendyol/stove/kafka/Reply;\n\tpublic synthetic fun parse (Ljava/io/InputStream;)Ljava/lang/Object;\n\tpublic fun stream (Lcom/trendyol/stove/kafka/Reply;)Ljava/io/InputStream;\n\tpublic synthetic fun stream (Ljava/lang/Object;)Ljava/io/InputStream;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceStub : io/grpc/kotlin/AbstractCoroutineStub {\n\tpublic synthetic fun build (Lio/grpc/Channel;Lio/grpc/CallOptions;)Lio/grpc/stub/AbstractStub;\n\tpublic final fun healthCheck (Lcom/trendyol/stove/kafka/HealthCheckRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun onAcknowledgedMessage (Lcom/trendyol/stove/kafka/AcknowledgedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun onCommittedMessage (Lcom/trendyol/stove/kafka/CommittedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun onConsumedMessage (Lcom/trendyol/stove/kafka/ConsumedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun onPublishedMessage (Lcom/trendyol/stove/kafka/PublishedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaValueDeserializer : org/apache/kafka/common/serialization/Deserializer {\n\tpublic fun <init> ()V\n\tpublic fun deserialize (Ljava/lang/String;[B)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/kafka/StoveKafkaValueSerializer : org/apache/kafka/common/serialization/Serializer {\n\tpublic fun <init> ()V\n\tpublic fun serialize (Ljava/lang/String;Ljava/lang/Object;)[B\n}\n\npublic final class com/trendyol/stove/kafka/TopicSuffixes {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/util/List;Ljava/util/List;)V\n\tpublic synthetic fun <init> (Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/util/List;\n\tpublic final fun component2 ()Ljava/util/List;\n\tpublic final fun copy (Ljava/util/List;Ljava/util/List;)Lcom/trendyol/stove/kafka/TopicSuffixes;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/TopicSuffixes;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/TopicSuffixes;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getError ()Ljava/util/List;\n\tpublic final fun getRetry ()Ljava/util/List;\n\tpublic fun hashCode ()I\n\tpublic final fun isErrorTopic (Ljava/lang/String;)Z\n\tpublic final fun isRetryTopic (Ljava/lang/String;)Z\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/intercepting/GrpcUtils {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/kafka/intercepting/GrpcUtils;\n\tpublic final fun createClient (Ljava/lang/String;Lkotlinx/coroutines/CoroutineScope;)Lcom/trendyol/stove/kafka/StoveKafkaObserverServiceClient;\n}\n\npublic final class com/trendyol/stove/kafka/intercepting/MessageStore {\n\tpublic fun <init> ()V\n\tpublic final fun committedMessages ()Ljava/util/Collection;\n\tpublic final fun consumedMessages ()Ljava/util/Collection;\n\tpublic final fun failedMessages ()Ljava/util/Collection;\n\tpublic final fun publishedMessages ()Ljava/util/Collection;\n\tpublic final fun retriedMessages ()Ljava/util/Collection;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/intercepting/StoveKafkaBridge : org/apache/kafka/clients/consumer/ConsumerInterceptor, org/apache/kafka/clients/producer/ProducerInterceptor {\n\tpublic fun <init> ()V\n\tpublic fun close ()V\n\tpublic fun configure (Ljava/util/Map;)V\n\tpublic fun onAcknowledgement (Lorg/apache/kafka/clients/producer/RecordMetadata;Ljava/lang/Exception;)V\n\tpublic fun onCommit (Ljava/util/Map;)V\n\tpublic fun onConsume (Lorg/apache/kafka/clients/consumer/ConsumerRecords;)Lorg/apache/kafka/clients/consumer/ConsumerRecords;\n\tpublic fun onSend (Lorg/apache/kafka/clients/producer/ProducerRecord;)Lorg/apache/kafka/clients/producer/ProducerRecord;\n}\n\npublic final class com/trendyol/stove/kafka/intercepting/StoveKafkaObserverGrpcServer : com/trendyol/stove/kafka/StoveKafkaObserverServiceWireGrpc$StoveKafkaObserverServiceImplBase {\n\tpublic fun <init> (Lcom/trendyol/stove/kafka/intercepting/StoveMessageSink;)V\n\tpublic fun healthCheck (Lcom/trendyol/stove/kafka/HealthCheckRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onAcknowledgedMessage (Lcom/trendyol/stove/kafka/AcknowledgedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onCommittedMessage (Lcom/trendyol/stove/kafka/CommittedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onConsumedMessage (Lcom/trendyol/stove/kafka/ConsumedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun onPublishedMessage (Lcom/trendyol/stove/kafka/PublishedMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/kafka/intercepting/StoveMessageSink : com/trendyol/stove/kafka/intercepting/CommonOps, com/trendyol/stove/kafka/intercepting/MessageSinkOps {\n\tpublic fun <init> (Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/serialization/StoveSerde;Lcom/trendyol/stove/kafka/TopicSuffixes;)V\n\tpublic fun deserializeCatching-gIAlu-s ([BLkotlin/reflect/KClass;)Ljava/lang/Object;\n\tpublic fun dumpMessages ()Ljava/lang/String;\n\tpublic fun getAdminClient ()Lorg/apache/kafka/clients/admin/Admin;\n\tpublic fun getLogger ()Lorg/slf4j/Logger;\n\tpublic fun getSerde ()Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic fun getStore ()Lcom/trendyol/stove/kafka/intercepting/MessageStore;\n\tpublic fun getTopicSuffixes ()Lcom/trendyol/stove/kafka/TopicSuffixes;\n\tpublic final fun onMessageAcknowledged (Lcom/trendyol/stove/kafka/AcknowledgedMessage;)V\n\tpublic final fun onMessageCommitted (Lcom/trendyol/stove/kafka/CommittedMessage;)V\n\tpublic final fun onMessageConsumed (Lcom/trendyol/stove/kafka/ConsumedMessage;)V\n\tpublic final fun onMessagePublished (Lcom/trendyol/stove/kafka/PublishedMessage;)V\n\tpublic fun recordAcknowledgedMessage (Lcom/trendyol/stove/kafka/AcknowledgedMessage;)V\n\tpublic fun recordCommittedMessage (Lcom/trendyol/stove/kafka/CommittedMessage;)V\n\tpublic fun recordConsumed (Lcom/trendyol/stove/kafka/ConsumedMessage;)V\n\tpublic fun recordError (Lcom/trendyol/stove/kafka/ConsumedMessage;)V\n\tpublic fun recordPublishedMessage (Lcom/trendyol/stove/kafka/PublishedMessage;)V\n\tpublic fun recordRetry (Lcom/trendyol/stove/kafka/ConsumedMessage;)V\n\tpublic fun throwIfFailed (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V\n\tpublic fun throwIfRetried (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V\n\tpublic fun waitUntilConditionMet-WPwdCS8 (Lkotlin/jvm/functions/Function0;JLjava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun waitUntilConsumed-rnQQ1Ag (JLkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun waitUntilCount-dWUq8MI (Lkotlin/jvm/functions/Function1;JILkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun waitUntilFailed-rnQQ1Ag (JLkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun waitUntilPublished-rnQQ1Ag (JLkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun waitUntilRetried-gRj5Bb8 (JILkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\n"
  },
  {
    "path": "lib/stove-kafka/build.gradle.kts",
    "content": "plugins {\n  alias(libs.plugins.wire)\n}\n\ndependencies {\n  api(projects.lib.stove)\n  api(libs.testcontainers.kafka)\n  api(libs.kafka)\n  api(libs.kafka.embedded)\n  implementation(libs.kotlinx.io.reactor.extensions)\n  implementation(libs.kotlinx.jdk8)\n  implementation(libs.kotlinx.core)\n  implementation(libs.wire.grpc.server)\n  implementation(libs.wire.grpc.client)\n  implementation(libs.wire.grpc.runtime)\n  implementation(libs.io.grpc)\n  implementation(libs.io.grpc.protobuf)\n  implementation(libs.io.grpc.stub)\n  implementation(libs.io.grpc.kotlin)\n  implementation(libs.io.grpc.netty)\n  implementation(libs.google.protobuf.kotlin)\n  implementation(libs.caffeine)\n  implementation(libs.pprint)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.logback.classic)\n  testImplementation(libs.kafkaKotlin)\n}\n\nbuildscript {\n  dependencies {\n    classpath(libs.wire.grpc.server.generator)\n  }\n}\n\nwire {\n  sourcePath(\"src/main/proto\")\n  kotlin {\n    rpcRole = \"client\"\n    rpcCallStyle = \"suspending\"\n    exclusive = false\n    javaInterop = false\n  }\n  kotlin {\n    custom {\n      schemaHandlerFactory = com.squareup.wire.kotlin.grpcserver.GrpcServerSchemaHandler.Factory()\n      options = mapOf(\n        \"singleMethodServices\" to \"false\",\n        \"rpcCallStyle\" to \"suspending\"\n      )\n    }\n    rpcRole = \"server\"\n    rpcCallStyle = \"suspending\"\n    exclusive = false\n    singleMethodServices = false\n    javaInterop = true\n    includes = listOf(\"com.trendyol.stove.kafka.StoveKafkaObserverService\")\n  }\n}\n\nval testWithEmbedded = tasks.register<Test>(\"testWithEmbedded\") {\n  group = \"verification\"\n  description = \"Runs tests with embedded Kafka\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useEmbeddedKafka\", \"true\")\n  doFirst {\n    println(\"Starting embedded Kafka tests...\")\n  }\n}\n\nval testWithProvided = tasks.register<Test>(\"testWithProvided\") {\n  group = \"verification\"\n  description = \"Runs tests with an externally provided Kafka instance\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useProvided\", \"true\")\n  doFirst {\n    println(\"Starting Kafka tests with provided instance...\")\n  }\n}\n\ntasks.test.configure {\n  dependsOn(testWithEmbedded, testWithProvided)\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/Caching.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.github.benmanes.caffeine.cache.*\n\nobject Caching {\n  fun <K : Any, V : Any> of(): Cache<K, V> = Caffeine.newBuilder().build()\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/Extensions.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.trendyol.stove.messaging.MessageMetadata\nimport org.apache.kafka.clients.producer.*\nimport java.util.*\nimport kotlin.coroutines.*\n\nfun <K, V> Map<K, V>.toProperties(): Properties = Properties().apply {\n  this@toProperties.forEach { (k, v) -> this[k] = v }\n}\n\nfun ConsumedMessage.metadata(): MessageMetadata = MessageMetadata(\n  topic = topic,\n  key = key,\n  headers = headers\n)\n\nfun PublishedMessage.metadata(): MessageMetadata = MessageMetadata(\n  topic = topic,\n  key = key,\n  headers = headers\n)\n\nsuspend inline fun <reified K : Any, reified V : Any> KafkaProducer<K, V>.dispatch(\n  record: ProducerRecord<K, V>\n): RecordMetadata = suspendCoroutine { continuation ->\n  val callback = Callback { metadata, exception ->\n    if (exception != null) {\n      continuation.resumeWithException(exception)\n    } else {\n      continuation.resume(metadata)\n    }\n  }\n  send(record, callback)\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/KafkaContainerOptions.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.trendyol.stove.containers.*\nimport org.testcontainers.kafka.ConfluentKafkaContainer\nimport org.testcontainers.utility.DockerImageName\n\nopen class StoveKafkaContainer(\n  override val imageNameAccess: DockerImageName\n) : ConfluentKafkaContainer(imageNameAccess),\n  StoveContainer\n\ndata class KafkaContainerOptions(\n  override val registry: String = DEFAULT_REGISTRY,\n  override val image: String = \"confluentinc/cp-kafka\",\n  override val tag: String = \"latest\",\n  val ports: List<Int> = DEFAULT_KAFKA_PORTS,\n  override val compatibleSubstitute: String? = null,\n  override val useContainerFn: UseContainerFn<StoveKafkaContainer> = { StoveKafkaContainer(it) },\n  override val containerFn: ContainerFn<StoveKafkaContainer> = { }\n) : ContainerOptions<StoveKafkaContainer> {\n  companion object {\n    val DEFAULT_KAFKA_PORTS = listOf(9092, 9093)\n  }\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/KafkaContext.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.containers.withProvidedRegistry\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\n\ndata class KafkaContext(\n  val runtime: SystemRuntime,\n  val options: KafkaSystemOptions,\n  val keyName: String? = null\n)\n\ninternal fun Stove.kafka(): KafkaSystem = getOrNone<KafkaSystem>().getOrElse {\n  throw SystemNotRegisteredException(KafkaSystem::class)\n}\n\ninternal fun Stove.kafka(key: SystemKey): KafkaSystem = getOrNone<KafkaSystem>(key).getOrElse {\n  throw SystemNotRegisteredException(KafkaSystem::class, \"No KafkaSystem registered with key '${keyDisplayName(key)}'\")\n}\n\ninternal fun Stove.withKafka(\n  options: KafkaSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(KafkaSystem(this, KafkaContext(runtime, options)))\n  return this\n}\n\ninternal fun Stove.withKafka(\n  key: SystemKey,\n  options: KafkaSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(key, KafkaSystem(this, KafkaContext(runtime, options, keyName = keyDisplayName(key))))\n  return this\n}\n\nsuspend fun ValidationDsl.kafka(\n  validation: @StoveDsl suspend KafkaSystem.() -> Unit\n): Unit = validation(this.stove.kafka())\n\nsuspend fun ValidationDsl.kafka(\n  key: SystemKey,\n  validation: @StoveDsl suspend KafkaSystem.() -> Unit\n): Unit = validation(this.stove.kafka(key))\n\n/**\n * Configures Kafka system.\n *\n * For container-based setup:\n * ```kotlin\n * kafka {\n *   KafkaSystemOptions(\n *     cleanup = { admin -> admin.deleteTopics(...) },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For embedded Kafka:\n * ```kotlin\n * kafka {\n *   KafkaSystemOptions(\n *     useEmbeddedKafka = true,\n *     cleanup = { admin -> admin.deleteTopics(...) },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * kafka {\n *   KafkaSystemOptions.provided(\n *     bootstrapServers = \"localhost:9092\",\n *     cleanup = { admin -> admin.deleteTopics(...) },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n */\nfun WithDsl.kafka(\n  configure: () -> KafkaSystemOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = when {\n    options is ProvidedKafkaSystemOptions -> ProvidedRuntime\n\n    options.useEmbeddedKafka -> EmbeddedKafkaRuntime\n\n    else -> withProvidedRegistry(\n      options.containerOptions.imageWithTag,\n      options.containerOptions.registry,\n      options.containerOptions.compatibleSubstitute\n    ) { dockerImageName ->\n      options.containerOptions\n        .useContainerFn(dockerImageName)\n        .withExposedPorts(*options.containerOptions.ports.toTypedArray())\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveKafkaContainer }\n        .apply(options.containerOptions.containerFn)\n    }\n  }\n  return stove.withKafka(options, runtime)\n}\n\n/**\n * Configures a keyed Kafka system for testing multiple Kafka instances.\n *\n * ```kotlin\n * Stove().with {\n *     kafka(PaymentKafka) {\n *         KafkaSystemOptions(\n *             configureExposedConfiguration = { cfg -> listOf(...) }\n *         )\n *     }\n * }\n * ```\n *\n * @param key The [SystemKey] identifying this Kafka instance.\n * @param configure Configuration block returning [KafkaSystemOptions].\n * @return The test system for fluent chaining.\n */\nfun WithDsl.kafka(\n  key: SystemKey,\n  configure: () -> KafkaSystemOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = when {\n    options is ProvidedKafkaSystemOptions -> ProvidedRuntime\n\n    options.useEmbeddedKafka -> EmbeddedKafkaRuntime\n\n    else -> withProvidedRegistry(\n      options.containerOptions.imageWithTag,\n      options.containerOptions.registry,\n      options.containerOptions.compatibleSubstitute\n    ) { dockerImageName ->\n      options.containerOptions\n        .useContainerFn(dockerImageName)\n        .withExposedPorts(*options.containerOptions.ports.toTypedArray())\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveKafkaContainer }\n        .apply(options.containerOptions.containerFn)\n    }\n  }\n  return stove.withKafka(key, options, runtime)\n}\n\n/**\n * Special runtime for embedded Kafka that doesn't use a container.\n */\ndata object EmbeddedKafkaRuntime : SystemRuntime\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/KafkaExposedConfiguration.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.trendyol.stove.system.abstractions.ExposedConfiguration\n\ndata class KafkaExposedConfiguration(\n  val bootstrapServers: String,\n  val interceptorClass: String\n) : ExposedConfiguration\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/KafkaSystem.kt",
    "content": "@file:Suppress(\"TooGenericExceptionCaught\")\n\npackage com.trendyol.stove.kafka\n\nimport arrow.core.*\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.kafka.intercepting.*\nimport com.trendyol.stove.messaging.*\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport com.trendyol.stove.tracing.TraceContext\nimport io.github.embeddedkafka.*\nimport io.grpc.Server\nimport io.grpc.netty.NettyServerBuilder\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.selects.*\nimport org.apache.kafka.clients.admin.*\nimport org.apache.kafka.clients.consumer.*\nimport org.apache.kafka.clients.producer.*\nimport org.apache.kafka.common.serialization.*\nimport org.slf4j.*\nimport scala.collection.immutable.`Map$`\nimport java.net.*\nimport java.util.*\nimport kotlin.reflect.KClass\nimport kotlin.time.*\nimport kotlin.time.Duration.Companion.milliseconds\nimport kotlin.time.Duration.Companion.seconds\n\nvar stoveSerdeRef: StoveSerde<Any, ByteArray> = StoveSerde.jackson.anyByteArraySerde()\n\n/**\n * Default port for the Stove Kafka Bridge gRPC server.\n * This can be overridden by setting the [STOVE_KAFKA_BRIDGE_PORT] environment variable\n * or by using [PortFinder.findAvailablePortAsString] to get a dynamically available port.\n */\nvar stoveKafkaBridgePortDefault: String = PortFinder.findAvailablePortAsString()\nconst val STOVE_KAFKA_BRIDGE_PORT = \"STOVE_KAFKA_BRIDGE_PORT\"\ninternal val StoveKafkaCoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n\n/**\n * Kafka messaging system for testing message publishing and consumption.\n *\n * Provides a comprehensive DSL for testing Kafka-based messaging patterns:\n * - Publishing messages to topics\n * - Asserting messages are consumed by the application\n * - Asserting messages are published by the application\n * - Asserting message processing failures\n *\n * ## Publishing Messages\n *\n * ```kotlin\n * kafka {\n *     // Publish a message to a topic\n *     publish(\"orders.created\", OrderCreatedEvent(orderId = \"123\", amount = 99.99))\n *\n *     // Publish with custom headers\n *     publish(\n *         topic = \"orders.created\",\n *         message = event,\n *         headers = mapOf(\"correlationId\" to \"abc-123\")\n *     )\n *\n *     // Publish with specific key\n *     publish(\n *         topic = \"orders.created\",\n *         key = \"customer-456\",\n *         message = event\n *     )\n * }\n * ```\n *\n * ## Asserting Consumed Messages\n *\n * Verify your application consumed messages correctly:\n *\n * ```kotlin\n * kafka {\n *     publish(\"orders.created\", OrderCreatedEvent(orderId = \"123\"))\n *\n *     // Assert the message was consumed\n *     shouldBeConsumed<OrderCreatedEvent> {\n *         actual.orderId == \"123\"\n *     }\n *\n *     // With custom timeout\n *     shouldBeConsumed<OrderCreatedEvent>(atLeastIn = 30.seconds) {\n *         actual.orderId == \"123\"\n *     }\n * }\n * ```\n *\n * ## Asserting Published Messages\n *\n * Verify your application published messages:\n *\n * ```kotlin\n * // Trigger action that publishes a message\n * http {\n *     postAndExpectBodilessResponse(\"/orders\", body = request.some()) {\n *         it.status shouldBe 201\n *     }\n * }\n *\n * kafka {\n *     // Assert message was published\n *     shouldBePublished<OrderConfirmedEvent> {\n *         actual.orderId == request.id\n *     }\n *\n *     // Assert with header validation\n *     shouldBePublished<OrderConfirmedEvent> {\n *         actual.orderId == request.id &&\n *         metadata.headers[\"X-Correlation-Id\"] == correlationId\n *     }\n * }\n * ```\n *\n * ## Asserting Failed Messages\n *\n * Verify messages failed processing (for error handling tests):\n *\n * ```kotlin\n * kafka {\n *     publish(\"orders.created\", InvalidOrderEvent(orderId = \"invalid\"))\n *\n *     shouldBeFailed<InvalidOrderEvent> {\n *         actual.orderId == \"invalid\"\n *     }\n * }\n * ```\n *\n * ## Topic Management\n *\n * ```kotlin\n * kafka {\n *     // Create topics\n *     createTopics(\"orders.created\", \"orders.confirmed\")\n *\n *     // Delete topics\n *     deleteTopics(\"orders.created\")\n * }\n * ```\n *\n * ## Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         kafka {\n *             stoveKafkaObjectMapperRef = myObjectMapper\n *             KafkaSystemOptions {\n *                 listOf(\n *                     \"spring.kafka.bootstrap-servers=${it.bootstrapServers}\",\n *                     \"spring.kafka.consumer.group-id=test-group\"\n *                 )\n *             }\n *         }\n *     }\n * ```\n *\n * @property stove The parent test system.\n * @see KafkaSystemOptions\n * @see KafkaExposedConfiguration\n */\n@Suppress(\"TooManyFunctions\", \"unused\", \"MagicNumber\")\n@StoveDsl\nclass KafkaSystem(\n  override val stove: Stove,\n  private val context: KafkaContext\n) : PluggedSystem,\n  ExposesConfiguration,\n  RunAware,\n  AfterRunAware,\n  BeforeRunAware,\n  Reports {\n  override val reportSystemName: String = \"Kafka\" + (context.keyName?.let { \" [$it]\" } ?: \"\")\n  override fun snapshot(): SystemSnapshot {\n    val currentTestId = reporter.currentTestId()\n    val store = sink.store\n    val belongsToTest: (Map<String, String>) -> Boolean = { headers ->\n      val testId = headers[TraceContext.STOVE_TEST_ID_HEADER].toOption()\n      testId.isNone() || testId.isSome { it == currentTestId }\n    }\n\n    val consumed = store.consumedMessages().filter { belongsToTest(it.headers) }\n    val published = store.publishedMessages().filter { belongsToTest(it.headers) }\n    val failed = store.failedMessages().filter { belongsToTest(it.headers) }\n    val retried = store.retriedMessages().filter { belongsToTest(it.headers) }\n    val topicPartitions = consumed.map { it.topic to it.partition }.toSet()\n    val committed = store.committedMessages().filter { (it.topic to it.partition) in topicPartitions }\n\n    return SystemSnapshot(\n      system = reportSystemName,\n      state = mapOf<String, Any>(\n        \"consumed\" to consumed.map { it.toReportMap() },\n        \"published\" to published.map { it.toReportMap() },\n        \"committed\" to committed.map { it.toReportMap() },\n        \"failed\" to failed.map { it.toReportMap() },\n        \"retried\" to retried.map { it.toReportMap() }\n      ),\n      summary = listOf(\n        \"Consumed (this test)\" to consumed.size,\n        \"Published (this test)\" to published.size,\n        \"Committed (this test)\" to committed.size,\n        \"Failed (this test)\" to failed.size,\n        \"Retried (this test)\" to retried.size\n      ).joinToString(\"\\n\") { (label, count) -> \"$label: $count\" }\n    )\n  }\n\n  private fun ConsumedMessage.toReportMap(): Map<String, Any> = mapOf(\n    \"id\" to id,\n    \"topic\" to topic,\n    \"key\" to key,\n    \"partition\" to partition,\n    \"offset\" to offset,\n    \"headers\" to headers,\n    \"message\" to String(message.toByteArray())\n  )\n\n  private fun PublishedMessage.toReportMap(): Map<String, Any> = mapOf(\n    \"id\" to id,\n    \"topic\" to topic,\n    \"key\" to key,\n    \"headers\" to headers,\n    \"message\" to String(message.toByteArray())\n  )\n\n  private fun CommittedMessage.toReportMap(): Map<String, Any> = mapOf<String, Any>(\n    \"topic\" to topic,\n    \"partition\" to partition,\n    \"offset\" to offset\n  )\n\n  private lateinit var exposedConfiguration: KafkaExposedConfiguration\n  private lateinit var adminClient: Admin\n  private lateinit var kafkaPublisher: KafkaProducer<String, Any>\n  private lateinit var grpcServer: Server\n\n  @PublishedApi\n  internal lateinit var sink: StoveMessageSink\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val state: StateStorage<KafkaExposedConfiguration> =\n    stove.createStateStorage<KafkaExposedConfiguration, KafkaSystem>(context.keyName)\n\n  override suspend fun beforeRun() {\n    stoveSerdeRef = context.options.serde\n  }\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n    adminClient = createAdminClient(exposedConfiguration)\n    kafkaPublisher = createPublisher(exposedConfiguration)\n    sink = StoveMessageSink(adminClient, context.options.serde, context.options.topicSuffixes)\n    grpcServer = startGrpcServer()\n    runMigrationsIfNeeded()\n  }\n\n  override suspend fun afterRun() = Unit\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (shouldRunMigrations()) {\n      context.options.migrationCollection.run(KafkaMigrationContext(adminClient, context.options))\n    }\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    context.options is ProvidedKafkaSystemOptions -> context.options.runMigrations\n    context.runtime is StoveKafkaContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    context.runtime is EmbeddedKafkaRuntime -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n  }\n\n  override suspend fun stop() {\n    when (val runtime = context.runtime) {\n      is ProvidedRuntime -> Unit\n      is EmbeddedKafkaRuntime -> stopEmbeddedKafka()\n      is StoveKafkaContainer -> runtime.stop()\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      context.options.cleanup(adminClient)\n      grpcServer.shutdownNow()\n      StoveKafkaCoroutineScope.cancel()\n      kafkaPublisher.close()\n      executeWithReuseCheck { stop() }\n    }\n  }.recover { logger.warn(\"got an error while stopping: ${it.message}\") }.let { }\n\n  override fun configuration(): List<String> = context.options.configureExposedConfiguration(exposedConfiguration)\n\n  suspend fun publish(\n    topic: String,\n    message: Any,\n    key: Option<String> = None,\n    headers: Map<String, String> = mapOf(),\n    partition: Int = 0,\n    testCase: Option<String> = None\n  ): KafkaSystem {\n    report(\n      action = \"Publish to '$topic'\",\n      input = arrow.core.Some(message),\n      metadata = buildMap {\n        key.onSome { put(\"key\", it) }\n        put(\"headers\", headers)\n        put(\"partition\", partition)\n      }\n    ) {\n      val record = ProducerRecord<String, Any>(topic, partition, key.getOrNull(), message)\n      headers.forEach { (k, v) -> record.headers().add(k, v.toByteArray()) }\n      testCase.map { record.headers().add(\"testCase\", it.toByteArray()) }\n      injectTraceHeaders(record)\n      kafkaPublisher.dispatch(record)\n    }\n    return this\n  }\n\n  private fun injectTraceHeaders(record: ProducerRecord<String, Any>) {\n    TraceContext.current()?.let { ctx ->\n      record.headers().add(TraceContext.TRACEPARENT_HEADER, ctx.toTraceparent().toByteArray())\n      record.headers().add(TraceContext.STOVE_TEST_ID_HEADER, ctx.testId.toByteArray())\n    }\n  }\n\n  suspend inline fun <reified T : Any> shouldBeConsumed(\n    atLeastIn: Duration = 5.seconds,\n    crossinline condition: ObservedMessage<T>.() -> Boolean\n  ): KafkaSystem = assertKafkaMessage(\n    assertionName = \"shouldBeConsumed\",\n    typeName = T::class.simpleName ?: \"Unknown\",\n    timeout = atLeastIn,\n    expected = \"Message matching condition within $atLeastIn\"\n  ) { onMatch ->\n    shouldBeConsumedInternal(T::class, atLeastIn) { parsed ->\n      parsed.message.isSome { o ->\n        val result = condition(ObservedMessage(o, parsed.metadata))\n        if (result) onMatch(o)\n        result\n      }\n    }\n  }\n\n  suspend inline fun <reified T : Any> shouldBePublished(\n    atLeastIn: Duration = 5.seconds,\n    crossinline condition: ObservedMessage<T>.() -> Boolean\n  ): KafkaSystem = assertKafkaMessage(\n    assertionName = \"shouldBePublished\",\n    typeName = T::class.simpleName ?: \"Unknown\",\n    timeout = atLeastIn,\n    expected = \"Message matching condition within $atLeastIn\"\n  ) { onMatch ->\n    shouldBePublishedInternal(T::class, atLeastIn) { parsed ->\n      parsed.message.isSome { o ->\n        val result = condition(ObservedMessage(o, parsed.metadata))\n        if (result) onMatch(o)\n        result\n      }\n    }\n  }\n\n  suspend inline fun <reified T : Any> shouldBeFailed(\n    atLeastIn: Duration = 5.seconds,\n    crossinline condition: ObservedMessage<T>.() -> Boolean\n  ): KafkaSystem = assertKafkaMessage(\n    assertionName = \"shouldBeFailed\",\n    typeName = T::class.simpleName ?: \"Unknown\",\n    timeout = atLeastIn,\n    expected = \"Failed message within $atLeastIn\"\n  ) { onMatch ->\n    shouldBeFailedInternal(T::class, atLeastIn) { parsed ->\n      parsed.message.isSome { o ->\n        val result = condition(ObservedMessage(o, parsed.metadata))\n        if (result) onMatch(o)\n        result\n      }\n    }\n  }\n\n  /**\n   * Helper to reduce boilerplate in Kafka assertion methods.\n   * Handles try-catch, recording, and re-throwing.\n   */\n  @PublishedApi\n  internal suspend inline fun <T : Any> assertKafkaMessage(\n    assertionName: String,\n    typeName: String,\n    timeout: Duration,\n    expected: String,\n    crossinline block: suspend ((T) -> Unit) -> Unit\n  ): KafkaSystem {\n    var matchedMessage: T? = null\n\n    val result = runCatching {\n      coroutineScope {\n        block { matchedMessage = it }\n      }\n    }\n\n    val failure = result.exceptionOrNull()?.let { e ->\n      e as? AssertionError ?: AssertionError(\n        \"Expected $assertionName<$typeName> matching condition within $timeout, but none was found\",\n        e\n      )\n    }\n\n    if (result.isSuccess) {\n      reporter.record(\n        ReportEntry.success(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = \"$assertionName<$typeName>\",\n          output = matchedMessage.toOption(),\n          metadata = mapOf(\"timeout\" to timeout.toString())\n        )\n      )\n    } else {\n      reporter.record(\n        ReportEntry.failure(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = \"$assertionName<$typeName>\",\n          error = failure?.message ?: \"No matching message found\",\n          expected = expected.some(),\n          actual = (matchedMessage ?: \"No matching message found\").some()\n        )\n      )\n    }\n\n    failure?.let { throw it }\n    return this\n  }\n\n  suspend inline fun <reified T : Any> shouldBeRetried(\n    atLeastIn: Duration = 5.seconds,\n    times: Int = 1,\n    crossinline condition: ObservedMessage<T>.() -> Boolean\n  ): KafkaSystem = coroutineScope {\n    shouldBeRetriedInternal(T::class, atLeastIn, times) { parsed ->\n      parsed.message.isSome { o -> condition(ObservedMessage(o, parsed.metadata)) }\n    }\n  }.let { this }\n\n  /**\n   * Waits until the consumed message is seen. This does not mean committed.\n   */\n  @Suppress(\"MagicNumber\")\n  suspend inline fun peekConsumedMessages(\n    atLeastIn: Duration = 5.seconds,\n    topic: String,\n    crossinline condition: (ConsumedRecord) -> Boolean\n  ) = withTimeout(atLeastIn) {\n    var offset = -1L\n    var loop = true\n    while (loop) {\n      sink.store\n        .consumedMessages()\n        .filter { it.topic == topic && it.offset > offset }\n        .onEach { offset = it.offset }\n        .map { ConsumedRecord(it.topic, it.key, it.message.toByteArray(), it.headers, it.offset, it.partition) }\n        .forEach { loop = !condition(it) }\n      delay(100)\n    }\n  }\n\n  /**\n   * Waits until the committed message is seen with the given condition.\n   */\n  @Suppress(\"MagicNumber\")\n  suspend inline fun peekCommittedMessages(\n    atLeastIn: Duration = 5.seconds,\n    topic: String,\n    crossinline condition: (CommittedRecord) -> Boolean\n  ) = withTimeout(atLeastIn) {\n    var offset = -1L\n    var loop = true\n    while (loop) {\n      sink.store\n        .committedMessages()\n        .filter { it.topic == topic && it.offset > offset }\n        .onEach { offset = it.offset }\n        .map { CommittedRecord(it.topic, it.metadata, it.offset, it.partition) }\n        .forEach { loop = !condition(it) }\n      delay(100)\n    }\n  }\n\n  /**\n   * Waits until the published message is seen with the given condition.\n   */\n  @Suppress(\"MagicNumber\")\n  suspend inline fun peekPublishedMessages(\n    atLeastIn: Duration = 5.seconds,\n    topic: String,\n    crossinline condition: (PublishedRecord) -> Boolean\n  ) = withTimeout(atLeastIn) {\n    val seenIds = mutableMapOf<String, PublishedMessage>()\n    var loop = true\n    while (loop) {\n      sink.store\n        .publishedMessages()\n        .filter { it.topic == topic && !seenIds.containsKey(it.id) }\n        .onEach { seenIds[it.id] = it }\n        .map { PublishedRecord(it.topic, it.key, it.message.toByteArray(), it.headers) }\n        .forEach { loop = !condition(it) }\n      delay(100)\n    }\n  }\n\n  /**\n   * Creates an inflight consumer that consumes messages from the given topic.\n   */\n  suspend fun <K : Any, V : Any> consumer(\n    topic: String,\n    readOnly: Boolean = true,\n    autoOffsetReset: String = \"earliest\",\n    autoCreateTopics: Boolean = false,\n    config: (Properties) -> Unit = {},\n    keyDeserializer: Deserializer<K> = StoveKafkaValueDeserializer(),\n    valueDeserializer: Deserializer<V> = StoveKafkaValueDeserializer(),\n    keepConsumingAtLeastFor: Duration = 5.seconds,\n    pollTimeout: Duration = (keepConsumingAtLeastFor.inWholeMilliseconds / 2).milliseconds,\n    groupId: String = UUID.randomUUID().toString(),\n    onConsume: suspend (ConsumerRecord<K, V>) -> Unit\n  ) = consume(\n    autoOffsetReset,\n    readOnly,\n    autoCreateTopics,\n    config,\n    keyDeserializer,\n    valueDeserializer,\n    topic,\n    pollTimeout,\n    keepConsumingAtLeastFor,\n    groupId,\n    onConsume\n  )\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance or embedded Kafka.\n   */\n  fun pause(): KafkaSystem = withContainerOrWarn(\"pause\") { it.pause() }\n\n  /**\n   * Unpauses the container. Use with care, as it will unpause the container which might affect other tests.\n   * This operation is not supported when using a provided instance or embedded Kafka.\n   */\n  fun unpause(): KafkaSystem = withContainerOrWarn(\"unpause\") { it.unpause() }\n\n  /**\n   * Provides access to the message store of the KafkaSystem.\n   */\n  fun messageStore(): MessageStore = this.sink.store\n\n  suspend fun adminOperations(block: suspend Admin.() -> Unit) = block(adminClient)\n\n  @PublishedApi\n  internal suspend fun <T : Any> shouldBeConsumedInternal(\n    clazz: KClass<T>,\n    atLeastIn: Duration,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ): Unit = coroutineScope { sink.waitUntilConsumed(atLeastIn, clazz, condition) }\n\n  @PublishedApi\n  internal suspend fun <T : Any> shouldBeFailedInternal(\n    clazz: KClass<T>,\n    atLeastIn: Duration,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ): Unit = coroutineScope { sink.waitUntilFailed(atLeastIn, clazz, condition) }\n\n  @PublishedApi\n  internal suspend fun <T : Any> shouldBePublishedInternal(\n    clazz: KClass<T>,\n    atLeastIn: Duration,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ): Unit = coroutineScope { sink.waitUntilPublished(atLeastIn, clazz, condition) }\n\n  @PublishedApi\n  internal suspend fun <T : Any> shouldBeRetriedInternal(\n    clazz: KClass<T>,\n    atLeastIn: Duration,\n    times: Int,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ): Unit = coroutineScope { sink.waitUntilRetried(atLeastIn, times, clazz, condition) }\n\n  private suspend fun obtainExposedConfiguration(): KafkaExposedConfiguration =\n    when {\n      context.options is ProvidedKafkaSystemOptions -> context.options.config\n      context.runtime is EmbeddedKafkaRuntime -> startEmbeddedKafka()\n      context.runtime is StoveKafkaContainer -> startKafkaContainer(context.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n    }\n\n  private suspend fun startEmbeddedKafka(): KafkaExposedConfiguration = state.capture {\n    val config = EmbeddedKafkaConfig.apply(0, 0, `Map$`.`MODULE$`.empty(), `Map$`.`MODULE$`.empty(), `Map$`.`MODULE$`.empty())\n    val server = EmbeddedKafka.start(config)\n    while (!EmbeddedKafka.isRunning()) {\n      delay(100)\n    }\n    KafkaExposedConfiguration(\"0.0.0.0:${server.config().kafkaPort()}\", StoveKafkaBridge::class.java.name)\n  }\n\n  private suspend fun startKafkaContainer(container: StoveKafkaContainer): KafkaExposedConfiguration = state.capture {\n    container.start()\n    KafkaExposedConfiguration(container.bootstrapServers, StoveKafkaBridge::class.java.name)\n  }\n\n  private suspend fun stopEmbeddedKafka() {\n    EmbeddedKafka.stop()\n    while (EmbeddedKafka.isRunning()) {\n      delay(100)\n    }\n  }\n\n  @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)\n  @Suppress(\"MagicNumber\")\n  private suspend fun <K : Any, V : Any> consume(\n    autoOffsetReset: String,\n    readOnly: Boolean,\n    autoCreateTopics: Boolean,\n    config: (Properties) -> Unit,\n    keyDeserializer: Deserializer<K>,\n    valueDeserializer: Deserializer<V>,\n    topic: String,\n    pollTimeout: Duration,\n    keepConsumingAtLeastFor: Duration,\n    groupId: String,\n    onConsume: suspend (ConsumerRecord<K, V>) -> Unit\n  ) = coroutineScope {\n    val props = createConsumerProperties(autoOffsetReset, autoCreateTopics, groupId).apply(config)\n    val c = KafkaConsumer(props, keyDeserializer, valueDeserializer)\n    c.subscribe(listOf(topic))\n    val channel = Channel<ConsumerRecord<K, V>>()\n    val job = launch {\n      while (isActive) {\n        c.poll(pollTimeout.toJavaDuration()).forEach { channel.send(it) }\n        delay(100)\n      }\n    }\n    whileSelect {\n      onTimeout(keepConsumingAtLeastFor) {\n        c.close()\n        job.cancelAndJoin()\n        false\n      }\n      channel.onReceive {\n        onConsume(it)\n        if (!readOnly) c.commitSync()\n        !channel.isClosedForReceive\n      }\n    }\n  }\n\n  private fun createConsumerProperties(\n    autoOffsetReset: String,\n    autoCreateTopics: Boolean,\n    groupId: String\n  ): Properties = Properties().apply {\n    putAll(context.options.properties)\n    this[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = exposedConfiguration.bootstrapServers\n    this[ConsumerConfig.GROUP_ID_CONFIG] = groupId\n    this[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] = autoOffsetReset\n    this[ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG] = false\n    this[ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG] = autoCreateTopics\n    this[ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG] = exposedConfiguration.interceptorClass\n  }\n\n  private fun createPublisher(config: KafkaExposedConfiguration): KafkaProducer<String, Any> = KafkaProducer(\n    buildMap {\n      putAll(context.options.properties)\n      put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, config.bootstrapServers)\n      put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer::class.java.name)\n      put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, context.options.valueSerializer::class.java.name)\n      put(ProducerConfig.CLIENT_ID_CONFIG, \"stove-kafka-producer\")\n      put(ProducerConfig.ACKS_CONFIG, \"1\")\n      if (context.options.listenPublishedMessagesFromStove) {\n        put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, config.interceptorClass)\n      }\n    }\n  )\n\n  private fun createAdminClient(config: KafkaExposedConfiguration): Admin = Admin.create(\n    buildMap {\n      putAll(context.options.properties)\n      put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, config.bootstrapServers)\n      put(AdminClientConfig.CLIENT_ID_CONFIG, \"stove-kafka-admin-client\")\n    }.toProperties()\n  )\n\n  private suspend fun startGrpcServer(): Server {\n    System.setProperty(STOVE_KAFKA_BRIDGE_PORT, context.options.bridgeGrpcServerPort.toString())\n    return Try {\n      NettyServerBuilder\n        .forAddress(InetSocketAddress(InetAddress.getLoopbackAddress(), context.options.bridgeGrpcServerPort))\n        .executor(StoveKafkaCoroutineScope.also { it.ensureActive() }.asExecutor)\n        .addService(StoveKafkaObserverGrpcServer(sink))\n        .handshakeTimeout(GRPC_TIMEOUT_IN_SECONDS, java.util.concurrent.TimeUnit.SECONDS)\n        .permitKeepAliveTime(GRPC_TIMEOUT_IN_SECONDS, java.util.concurrent.TimeUnit.SECONDS)\n        .keepAliveTime(GRPC_TIMEOUT_IN_SECONDS, java.util.concurrent.TimeUnit.SECONDS)\n        .keepAliveTimeout(GRPC_TIMEOUT_IN_SECONDS, java.util.concurrent.TimeUnit.SECONDS)\n        .maxConnectionAge(GRPC_TIMEOUT_IN_SECONDS, java.util.concurrent.TimeUnit.SECONDS)\n        .maxConnectionAgeGrace(GRPC_TIMEOUT_IN_SECONDS, java.util.concurrent.TimeUnit.SECONDS)\n        .maxConnectionIdle(GRPC_TIMEOUT_IN_SECONDS, java.util.concurrent.TimeUnit.SECONDS)\n        .maxInboundMessageSize(MAX_MESSAGE_SIZE)\n        .maxInboundMetadataSize(MAX_MESSAGE_SIZE)\n        .permitKeepAliveWithoutCalls(true)\n        .build()\n        .start()\n        .also { waitUntilHealthy(it, 30.seconds) }\n    }.recover {\n      logger.error(\"Failed to start Stove Message Sink Grpc Server\", it)\n      throw it\n    }.map {\n      logger.info(\"Stove Sink Grpc Server started on port ${context.options.bridgeGrpcServerPort}\")\n      it\n    }.get()\n  }\n\n  private suspend fun waitUntilHealthy(server: Server, duration: Duration) {\n    val client = GrpcUtils.createClient(server.port.toString(), StoveKafkaCoroutineScope)\n    var healthy = false\n    withTimeout(duration) {\n      while (!healthy) {\n        logger.info(\"Waiting for Stove Message Sink Grpc Server to be healthy\")\n        Try {\n          val response = client.healthCheck().execute(HealthCheckRequest())\n          healthy = response.status == HealthCheckResponse.ServingStatus.SERVING\n        }\n        delay(GRPC_SERVER_DELAY)\n      }\n      logger.info(\"Stove Message Sink Grpc Server is healthy!\")\n    }\n  }\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StoveKafkaContainer) -> Unit\n  ): KafkaSystem = when (val runtime = context.runtime) {\n    is ProvidedRuntime, is EmbeddedKafkaRuntime -> {\n      logger.warn(\"$operation() is not supported when using embedded Kafka or a provided instance\")\n      this\n    }\n\n    is StoveKafkaContainer -> {\n      action(runtime)\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  companion object {\n    private const val GRPC_SERVER_DELAY = 500L\n    private const val GRPC_TIMEOUT_IN_SECONDS = 300L\n    private const val MAX_MESSAGE_SIZE = 1024 * 1024 * 1024\n  }\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/KafkaSystemOptions.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.kafka.intercepting.StoveKafkaBridge\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.apache.kafka.clients.admin.Admin\nimport org.apache.kafka.common.serialization.Serializer\n\n/**\n * Options for configuring the Kafka system in container or embedded mode.\n */\n@StoveDsl\nopen class KafkaSystemOptions(\n  /**\n   * When set to `true`, an embedded Kafka broker is automatically started and used for the test run.\n   * This is ideal for self-contained integration tests without external dependencies.\n   * When `false`, the system will attempt to connect to a TestContainer Kafka instance.\n   *\n   * The default is `false`.\n   */\n  open val useEmbeddedKafka: Boolean = false,\n  /**\n   * Suffixes for error and retry topics in the application.\n   */\n  open val topicSuffixes: TopicSuffixes = TopicSuffixes(),\n  /**\n   * If true, the system will listen to the messages published by the Kafka system.\n   */\n  open val listenPublishedMessagesFromStove: Boolean = false,\n  /**\n   * The port of the bridge gRPC server that is used to communicate with the Kafka system.\n   */\n  open val bridgeGrpcServerPort: Int = stoveKafkaBridgePortDefault.toInt(),\n  /**\n   * The Serde that is used while asserting the messages,\n   * serializing while bridging the messages.\n   *\n   * The default value is [StoveSerde.jackson]'s anyByteArraySerde.\n   *\n   * @see [com.trendyol.stove.kafka.intercepting.StoveKafkaBridge] for bridging the messages.\n   * @see StoveKafkaValueSerializer for serializing the messages.\n   * @see StoveKafkaValueDeserializer for deserializing the messages.\n   */\n  open val serde: StoveSerde<Any, ByteArray> = stoveSerdeRef,\n  /**\n   * The Value serializer that is used to serialize messages.\n   */\n  open val valueSerializer: Serializer<Any> = StoveKafkaValueSerializer(),\n  /**\n   * The options for the Kafka container.\n   */\n  open val containerOptions: KafkaContainerOptions = KafkaContainerOptions(),\n  /**\n   * A suspend function to clean up data after tests complete.\n   */\n  open val cleanup: suspend (Admin) -> Unit = {},\n  /**\n   * Additional Kafka client properties applied to all internal clients (admin, producer, consumer).\n   * Use this to pass security configs (SASL_SSL, truststore, etc.) when connecting to a secured cluster.\n   *\n   * Example:\n   * ```kotlin\n   * properties = mapOf(\n   *   \"security.protocol\" to \"SASL_SSL\",\n   *   \"sasl.mechanism\" to \"PLAIN\",\n   *   \"sasl.jaas.config\" to \"...PlainLoginModule required username=\\\"user\\\" password=\\\"pass\\\";\"\n   * )\n   * ```\n   */\n  open val properties: Map<String, Any> = emptyMap(),\n  /**\n   * The options for the Kafka system that is exposed to the application.\n   */\n  override val configureExposedConfiguration: (KafkaExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<KafkaExposedConfiguration>,\n  SupportsMigrations<KafkaMigrationContext, KafkaSystemOptions> {\n  override val migrationCollection: MigrationCollection<KafkaMigrationContext> = MigrationCollection()\n\n  companion object {\n    /**\n     * Creates options configured to use an externally provided Kafka instance\n     * instead of a testcontainer or embedded Kafka.\n     *\n     * @param bootstrapServers The Kafka bootstrap servers (e.g., \"localhost:9092\")\n     * @param topicSuffixes Suffixes for error and retry topics\n     * @param listenPublishedMessagesFromStove If true, the system will listen to published messages\n     * @param bridgeGrpcServerPort The port of the bridge gRPC server\n     * @param serde The Serde used for message serialization\n     * @param valueSerializer The Value serializer for messages\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      bootstrapServers: String,\n      topicSuffixes: TopicSuffixes = TopicSuffixes(),\n      listenPublishedMessagesFromStove: Boolean = false,\n      bridgeGrpcServerPort: Int = stoveKafkaBridgePortDefault.toInt(),\n      serde: StoveSerde<Any, ByteArray> = stoveSerdeRef,\n      valueSerializer: Serializer<Any> = StoveKafkaValueSerializer(),\n      properties: Map<String, Any> = emptyMap(),\n      runMigrations: Boolean = true,\n      cleanup: suspend (Admin) -> Unit = {},\n      configureExposedConfiguration: (KafkaExposedConfiguration) -> List<String>\n    ): ProvidedKafkaSystemOptions = ProvidedKafkaSystemOptions(\n      config = KafkaExposedConfiguration(\n        bootstrapServers = bootstrapServers,\n        interceptorClass = StoveKafkaBridge::class.java.name\n      ),\n      topicSuffixes = topicSuffixes,\n      listenPublishedMessagesFromStove = listenPublishedMessagesFromStove,\n      bridgeGrpcServerPort = bridgeGrpcServerPort,\n      serde = serde,\n      valueSerializer = valueSerializer,\n      properties = properties,\n      runMigrations = runMigrations,\n      cleanup = cleanup,\n      configureExposedConfiguration = configureExposedConfiguration\n    )\n  }\n}\n\n/**\n * Options for using an externally provided Kafka instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedKafkaSystemOptions(\n  /**\n   * The configuration for the provided Kafka instance.\n   */\n  val config: KafkaExposedConfiguration,\n  topicSuffixes: TopicSuffixes = TopicSuffixes(),\n  listenPublishedMessagesFromStove: Boolean = false,\n  bridgeGrpcServerPort: Int = stoveKafkaBridgePortDefault.toInt(),\n  serde: StoveSerde<Any, ByteArray> = stoveSerdeRef,\n  valueSerializer: Serializer<Any> = StoveKafkaValueSerializer(),\n  properties: Map<String, Any> = emptyMap(),\n  cleanup: suspend (Admin) -> Unit = {},\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  configureExposedConfiguration: (KafkaExposedConfiguration) -> List<String>\n) : KafkaSystemOptions(\n  useEmbeddedKafka = false,\n  topicSuffixes = topicSuffixes,\n  listenPublishedMessagesFromStove = listenPublishedMessagesFromStove,\n  bridgeGrpcServerPort = bridgeGrpcServerPort,\n  serde = serde,\n  valueSerializer = valueSerializer,\n  containerOptions = KafkaContainerOptions(),\n  properties = properties,\n  cleanup = cleanup,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<KafkaExposedConfiguration> {\n  override val providedConfig: KafkaExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n\n/**\n * Context provided to Kafka migrations.\n * Contains the Admin client and options for performing setup operations.\n *\n * @property admin The Kafka Admin client for managing topics, ACLs, etc.\n * @property options The Kafka system options\n */\n@StoveDsl\ndata class KafkaMigrationContext(\n  val admin: Admin,\n  val options: KafkaSystemOptions\n)\n\n/**\n * Convenience type alias for Kafka migrations.\n *\n * Instead of writing `DatabaseMigration<KafkaMigrationContext>`, use `KafkaMigration`:\n * ```kotlin\n * class MyMigration : KafkaMigration {\n *   override val order: Int = 1\n *   override suspend fun execute(connection: KafkaMigrationContext) { ... }\n * }\n * ```\n */\ntypealias KafkaMigration = DatabaseMigration<KafkaMigrationContext>\n\n/**\n * Suffixes for error and retry topics in the application.\n * Stove Kafka uses these suffixes to understand the intent of the topic and the message.\n */\ndata class TopicSuffixes(\n  val error: List<String> = listOf(\".error\", \".DLT\"),\n  val retry: List<String> = listOf(\".retry\")\n) {\n  fun isRetryTopic(topic: String): Boolean = retry.any { topic.endsWith(it, ignoreCase = true) }\n\n  fun isErrorTopic(topic: String): Boolean = error.any { topic.endsWith(it, ignoreCase = true) }\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/SerDe.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport org.apache.kafka.common.serialization.*\n\n@Suppress(\"UNCHECKED_CAST\")\nclass StoveKafkaValueDeserializer<T : Any> : Deserializer<T> {\n  override fun deserialize(\n    topic: String,\n    data: ByteArray\n  ): T = stoveSerdeRef.deserialize(data, Any::class.java) as T\n}\n\nclass StoveKafkaValueSerializer<T : Any> : Serializer<T> {\n  override fun serialize(\n    topic: String,\n    data: T\n  ): ByteArray = stoveSerdeRef.serialize(data)\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/coroutines.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport kotlinx.coroutines.*\nimport java.util.concurrent.*\n\nval CoroutineScope.asExecutor: Executor\n  get() = StoveCoroutineExecutor(this)\n\nval CoroutineScope.asExecutorService: ExecutorService\n  get() = CoroutineExecutorService(this)\n\ninternal class CoroutineExecutorService(\n  private val coroutineScope: CoroutineScope\n) : AbstractExecutorService() {\n  override fun execute(command: Runnable) {\n    coroutineScope.launch { command.run() }\n  }\n\n  override fun shutdown() {\n    coroutineScope.cancel()\n  }\n\n  override fun shutdownNow(): List<Runnable> {\n    coroutineScope.cancel()\n    return emptyList()\n  }\n\n  override fun isShutdown(): Boolean = coroutineScope.coroutineContext[Job]?.isCancelled ?: true\n\n  override fun isTerminated(): Boolean = coroutineScope.coroutineContext[Job]?.isCompleted ?: true\n\n  override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean {\n    // Coroutine jobs don't support await termination out of the box\n    // This is a simplified implementation\n    return isTerminated\n  }\n}\n\ninternal class StoveCoroutineExecutor(\n  private val scope: CoroutineScope\n) : Executor {\n  override fun execute(command: Runnable) {\n    scope.launch { command.run() }\n  }\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/intercepting/CommonOps.kt",
    "content": "package com.trendyol.stove.kafka.intercepting\n\nimport arrow.core.toOption\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.messaging.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport kotlinx.coroutines.*\nimport org.apache.kafka.clients.admin.Admin\nimport org.slf4j.Logger\nimport kotlin.reflect.KClass\nimport kotlin.time.Duration\n\ninternal interface CommonOps {\n  val store: MessageStore\n  val serde: StoveSerde<Any, ByteArray>\n  val adminClient: Admin\n  val topicSuffixes: TopicSuffixes\n  val logger: Logger\n\n  companion object {\n    const val DELAY_MS = 50L\n  }\n\n  suspend fun <T> (() -> Collection<T>).waitUntilConditionMet(\n    duration: Duration,\n    subject: String,\n    condition: (T) -> Boolean\n  ): Collection<T> = runCatching {\n    val collectionFunc = this\n    withTimeout(duration) {\n      while (!collectionFunc().any { condition(it) }) {\n        delay(DELAY_MS)\n      }\n    }\n    collectionFunc().filter { condition(it) }\n  }.fold(\n    onFailure = { throw AssertionError(\"GOT A TIMEOUT: $subject. ${dumpMessages()}\") },\n    onSuccess = { it }\n  )\n\n  suspend fun <T> (suspend () -> Collection<T>).waitUntilCount(\n    duration: Duration,\n    count: Int\n  ): Collection<T> = runCatching {\n    val collectionFunc = this\n    withTimeout(duration) {\n      while (collectionFunc().size < count) {\n        delay(DELAY_MS)\n      }\n    }\n    collectionFunc()\n  }.getOrElse {\n    throw AssertionError(\n      \"GOT A TIMEOUT: While expecting $count items to be retried, \" +\n        \"but was ${this().size}.\\n ${dumpMessages()}\"\n    )\n  }\n\n  fun <T : Any> throwIfFailed(\n    clazz: KClass<T>,\n    selector: (message: ParsedMessage<T>) -> Boolean\n  ): Unit = store\n    .failedMessages()\n    .filter {\n      selector(SuccessfulParsedMessage(deserializeCatching(it.message.toByteArray(), clazz).getOrNull().toOption(), it.metadata()))\n    }.forEach {\n      throw AssertionError(\"Message was expected to be consumed successfully, but failed: $it \\n ${dumpMessages()}\")\n    }\n\n  fun <T : Any> throwIfRetried(\n    clazz: KClass<T>,\n    selector: (message: ParsedMessage<T>) -> Boolean\n  ): Unit = store\n    .retriedMessages()\n    .filter {\n      selector(\n        SuccessfulParsedMessage(\n          deserializeCatching(it.message.toByteArray(), clazz).getOrNull().toOption(),\n          MessageMetadata(it.topic, it.key, it.headers)\n        )\n      )\n    }.forEach {\n      throw AssertionError(\"Message was expected to be consumed successfully, but was retried: $it \\n ${dumpMessages()}\")\n    }\n\n  fun <T : Any> deserializeCatching(\n    value: ByteArray,\n    clazz: KClass<T>\n  ): Result<T> = runCatching { serde.deserialize(value, clazz.java) }\n    .onFailure { exception -> logger.debug(\"Failed to deserialize message: ${String(value)}\", exception) }\n\n  fun dumpMessages(): String\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/intercepting/GrpcUtils.kt",
    "content": "@file:Suppress(\"HttpUrlsUsage\")\n\npackage com.trendyol.stove.kafka.intercepting\n\nimport com.squareup.wire.*\nimport com.trendyol.stove.kafka.*\nimport kotlinx.coroutines.CoroutineScope\nimport okhttp3.*\nimport java.net.InetAddress\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.toJavaDuration\n\nobject GrpcUtils {\n  private val getClient = { scope: CoroutineScope ->\n    OkHttpClient\n      .Builder()\n      .protocols(listOf(Protocol.H2_PRIOR_KNOWLEDGE))\n      .callTimeout(30.seconds.toJavaDuration())\n      .readTimeout(30.seconds.toJavaDuration())\n      .writeTimeout(30.seconds.toJavaDuration())\n      .connectTimeout(30.seconds.toJavaDuration())\n      .dispatcher(Dispatcher(scope.asExecutorService))\n      .build()\n  }\n\n  fun createClient(onPort: String, scope: CoroutineScope): StoveKafkaObserverServiceClient = GrpcClient\n    .Builder()\n    .client(getClient(scope))\n    .baseUrl(onLoopback(onPort))\n    .build()\n    .create<StoveKafkaObserverServiceClient>()\n\n  private fun onLoopback(port: String): GrpcHttpUrl = \"http://${InetAddress.getLoopbackAddress().hostAddress}:$port\".toHttpUrl()\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/intercepting/MessageSinkOps.kt",
    "content": "package com.trendyol.stove.kafka.intercepting\n\nimport arrow.core.toOption\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.messaging.*\nimport kotlinx.coroutines.runBlocking\nimport kotlin.reflect.KClass\nimport kotlin.time.Duration\n\ninternal interface MessageSinkOps :\n  MessageSinkPublishOps,\n  CommonOps {\n  fun recordConsumed(record: ConsumedMessage): Unit = runBlocking {\n    store.record(record)\n    logger.info(\n      \"Recorded Consumed Message: {}, testCase: {}\",\n      record,\n      record.headers.firstNotNullOf { it.key == \"testCase\" }\n    )\n  }\n\n  fun recordRetry(record: ConsumedMessage): Unit = runBlocking {\n    store.recordRetry(record)\n    logger.info(\n      \"Recorded Retried Message: {}, testCase: {}\",\n      record,\n      record.headers.firstNotNullOf { it.key == \"testCase\" }\n    )\n  }\n\n  fun recordCommittedMessage(record: CommittedMessage): Unit = runBlocking {\n    store.record(record)\n    logger.info(\"Recorded Committed Message:{}\", record)\n  }\n\n  fun recordAcknowledgedMessage(record: AcknowledgedMessage): Unit = runBlocking {\n    store.record(record)\n    logger.info(\"Recorded Acknowledged Message:{}\", record)\n  }\n\n  fun recordError(record: ConsumedMessage): Unit = runBlocking {\n    store.recordFailure(record)\n    logger.info(\"Recorded Failed Message: {}\", record)\n  }\n\n  suspend fun <T : Any> waitUntilConsumed(\n    atLeastIn: Duration,\n    clazz: KClass<T>,\n    condition: (metadata: ParsedMessage<T>) -> Boolean\n  ) {\n    val getRecords = { store.consumedMessages() }\n    getRecords.waitUntilConditionMet(atLeastIn, \"While expecting consuming of ${clazz.java.simpleName}\") {\n      val outcome = deserializeCatching(it.message.toByteArray(), clazz)\n      outcome.isSuccess &&\n        condition(\n          SuccessfulParsedMessage(\n            outcome.getOrNull().toOption(),\n            it.metadata()\n          )\n        ) &&\n        store.isCommitted(it.topic, it.offset, it.partition)\n    }\n\n    throwIfFailed(clazz, condition)\n    throwIfRetried(clazz, condition)\n  }\n\n  suspend fun <T : Any> waitUntilFailed(\n    atLeastIn: Duration,\n    clazz: KClass<T>,\n    condition: (ParsedMessage<T>) -> Boolean\n  ) {\n    class FailedMessage(\n      val message: ByteArray,\n      val metadata: MessageMetadata\n    )\n\n    val getRecords = {\n      store.failedMessages().map { FailedMessage(it.message.toByteArray(), it.metadata()) } +\n        store\n          .publishedMessages()\n          .filter { topicSuffixes.isErrorTopic(it.topic) }\n          .map { FailedMessage(it.message.toByteArray(), it.metadata()) }\n    }\n    getRecords.waitUntilConditionMet(atLeastIn, \"While expecting Failure of ${clazz.java.simpleName}\") {\n      val outcome = deserializeCatching(it.message, clazz)\n      outcome.isSuccess && condition(SuccessfulParsedMessage(outcome.getOrNull().toOption(), it.metadata))\n    }\n  }\n\n  suspend fun <T : Any> waitUntilRetried(\n    atLeastIn: Duration,\n    times: Int = 1,\n    clazz: KClass<T>,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ) {\n    val getRecords = { store.retriedMessages() }\n    val failedFunc = suspend {\n      getRecords.waitUntilConditionMet(atLeastIn, \"While expecting Retrying of ${clazz.java.simpleName}\") {\n        val outcome = deserializeCatching(it.message.toByteArray(), clazz)\n        outcome.isSuccess &&\n          condition(\n            SuccessfulParsedMessage(\n              outcome.getOrNull().toOption(),\n              MessageMetadata(it.topic, it.key, it.headers)\n            )\n          )\n      }\n    }\n\n    failedFunc.waitUntilCount(atLeastIn, times)\n  }\n\n  override fun dumpMessages(): String = \"Sink so far:\\n$store\"\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/intercepting/MessageSinkPublishOps.kt",
    "content": "package com.trendyol.stove.kafka.intercepting\n\nimport arrow.core.toOption\nimport com.trendyol.stove.kafka.PublishedMessage\nimport com.trendyol.stove.messaging.*\nimport kotlinx.coroutines.runBlocking\nimport kotlin.reflect.KClass\nimport kotlin.time.Duration\n\ninternal interface MessageSinkPublishOps : CommonOps {\n  suspend fun <T : Any> waitUntilPublished(\n    atLeastIn: Duration,\n    clazz: KClass<T>,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ) {\n    val getRecords = { store.publishedMessages().map { it } }\n    getRecords.waitUntilConditionMet(atLeastIn, \"While expecting Publishing of ${clazz.java.simpleName}\") {\n      val outcome = deserializeCatching(it.message.toByteArray(), clazz)\n      outcome.isSuccess && condition(SuccessfulParsedMessage(outcome.getOrNull().toOption(), MessageMetadata(it.topic, it.key, it.headers)))\n    }\n  }\n\n  fun recordPublishedMessage(record: PublishedMessage): Unit = runBlocking {\n    store.record(record)\n    logger.info(\n      \"Recorded Published Message: {}, testCase: {}\",\n      record,\n      record.headers.firstNotNullOf { it.key == \"testCase\" }\n    )\n  }\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/intercepting/MessageStore.kt",
    "content": "package com.trendyol.stove.kafka.intercepting\n\nimport com.trendyol.stove.kafka.*\nimport io.exoquery.pprint\n\nclass MessageStore {\n  private val consumed = Caching.of<String, ConsumedMessage>()\n  private val published = Caching.of<String, PublishedMessage>()\n  private val committed = Caching.of<String, CommittedMessage>()\n  private val retried = Caching.of<String, ConsumedMessage>()\n  private val failedMessages = Caching.of<String, ConsumedMessage>()\n  private val acknowledged = Caching.of<String, AcknowledgedMessage>()\n\n  internal fun record(message: ConsumedMessage) = consumed.put(message.id, message)\n\n  internal fun record(message: PublishedMessage) = published.put(message.id, message)\n\n  internal fun record(message: CommittedMessage) = committed.put(message.id, message)\n\n  internal fun record(message: AcknowledgedMessage) = acknowledged.put(message.id, message)\n\n  internal fun recordRetry(message: ConsumedMessage) = retried.put(message.id, message)\n\n  internal fun recordFailure(message: ConsumedMessage) = failedMessages.put(message.id, message)\n\n  fun failedMessages(): Collection<ConsumedMessage> = failedMessages.asMap().values\n\n  fun consumedMessages(): Collection<ConsumedMessage> = consumed.asMap().values\n\n  fun publishedMessages(): Collection<PublishedMessage> = published.asMap().values\n\n  fun committedMessages(): Collection<CommittedMessage> = committed.asMap().values\n\n  fun retriedMessages(): Collection<ConsumedMessage> = retried.asMap().values\n\n  internal fun isCommitted(\n    topic: String,\n    offset: Long,\n    partition: Int\n  ): Boolean = committedMessages()\n    .filter { it.topic == topic && it.partition == partition }\n    .any { committed -> committed.offset >= offset + 1 }\n\n  override fun toString(): String = \"\"\"\n    |Consumed: ${pprint(consumedMessages())}\n    |Published: ${pprint(publishedMessages())}\n    |Committed: ${pprint(committedMessages())}\n    |Retried: ${pprint(retriedMessages())}\n    |Failed: ${pprint(failedMessages())}\n  \"\"\".trimIndent().trimMargin()\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/intercepting/StoveKafkaBridge.kt",
    "content": "package com.trendyol.stove.kafka.intercepting\n\nimport com.squareup.wire.GrpcException\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport kotlinx.coroutines.runBlocking\nimport okio.ByteString.Companion.toByteString\nimport org.apache.kafka.clients.consumer.*\nimport org.apache.kafka.clients.producer.*\nimport org.apache.kafka.common.TopicPartition\nimport org.slf4j.Logger\nimport java.nio.charset.Charset\nimport java.util.*\n\n@Suppress(\"UNUSED\")\nclass StoveKafkaBridge<K, V> :\n  ConsumerInterceptor<K, V>,\n  ProducerInterceptor<K, V> {\n  private val logger: Logger = org.slf4j.LoggerFactory.getLogger(StoveKafkaBridge::class.java)\n  private val client: StoveKafkaObserverServiceClient by lazy { startGrpcClient() }\n  private val serde: StoveSerde<Any, ByteArray> by lazy { stoveSerdeRef }\n\n  override fun onSend(record: ProducerRecord<K, V>): ProducerRecord<K, V> = runBlocking {\n    record.also { send(publishedMessage(it)) }\n  }\n\n  override fun onConsume(records: ConsumerRecords<K, V>): ConsumerRecords<K, V> = runBlocking {\n    records.also { consumedMessages(it).forEach { message -> send(message) } }\n  }\n\n  override fun onCommit(offsets: MutableMap<TopicPartition, OffsetAndMetadata>) = runBlocking {\n    committedMessages(offsets).forEach { send(it) }\n  }\n\n  override fun configure(configs: MutableMap<String, *>) = Unit\n\n  override fun close() = Unit\n\n  override fun onAcknowledgement(metadata: RecordMetadata?, exception: Exception?) = runBlocking {\n    ackedMessages(metadata, exception).forEach { send(it) }\n  }\n\n  private suspend fun send(consumedMessage: ConsumedMessage) {\n    Try {\n      client.onConsumedMessage().execute(consumedMessage)\n    }.map {\n      logger.info(\"Consumed message sent to Stove Kafka Bridge: $consumedMessage\")\n    }.recover { e ->\n      when {\n        e is GrpcException && e.grpcStatus.code == 2 && e.grpcStatus.name == \"UNKNOWN\" -> Unit\n        else -> logger.error(\"Failed to send consumed message to Stove Kafka Bridge: $consumedMessage\", e)\n      }\n    }\n  }\n\n  private suspend fun send(committedMessage: CommittedMessage) {\n    Try {\n      client.onCommittedMessage().execute(committedMessage)\n    }.map {\n      logger.info(\"Committed message sent to Stove Kafka Bridge: $committedMessage\")\n    }.recover { e ->\n      when {\n        e is GrpcException && e.grpcStatus.code == 2 && e.grpcStatus.name == \"UNKNOWN\" -> Unit\n        else -> logger.error(\"Failed to send committed message to Stove Kafka Bridge: $committedMessage\", e)\n      }\n    }\n  }\n\n  private suspend fun send(publishedMessage: PublishedMessage) {\n    Try {\n      client.onPublishedMessage().execute(publishedMessage)\n    }.map {\n      logger.info(\"Published message sent to Stove Kafka Bridge: $publishedMessage\")\n    }.recover { e ->\n      when {\n        e is GrpcException && e.grpcStatus.code == 2 && e.grpcStatus.name == \"UNKNOWN\" -> Unit\n        else -> logger.error(\"Failed to send published message to Stove Kafka Bridge: $publishedMessage\", e)\n      }\n    }\n  }\n\n  private suspend fun send(ackedMessage: AcknowledgedMessage) {\n    Try {\n      client.onAcknowledgedMessage().execute(ackedMessage)\n    }.map {\n      logger.info(\"Acknowledged message sent to Stove Kafka Bridge: $ackedMessage\")\n    }.recover { e ->\n      when {\n        e is GrpcException && e.grpcStatus.code == 2 && e.grpcStatus.name == \"UNKNOWN\" -> Unit\n        else -> logger.error(\"Failed to send acknowledged message to Stove Kafka Bridge: $ackedMessage\", e)\n      }\n    }\n  }\n\n  private fun ackedMessages(metadata: RecordMetadata?, exception: Exception?): List<AcknowledgedMessage> {\n    val ackedMessage = AcknowledgedMessage(\n      id = UUID.randomUUID().toString(),\n      topic = metadata?.topic() ?: \"\",\n      partition = metadata?.partition() ?: -1,\n      offset = metadata?.offset() ?: -1,\n      exception = exception?.message ?: \"\"\n    )\n    return listOf(ackedMessage)\n  }\n\n  private fun consumedMessages(records: ConsumerRecords<K, V>) = records.map { record ->\n    ConsumedMessage(\n      id = UUID.randomUUID().toString(),\n      key = record.key().toString(),\n      message = serializeIfNotYet(record.value()).toByteString(),\n      topic = record.topic(),\n      offset = record.offset(),\n      partition = record.partition(),\n      headers = record.headers().associate { it.key() to it.value().toString(Charset.defaultCharset()) }\n    )\n  }\n\n  private fun publishedMessage(record: ProducerRecord<K, V>) = PublishedMessage(\n    id = UUID.randomUUID().toString(),\n    key = record.key().toString(),\n    message = serializeIfNotYet(record.value()).toByteString(),\n    topic = record.topic(),\n    headers = record.headers().associate { it.key() to it.value().toString(Charset.defaultCharset()) }\n  )\n\n  private fun committedMessages(\n    offsets: Map<TopicPartition, OffsetAndMetadata>\n  ): List<CommittedMessage> = offsets.map {\n    CommittedMessage(\n      id = UUID.randomUUID().toString(),\n      topic = it.key.topic(),\n      partition = it.key.partition(),\n      offset = it.value.offset(),\n      metadata = it.value.metadata()\n    )\n  }\n\n  private fun serializeIfNotYet(value: V): ByteArray = when (value) {\n    is ByteArray -> value\n    else -> serde.serialize(value as Any)\n  }\n\n  private fun startGrpcClient(): StoveKafkaObserverServiceClient {\n    val onPort = System.getenv(STOVE_KAFKA_BRIDGE_PORT)\n      ?: System.getProperty(STOVE_KAFKA_BRIDGE_PORT)\n      ?: stoveKafkaBridgePortDefault\n    logger.info(\"Connecting to Stove Kafka Bridge on port $onPort\")\n    return Try { GrpcUtils.createClient(onPort, StoveKafkaCoroutineScope) }\n      .map {\n        logger.info(\"Stove Kafka Observer Client created on port $onPort\")\n        it\n      }.getOrElse { error(\"failed to connect Stove Kafka observer client\") }\n  }\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/intercepting/StoveKafkaObserverGrpcServer.kt",
    "content": "package com.trendyol.stove.kafka.intercepting\n\nimport com.trendyol.stove.kafka.*\nimport org.slf4j.*\n\nclass StoveKafkaObserverGrpcServer(\n  private val sink: StoveMessageSink\n) : StoveKafkaObserverServiceWireGrpc.StoveKafkaObserverServiceImplBase() {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun healthCheck(request: HealthCheckRequest): HealthCheckResponse {\n    logger.info(\"Received health check request: $request\")\n    return HealthCheckResponse(status = HealthCheckResponse.ServingStatus.SERVING)\n  }\n\n  override suspend fun onPublishedMessage(request: PublishedMessage): Reply {\n    logger.info(\"Received published message: $request\")\n    sink.onMessagePublished(request)\n    return Reply(status = 200)\n  }\n\n  override suspend fun onConsumedMessage(request: ConsumedMessage): Reply {\n    logger.info(\"Received consumed message: $request\")\n    sink.onMessageConsumed(request)\n    return Reply(status = 200)\n  }\n\n  override suspend fun onCommittedMessage(request: CommittedMessage): Reply {\n    logger.info(\"Received committed message: $request\")\n    sink.onMessageCommitted(request)\n    return Reply(status = 200)\n  }\n\n  override suspend fun onAcknowledgedMessage(request: AcknowledgedMessage): Reply {\n    logger.info(\"Received acknowledged message: $request\")\n    sink.onMessageAcknowledged(request)\n    return Reply(status = 200)\n  }\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/intercepting/StoveMessageSink.kt",
    "content": "package com.trendyol.stove.kafka.intercepting\n\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport org.apache.kafka.clients.admin.Admin\nimport org.slf4j.*\n\nclass StoveMessageSink(\n  override val adminClient: Admin,\n  override val serde: StoveSerde<Any, ByteArray>,\n  override val topicSuffixes: TopicSuffixes\n) : MessageSinkOps,\n  CommonOps {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n  override val store: MessageStore = MessageStore()\n\n  fun onMessageConsumed(record: ConsumedMessage): Unit = when {\n    topicSuffixes.isErrorTopic(record.topic) -> recordError(record)\n    topicSuffixes.isRetryTopic(record.topic) -> recordRetry(record)\n    else -> recordConsumed(record)\n  }\n\n  fun onMessagePublished(record: PublishedMessage): Unit = recordPublishedMessage(record)\n\n  fun onMessageCommitted(record: CommittedMessage): Unit = recordCommittedMessage(record)\n\n  fun onMessageAcknowledged(record: AcknowledgedMessage): Unit = recordAcknowledgedMessage(record)\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/main/kotlin/com/trendyol/stove/kafka/messages.kt",
    "content": "package com.trendyol.stove.kafka\n\nclass PublishedRecord(\n  val topic: String,\n  val key: String,\n  val value: ByteArray,\n  val headers: Map<String, String>\n)\n\nclass CommittedRecord(\n  val topic: String,\n  val metadata: String,\n  val offset: Long,\n  val partition: Int\n)\n\nclass ConsumedRecord(\n  val topic: String,\n  val key: String,\n  val value: ByteArray,\n  val headers: Map<String, String>,\n  val offset: Long,\n  val partition: Int\n)\n"
  },
  {
    "path": "lib/stove-kafka/src/main/proto/messages.proto",
    "content": "syntax = \"proto3\";\n\n// buf:lint:ignore FILE_SAME_PACKAGE\npackage com.trendyol.stove.kafka;\n\nmessage ConsumedMessage {\n  string id = 1;\n  bytes message = 2;\n  string topic = 3;\n  int32 partition = 4;\n  int64 offset = 5;\n  string key = 6;\n  map<string, string> headers = 8;\n}\n\nmessage PublishedMessage {\n  string id = 1;\n  bytes message = 2;\n  string topic = 3;\n  string key = 4;\n  map<string, string> headers = 5;\n}\n\nmessage CommittedMessage {\n  string id = 1;\n  string topic = 2;\n  int32 partition = 3;\n  int64 offset = 4;\n  string metadata = 5;\n}\n\nmessage AcknowledgedMessage {\n  string id = 1;\n  string topic = 2;\n  int32 partition = 3;\n  int64 offset = 4;\n  string exception = 5;\n}\n\nmessage Reply {\n  int32 status = 3;\n}\n\nmessage HealthCheckRequest {\n  string service = 1;\n}\n\nmessage HealthCheckResponse {\n  enum ServingStatus {\n    UNKNOWN = 0;\n    SERVING = 1;\n    NOT_SERVING = 2;\n    SERVICE_UNKNOWN = 3; // Used only by the Watch method.\n  }\n  ServingStatus status = 1;\n}\n\nservice StoveKafkaObserverService {\n  rpc healthCheck(HealthCheckRequest) returns (HealthCheckResponse) {}\n\n  // buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n  rpc onConsumedMessage(ConsumedMessage) returns (Reply) {}\n\n  // buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n  rpc onPublishedMessage(PublishedMessage) returns (Reply) {}\n\n  // buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n  rpc onCommittedMessage(CommittedMessage) returns (Reply) {}\n\n  // buf:lint:ignore RPC_REQUEST_RESPONSE_UNIQUE\n  rpc onAcknowledgedMessage(AcknowledgedMessage) returns (Reply) {}\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/setup/TestSystemConfig.kt",
    "content": "package com.trendyol.stove.kafka.setup\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.kafka.setup.example.KafkaTestShared\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport io.github.nomisRev.kafka.publisher.PublisherSettings\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.apache.kafka.clients.admin.*\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.*\nimport org.slf4j.*\nimport org.testcontainers.kafka.ConfluentKafkaContainer\nimport org.testcontainers.utility.DockerImageName\nimport java.util.*\n\n// ============================================================================\n// Shared components\n// ============================================================================\n\nclass KafkaApplicationUnderTest : ApplicationUnderTest<Unit> {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private lateinit var client: AdminClient\n  private val consumers: MutableList<AutoCloseable> = mutableListOf()\n\n  override suspend fun start(configurations: List<String>) {\n    val bootstrapServers = configurations.first { it.contains(\"kafka\", true) }.split('=')[1]\n    logger.info(\"Starting Kafka application with bootstrap servers: $bootstrapServers\")\n\n    client = mapOf<String, Any>(\n      AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers\n    ).let { AdminClient.create(it) }\n\n    val newTopics = KafkaTestShared.topics\n      .flatMap { listOf(it.topic, it.retryTopic, it.deadLetterTopic) }\n      .map { NewTopic(it, 1, 1) }\n    client.createTopics(newTopics).all().get()\n    startConsumers(bootstrapServers)\n  }\n\n  private suspend fun startConsumers(bootStrapServers: String) {\n    val consumerSettings = mapOf(\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootStrapServers,\n      ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to \"2000\",\n      ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG to \"true\",\n      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to StoveKafkaValueDeserializer::class.java,\n      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java,\n      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to \"earliest\",\n      ConsumerConfig.GROUP_ID_CONFIG to \"stove-application-consumers\",\n      ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG to listOf(\"com.trendyol.stove.kafka.intercepting.StoveKafkaBridge\")\n    )\n\n    val producerSettings = PublisherSettings<String, Any>(\n      bootStrapServers,\n      StringSerializer(),\n      StoveKafkaValueSerializer(),\n      properties = Properties().apply {\n        put(\n          ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,\n          listOf(\"com.trendyol.stove.kafka.intercepting.StoveKafkaBridge\")\n        )\n      }\n    )\n\n    val listeners = KafkaTestShared.consumers(consumerSettings, producerSettings)\n    listeners.forEach { it.start() }\n    consumers.addAll(listeners)\n  }\n\n  override suspend fun stop() {\n    client.close()\n    consumers.forEach { it.close() }\n  }\n}\n\n/**\n * Migration that creates additional topics for testing.\n */\nclass CreateTestTopicsMigration : KafkaMigration {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override val order: Int = 1\n\n  override suspend fun execute(connection: KafkaMigrationContext) {\n    logger.info(\"Executing CreateTestTopicsMigration\")\n    val topics = listOf(\n      NewTopic(\"migration-test-topic\", 1, 1),\n      NewTopic(\"migration-test-topic-2\", 2, 1)\n    )\n    connection.admin\n      .createTopics(topics)\n      .all()\n      .get()\n    logger.info(\"Created migration test topics\")\n  }\n}\n\n// ============================================================================\n// Strategy interface\n// ============================================================================\n\nsealed interface KafkaTestStrategy {\n  val logger: Logger\n\n  suspend fun start()\n\n  suspend fun stop()\n\n  companion object {\n    fun select(): KafkaTestStrategy {\n      val useProvided = System.getenv(\"USE_PROVIDED\")?.toBoolean()\n        ?: System.getProperty(\"useProvided\")?.toBoolean()\n        ?: false\n\n      val useEmbedded = System.getenv(\"USE_EMBEDDED\")?.toBoolean()\n        ?: System.getProperty(\"useEmbeddedKafka\")?.toBoolean()\n        ?: false\n\n      return when {\n        useProvided -> ProvidedKafkaStrategy()\n        useEmbedded -> EmbeddedKafkaStrategy()\n        else -> ContainerKafkaStrategy()\n      }\n    }\n  }\n}\n\n// ============================================================================\n// Container-based strategy (default)\n// ============================================================================\n\nclass ContainerKafkaStrategy : KafkaTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  init {\n    setupBridgePort()\n  }\n\n  override suspend fun start() {\n    logger.info(\"Starting Kafka tests with container mode\")\n\n    val options = KafkaSystemOptions(\n      useEmbeddedKafka = false,\n      listenPublishedMessagesFromStove = true,\n      containerOptions = KafkaContainerOptions(tag = \"8.0.3\"),\n      configureExposedConfiguration = { cfg ->\n        listOf(\"kafka.servers=${cfg.bootstrapServers}\")\n      }\n    ).migrations {\n      register<CreateTestTopicsMigration>()\n    }\n\n    Stove()\n      .with {\n        kafka { options }\n        applicationUnderTest(KafkaApplicationUnderTest())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    logger.info(\"Kafka container tests completed\")\n  }\n}\n\n// ============================================================================\n// Embedded Kafka strategy\n// ============================================================================\n\nclass EmbeddedKafkaStrategy : KafkaTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  init {\n    setupBridgePort()\n  }\n\n  override suspend fun start() {\n    logger.info(\"Starting Kafka tests with embedded mode\")\n\n    val options = KafkaSystemOptions(\n      useEmbeddedKafka = true,\n      listenPublishedMessagesFromStove = true,\n      configureExposedConfiguration = { cfg ->\n        listOf(\"kafka.servers=${cfg.bootstrapServers}\")\n      }\n    ).migrations {\n      register<CreateTestTopicsMigration>()\n    }\n\n    Stove()\n      .with {\n        kafka { options }\n        applicationUnderTest(KafkaApplicationUnderTest())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    logger.info(\"Kafka embedded tests completed\")\n  }\n}\n\n// ============================================================================\n// Provided instance strategy\n// ============================================================================\n\nclass ProvidedKafkaStrategy : KafkaTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  private lateinit var externalContainer: ConfluentKafkaContainer\n\n  init {\n    setupBridgePort()\n  }\n\n  override suspend fun start() {\n    logger.info(\"Starting Kafka tests with provided mode\")\n\n    // Start an external container to simulate a provided instance\n    externalContainer = ConfluentKafkaContainer(\n      DockerImageName.parse(\"confluentinc/cp-kafka:7.8.1\")\n    ).apply { start() }\n\n    logger.info(\"External Kafka container started at ${externalContainer.bootstrapServers}\")\n\n    val options = KafkaSystemOptions\n      .provided(\n        bootstrapServers = externalContainer.bootstrapServers,\n        listenPublishedMessagesFromStove = true,\n        runMigrations = true,\n        cleanup = { admin ->\n          logger.info(\"Running cleanup on provided instance\")\n          val topics = admin\n            .listTopics()\n            .names()\n            .get()\n            .filter { it.startsWith(\"migration-test\") }\n          if (topics.isNotEmpty()) {\n            admin.deleteTopics(topics).all().get()\n          }\n        },\n        configureExposedConfiguration = { cfg ->\n          listOf(\"kafka.servers=${cfg.bootstrapServers}\")\n        }\n      ).migrations {\n        register<CreateTestTopicsMigration>()\n      }\n\n    Stove()\n      .with {\n        kafka { options }\n        applicationUnderTest(KafkaApplicationUnderTest())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    externalContainer.stop()\n    logger.info(\"Kafka provided tests completed\")\n  }\n}\n\n// ============================================================================\n// Helper function\n// ============================================================================\n\nprivate fun setupBridgePort() {\n  stoveKafkaBridgePortDefault = PortFinder.findAvailablePortAsString()\n  System.setProperty(STOVE_KAFKA_BRIDGE_PORT, stoveKafkaBridgePortDefault)\n}\n\n// ============================================================================\n// Kotest project config - selects strategy based on environment\n// ============================================================================\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val strategy = KafkaTestStrategy.select()\n\n  override suspend fun beforeProject() = strategy.start()\n\n  override suspend fun afterProject() = strategy.stop()\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/setup/example/DomainEvents.kt",
    "content": "package com.trendyol.stove.kafka.setup.example\n\nimport kotlin.random.Random\n\nobject DomainEvents {\n  data class ProductCreated(\n    val productId: String\n  ) {\n    companion object {\n      val randomString = { Random.nextInt(0, Int.MAX_VALUE).toString() }\n\n      fun randoms(count: Int): List<ProductCreated> = (0 until count).map { ProductCreated(randomString()) }\n    }\n  }\n\n  data class ProductFailingCreated(\n    val productId: String\n  )\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/setup/example/KafkaTestShared.kt",
    "content": "package com.trendyol.stove.kafka.setup.example\n\nimport com.trendyol.stove.kafka.setup.example.consumers.*\nimport io.github.nomisRev.kafka.publisher.PublisherSettings\n\nobject KafkaTestShared {\n  data class TopicDefinition(\n    val topic: String,\n    val retryTopic: String,\n    val deadLetterTopic: String\n  )\n\n  val topics = listOf(\n    TopicDefinition(\"product\", \"product.retry\", \"product.error\"),\n    TopicDefinition(\"productFailing\", \"productFailing.retry\", \"productFailing.error\")\n  )\n  val consumers: (\n    consumerSettings: Map<String, Any>,\n    producerSettings: PublisherSettings<String, Any>\n  ) -> List<StoveListener> = { a, b ->\n    listOf(\n      ProductConsumer(a, b),\n      ProductFailingConsumer(a, b)\n    )\n  }\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/setup/example/StoveListener.kt",
    "content": "package com.trendyol.stove.kafka.setup.example\n\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.kafka.setup.example.KafkaTestShared.TopicDefinition\nimport com.trendyol.stove.kafka.setup.example.consumers.*\nimport io.github.nomisRev.kafka.publisher.*\nimport kotlinx.coroutines.*\nimport org.apache.kafka.clients.consumer.*\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport java.time.Duration\n\nabstract class StoveListener(\n  consumerSettings: Map<String, Any>,\n  publisherSettings: PublisherSettings<String, Any>\n) : AutoCloseable {\n  private val logger = org.slf4j.LoggerFactory.getLogger(javaClass)\n  abstract val topicDefinition: TopicDefinition\n\n  private val consumer: KafkaConsumer<String, String> = KafkaConsumer<String, String>(consumerSettings)\n  private val publisher: KafkaPublisher<String, Any> = KafkaPublisher(publisherSettings)\n  private var scope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n\n  suspend fun start() {\n    consumer.subscribe(listOf(topicDefinition.topic, topicDefinition.retryTopic, topicDefinition.deadLetterTopic))\n    scope.launch {\n      while (isActive) {\n        consumer\n          .poll(Duration.ofMillis(100))\n          .forEach { message ->\n            logger.info(\"Message RECEIVED on the application side: ${message.value()}\")\n            consume(message, consumer)\n          }\n      }\n    }\n  }\n\n  private suspend fun consume(message: ConsumerRecord<String, String>, consumer: KafkaConsumer<String, String>) {\n    Try { listen(message) }\n      .map {\n        logger.info(\"Message COMMITTED on the application side: ${message.value()}\")\n        consumer.commitAsync()\n      }.recover {\n        logger.warn(\"CONSUMER GOT an ERROR on the application side, exception: $it\")\n        if (message.getRetryCount() < 3) {\n          logger.warn(\"CONSUMER GOT an ERROR, retrying...\")\n          try {\n            message.incrementRetryCount()\n            publisher.publishScope {\n              offer(\n                ProducerRecord(\n                  topicDefinition.retryTopic,\n                  message.partition(),\n                  message.key(),\n                  message.value(),\n                  message.headers()\n                )\n              )\n            }\n          } catch (e: Exception) {\n            logger.error(\"Failed to publish message to retry topic: $message\", e)\n          }\n        } else {\n          logger.error(\"CONSUMER GOT an ERROR, retry limit exceeded: $message\")\n          val record = ProducerRecord<String, Any>(\n            topicDefinition.deadLetterTopic,\n            message.partition(),\n            message.key(),\n            message.value(),\n            message.headers()\n          ).apply {\n            headers().add(\"doNotFail\", \"true\".toByteArray())\n          }\n          try {\n            publisher.publishScope { offer(record) }\n          } catch (e: Exception) {\n            logger.error(\"Failed to publish message to dead letter topic: $message\", e)\n          }\n        }\n      }\n  }\n\n  abstract suspend fun listen(record: ConsumerRecord<String, String>)\n\n  override fun close(): Unit = scope.cancel()\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/setup/example/consumers/ProductConsumer.kt",
    "content": "package com.trendyol.stove.kafka.setup.example.consumers\n\nimport com.trendyol.stove.kafka.setup.example.KafkaTestShared.TopicDefinition\nimport com.trendyol.stove.kafka.setup.example.StoveListener\nimport io.github.nomisRev.kafka.publisher.PublisherSettings\nimport org.apache.kafka.clients.consumer.ConsumerRecord\n\n// TODO: Convert into in-flight consumer\nclass ProductConsumer(\n  consumerSettings: Map<String, Any>,\n  producerSettings: PublisherSettings<String, Any>\n) : StoveListener(consumerSettings, producerSettings) {\n  private val logger = org.slf4j.LoggerFactory.getLogger(javaClass)\n  override val topicDefinition: TopicDefinition = TopicDefinition(\"product\", \"product.retry\", \"product.error\")\n\n  override suspend fun listen(record: ConsumerRecord<String, String>) {\n    logger.info(\"Product consumed: ${record.value()} from topic: ${record.topic()} with key: ${record.key()}\")\n  }\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/setup/example/consumers/ProductFailingConsumer.kt",
    "content": "package com.trendyol.stove.kafka.setup.example.consumers\n\nimport arrow.core.*\nimport com.trendyol.stove.kafka.setup.example.KafkaTestShared.TopicDefinition\nimport com.trendyol.stove.kafka.setup.example.StoveListener\nimport io.github.nomisRev.kafka.publisher.PublisherSettings\nimport org.apache.kafka.clients.consumer.ConsumerRecord\n\n// TODO: Convert into in-flight consumer\n@Suppress(\"TooGenericExceptionCaught\", \"TooGenericExceptionThrown\")\nclass ProductFailingConsumer(\n  consumerSettings: Map<String, Any>,\n  producerSettings: PublisherSettings<String, Any>\n) : StoveListener(consumerSettings, producerSettings) {\n  override val topicDefinition: TopicDefinition = TopicDefinition(\n    \"productFailing\",\n    \"productFailing.retry\",\n    \"productFailing.error\"\n  )\n\n  override suspend fun listen(record: ConsumerRecord<String, String>) {\n    record\n      .headers()\n      .firstOrNone { it.key() == \"doNotFail\" }\n      .onSome { return }\n      .onNone { throw Exception(\"exception occurred on purpose\") }\n  }\n}\n\nfun <K, V> ConsumerRecord<K, V>.getRetryCount(): Int =\n  this\n    .headers()\n    .firstOrNone { it.key() == \"retry\" }\n    .map { it.value().toString(Charsets.UTF_8).toInt() }\n    .getOrElse { 0 }\n\nfun <K, V> ConsumerRecord<K, V>.incrementRetryCount(): Int {\n  val currentRetry = this.getRetryCount()\n  this.headers().remove(\"retry\")\n  this.headers().add(\"retry\", (currentRetry + 1).toString().toByteArray(Charsets.UTF_8))\n  return currentRetry + 1\n}\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/tests/CoroutineExecutorServiceTests.kt",
    "content": "package com.trendyol.stove.kafka.tests\n\nimport com.trendyol.stove.kafka.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.*\nimport java.util.concurrent.*\nimport java.util.concurrent.atomic.*\n\nclass CoroutineExecutorServiceTests :\n  FunSpec({\n\n    test(\"execute should run the command\") {\n      val scope = CoroutineScope(Dispatchers.Default + Job())\n      val executorService = scope.asExecutorService\n      val executed = CountDownLatch(1)\n\n      executorService.execute { executed.countDown() }\n\n      executed.await(2, TimeUnit.SECONDS) shouldBe true\n      executorService.shutdown()\n    }\n\n    test(\"shutdown should cancel the coroutine scope\") {\n      val scope = CoroutineScope(Dispatchers.Default + Job())\n      val executorService = scope.asExecutorService\n\n      executorService.isShutdown shouldBe false\n      executorService.shutdown()\n      executorService.isShutdown shouldBe true\n    }\n\n    test(\"shutdownNow should cancel scope and return empty list\") {\n      val scope = CoroutineScope(Dispatchers.Default + Job())\n      val executorService = scope.asExecutorService\n\n      val result = executorService.shutdownNow()\n\n      result shouldBe emptyList()\n      executorService.isShutdown shouldBe true\n    }\n\n    test(\"isTerminated should return true when job is completed\") {\n      val scope = CoroutineScope(Dispatchers.Default + Job())\n      val executorService = scope.asExecutorService\n\n      executorService.isTerminated shouldBe false\n      executorService.shutdown()\n      // After shutdown, the job is cancelled which means completed\n      executorService.isTerminated shouldBe true\n    }\n\n    test(\"awaitTermination should return isTerminated status\") {\n      val scope = CoroutineScope(Dispatchers.Default + Job())\n      val executorService = scope.asExecutorService\n\n      executorService.shutdown()\n      executorService.awaitTermination(1, TimeUnit.SECONDS) shouldBe true\n    }\n\n    test(\"execute should run multiple commands\") {\n      val scope = CoroutineScope(Dispatchers.Default + Job())\n      val executorService = scope.asExecutorService\n      val counter = AtomicInteger(0)\n      val latch = CountDownLatch(3)\n\n      repeat(3) {\n        executorService.execute {\n          counter.incrementAndGet()\n          latch.countDown()\n        }\n      }\n\n      latch.await(2, TimeUnit.SECONDS) shouldBe true\n      counter.get() shouldBe 3\n      executorService.shutdown()\n    }\n\n    test(\"StoveCoroutineExecutor should execute commands\") {\n      val scope = CoroutineScope(Dispatchers.Default + Job())\n      val executor = scope.asExecutor\n      val executed = AtomicBoolean(false)\n      val latch = CountDownLatch(1)\n\n      executor.execute {\n        executed.set(true)\n        latch.countDown()\n      }\n\n      latch.await(2, TimeUnit.SECONDS) shouldBe true\n      executed.get() shouldBe true\n      scope.cancel()\n    }\n  })\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/tests/KafkaOptionsTests.kt",
    "content": "package com.trendyol.stove.kafka.tests\n\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.kafka.intercepting.StoveKafkaBridge\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.booleans.shouldBeFalse\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.shouldNotBeBlank\n\nclass KafkaOptionsTests :\n  FunSpec({\n\n    test(\"KafkaSystemOptions should have sensible defaults\") {\n      val options = object : KafkaSystemOptions(\n        configureExposedConfiguration = { _ -> listOf() }\n      ) {}\n\n      options.useEmbeddedKafka shouldBe false\n      options.topicSuffixes shouldBe TopicSuffixes()\n      options.listenPublishedMessagesFromStove shouldBe false\n      options.serde shouldNotBe null\n      options.valueSerializer shouldNotBe null\n      options.containerOptions shouldNotBe null\n    }\n\n    test(\"KafkaSystemOptions.provided should create ProvidedKafkaSystemOptions with correct config\") {\n      val options = KafkaSystemOptions.provided(\n        bootstrapServers = \"localhost:9092\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\"kafka.bootstrap-servers=${cfg.bootstrapServers}\")\n        }\n      )\n\n      options.providedConfig.bootstrapServers shouldBe \"localhost:9092\"\n      options.providedConfig.interceptorClass shouldBe StoveKafkaBridge::class.java.name\n      options.runMigrationsForProvided shouldBe true\n      options.useEmbeddedKafka.shouldBeFalse()\n    }\n\n    test(\"ProvidedKafkaSystemOptions should expose correct properties\") {\n      val config = KafkaExposedConfiguration(\n        bootstrapServers = \"broker1:9092,broker2:9092\",\n        interceptorClass = \"com.example.Interceptor\"\n      )\n      val options = ProvidedKafkaSystemOptions(\n        config = config,\n        runMigrations = false,\n        configureExposedConfiguration = { cfg ->\n          listOf(\"servers=${cfg.bootstrapServers}\")\n        }\n      )\n\n      options.config shouldBe config\n      options.providedConfig shouldBe config\n      options.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"KafkaExposedConfiguration should hold bootstrap servers and interceptor class\") {\n      val cfg = KafkaExposedConfiguration(\n        bootstrapServers = \"host1:9092\",\n        interceptorClass = \"com.test.MyInterceptor\"\n      )\n\n      cfg.bootstrapServers shouldBe \"host1:9092\"\n      cfg.interceptorClass shouldBe \"com.test.MyInterceptor\"\n    }\n\n    test(\"KafkaContainerOptions should have defaults\") {\n      val opts = KafkaContainerOptions()\n      opts shouldNotBe null\n    }\n\n    test(\"stoveKafkaBridgePortDefault should return a valid port string\") {\n      stoveKafkaBridgePortDefault.shouldNotBeBlank()\n      stoveKafkaBridgePortDefault.toInt() shouldNotBe 0\n    }\n  })\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/tests/KafkaSystemTests.kt",
    "content": "package com.trendyol.stove.kafka.tests\n\nimport arrow.core.some\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.kafka.setup.example.DomainEvents.ProductCreated\nimport com.trendyol.stove.kafka.setup.example.DomainEvents.ProductFailingCreated\nimport com.trendyol.stove.system.stove\nimport io.github.nomisRev.kafka.createTopic\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldNotContainAll\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.*\nimport org.apache.kafka.clients.admin.NewTopic\nimport org.apache.kafka.clients.consumer.ConsumerRecord\nimport kotlin.random.Random\nimport kotlin.time.Duration.Companion.minutes\nimport kotlin.time.Duration.Companion.seconds\n\nclass KafkaSystemTests :\n  FunSpec({\n    val randomString = { Random.nextInt(0, Int.MAX_VALUE).toString() }\n\n    test(\"migration should create test topics\") {\n      stove {\n        kafka {\n          adminOperations {\n            val topics = listTopics().names().get()\n            // Verify migration-created topics exist\n            topics.contains(\"migration-test-topic\") shouldBe true\n            topics.contains(\"migration-test-topic-2\") shouldBe true\n\n            // Verify partition count\n            val topicDescription = describeTopics(listOf(\"migration-test-topic-2\")).allTopicNames().get()\n            topicDescription[\"migration-test-topic-2\"]?.partitions()?.size shouldBe 2\n          }\n        }\n      }\n    }\n\n    test(\"When publish then it should work\") {\n      stove {\n        kafka {\n          val key = randomString()\n          val productId = \"$key[productCreated]\"\n          publish(\"product\", message = ProductCreated(productId), key = key.some())\n          shouldBePublished<ProductCreated> {\n            actual.productId == productId\n          }\n\n          peekPublishedMessages(topic = \"product\") {\n            it.key == key\n          }\n\n          shouldBeConsumed<ProductCreated>(1.minutes) {\n            actual.productId == productId\n          }\n\n          peekConsumedMessages(topic = \"product\") {\n            it.key == key\n          }\n        }\n      }\n    }\n    test(\"lots of messages\") {\n      stove {\n        kafka {\n          val messages = ProductCreated.randoms(100)\n          messages.map { async { publish(\"product\", it, key = randomString().some()) } }.awaitAll()\n          messages.map { async { shouldBePublished<ProductCreated> { actual.productId == it.productId } } }.awaitAll()\n          messages.map { async { shouldBeConsumed<ProductCreated>(1.minutes) { actual.productId == it.productId } } }.awaitAll()\n\n          peekConsumedMessages(topic = \"product\") {\n            it.offset == 100L\n          }\n\n          peekCommittedMessages(topic = \"product\") { record ->\n            record.offset == 101L // next offset\n          }\n        }\n      }\n    }\n\n    test(\"When publish to a failing consumer should end-up throwing exception\") {\n      stove {\n        kafka {\n          val string = randomString()\n          val productId = \"$string[productFailingCreated]\"\n          val key = string.some()\n          publish(\"productFailing\", ProductFailingCreated(productId), key = key)\n          shouldBeRetried<ProductFailingCreated>(atLeastIn = 1.minutes, times = 3) {\n            actual.productId == productId\n          }\n\n          shouldBePublished<ProductFailingCreated>(atLeastIn = 1.minutes) {\n            this.metadata.topic == \"productFailing.error\"\n          }\n\n          peekPublishedMessages(topic = \"productFailing.error\") {\n            it.key == string\n          }\n        }\n      }\n    }\n\n    test(\"in-flight consumer should commit the message after consuming it successfully\") {\n      stove {\n        kafka {\n          val key = randomString()\n          val productId = \"$key[productCreated]\"\n          val topic = randomString()\n\n          adminOperations {\n            createTopic(NewTopic(topic, 1, 1))\n          }\n\n          publish(topic, message = ProductCreated(productId), key = key.some())\n          shouldBePublished<ProductCreated> {\n            actual.productId == productId\n          }\n\n          consumer<String, ProductCreated>(topic, readOnly = false) {\n            println(it) // it should commit\n          }\n\n          shouldBeConsumed<ProductCreated> {\n            actual.productId == productId\n          }\n        }\n      }\n    }\n\n    test(\"in-flight consumer: same consumer group after consuming successfully should not consume the same message again\") {\n      stove {\n        kafka {\n          val key = randomString()\n          val productId = \"$key[productCreated]\"\n          val topic = randomString()\n\n          adminOperations {\n            createTopic(NewTopic(topic, 1, 1))\n          }\n\n          publish(topic, message = ProductCreated(productId), key = key.some())\n          shouldBePublished<ProductCreated> {\n            actual.productId == productId\n          }\n\n          val consumerGroup1 = randomString()\n          consumer<String, ProductCreated>(topic, readOnly = true, autoOffsetReset = \"earliest\", groupId = consumerGroup1) {\n            println(it)\n          }\n\n          delay(3.seconds)\n          val consumedMessages = this.messageStore().consumedMessages().filter { it.topic == topic }\n          val committedMessages = this.messageStore().committedMessages().filter { it.topic == topic }\n          committedMessages.map { it.offset } shouldNotContainAll consumedMessages.map { it.offset }\n\n          // and consumer can re-read\n          val reReadMessages = mutableListOf<ConsumerRecord<*, *>>()\n          consumer<String, ProductCreated>(topic, readOnly = true, autoOffsetReset = \"earliest\", groupId = consumerGroup1) {\n            reReadMessages.add(it)\n          }\n          reReadMessages.size shouldBe consumedMessages.size\n\n          // and consumer can commit\n          consumer<String, ProductCreated>(topic, readOnly = false, autoOffsetReset = \"earliest\", groupId = consumerGroup1) {\n            println(it)\n          }\n\n          val committedMessagesAfterCommit = mutableListOf<ConsumerRecord<*, *>>()\n          consumer<String, ProductCreated>(topic, readOnly = true, autoOffsetReset = \"earliest\", groupId = consumerGroup1) {\n            committedMessagesAfterCommit.add(it)\n          }\n          committedMessagesAfterCommit.size shouldBe 0\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/tests/MessageStoreTests.kt",
    "content": "package com.trendyol.stove.kafka.tests\n\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.kafka.intercepting.MessageStore\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport okio.ByteString.Companion.EMPTY\nimport okio.ByteString.Companion.toByteString\n\nclass MessageStoreTests :\n  FunSpec({\n\n    test(\"returns false when checking offset not yet committed\") {\n      val messageStore = MessageStore()\n\n      val message1 = ConsumedMessage(\n        id = \"1\",\n        message = \"message/1\".toByteArray().toByteString(),\n        topic = \"topic\",\n        partition = 0,\n        offset = 0,\n        key = \"key/1\",\n        headers = emptyMap(),\n        unknownFields = EMPTY\n      )\n\n      val message2 = ConsumedMessage(\n        id = \"2\",\n        message = \"message/2\".toByteArray().toByteString(),\n        topic = \"topic\",\n        partition = 0,\n        offset = 1,\n        key = \"key/2\",\n        headers = emptyMap(),\n        unknownFields = EMPTY\n      )\n      messageStore.record(\n        message1\n      )\n      messageStore.record(\n        message2\n      )\n\n      val committedMessage1 = CommittedMessage(\n        id = \"1\",\n        topic = \"topic\",\n        partition = 0,\n        offset = message1.offset + 1,\n        metadata = \"\",\n        unknownFields = EMPTY\n      )\n      messageStore.record(committedMessage1)\n      messageStore.isCommitted(\n        \"topic\",\n        0,\n        0\n      ) shouldBe true\n      messageStore.isCommitted(\n        \"topic\",\n        1,\n        0\n      ) shouldBe false\n    }\n  })\n"
  },
  {
    "path": "lib/stove-kafka/src/test/kotlin/com/trendyol/stove/kafka/tests/TopicSuffixesTests.kt",
    "content": "package com.trendyol.stove.kafka.tests\n\nimport com.trendyol.stove.kafka.TopicSuffixes\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass TopicSuffixesTests :\n  FunSpec({\n\n    test(\"default error suffixes should match .error and .DLT\") {\n      val suffixes = TopicSuffixes()\n\n      suffixes.isErrorTopic(\"my-topic.error\") shouldBe true\n      suffixes.isErrorTopic(\"my-topic.DLT\") shouldBe true\n      suffixes.isErrorTopic(\"my-topic\") shouldBe false\n      suffixes.isErrorTopic(\"my-topic.retry\") shouldBe false\n    }\n\n    test(\"default retry suffixes should match .retry\") {\n      val suffixes = TopicSuffixes()\n\n      suffixes.isRetryTopic(\"my-topic.retry\") shouldBe true\n      suffixes.isRetryTopic(\"my-topic\") shouldBe false\n      suffixes.isRetryTopic(\"my-topic.error\") shouldBe false\n    }\n\n    test(\"error topic matching should be case-insensitive\") {\n      val suffixes = TopicSuffixes()\n\n      suffixes.isErrorTopic(\"my-topic.ERROR\") shouldBe true\n      suffixes.isErrorTopic(\"my-topic.Error\") shouldBe true\n      suffixes.isErrorTopic(\"my-topic.dlt\") shouldBe true\n    }\n\n    test(\"retry topic matching should be case-insensitive\") {\n      val suffixes = TopicSuffixes()\n\n      suffixes.isRetryTopic(\"my-topic.RETRY\") shouldBe true\n      suffixes.isRetryTopic(\"my-topic.Retry\") shouldBe true\n    }\n\n    test(\"custom suffixes should be used for matching\") {\n      val suffixes = TopicSuffixes(\n        error = listOf(\".dead-letter\", \".failed\"),\n        retry = listOf(\".retry-1\", \".retry-2\")\n      )\n\n      suffixes.isErrorTopic(\"my-topic.dead-letter\") shouldBe true\n      suffixes.isErrorTopic(\"my-topic.failed\") shouldBe true\n      suffixes.isErrorTopic(\"my-topic.error\") shouldBe false\n\n      suffixes.isRetryTopic(\"my-topic.retry-1\") shouldBe true\n      suffixes.isRetryTopic(\"my-topic.retry-2\") shouldBe true\n      suffixes.isRetryTopic(\"my-topic.retry\") shouldBe false\n    }\n\n    test(\"empty suffixes should never match\") {\n      val suffixes = TopicSuffixes(error = emptyList(), retry = emptyList())\n\n      suffixes.isErrorTopic(\"my-topic.error\") shouldBe false\n      suffixes.isRetryTopic(\"my-topic.retry\") shouldBe false\n    }\n  })\n"
  },
  {
    "path": "lib/stove-kafka/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.kafka.setup.StoveConfig\n"
  },
  {
    "path": "lib/stove-kafka/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "lib/stove-mongodb/api/stove-mongodb.api",
    "content": "public final class com/trendyol/stove/mongodb/DatabaseOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lcom/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase;\n\tpublic final fun copy (Lcom/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase;)Lcom/trendyol/stove/mongodb/DatabaseOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mongodb/DatabaseOptions;Lcom/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase;ILjava/lang/Object;)Lcom/trendyol/stove/mongodb/DatabaseOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getDefault ()Lcom/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/mongodb/DatabaseOptions$DefaultDatabase;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getCollection ()Ljava/lang/String;\n\tpublic final fun getName ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mongodb/MongoContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/mongodb/MongoContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mongodb/MongoContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/mongodb/MongoContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract interface annotation class com/trendyol/stove/mongodb/MongoDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/mongodb/MongodbContext {\n\tpublic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/mongodb/MongodbSystemOptions;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/mongodb/MongodbSystemOptions;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic final fun component2 ()Lcom/trendyol/stove/mongodb/MongodbSystemOptions;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/mongodb/MongodbSystemOptions;Ljava/lang/String;)Lcom/trendyol/stove/mongodb/MongodbContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mongodb/MongodbContext;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/mongodb/MongodbSystemOptions;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/mongodb/MongodbContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getKeyName ()Ljava/lang/String;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/mongodb/MongodbSystemOptions;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mongodb/MongodbExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()I\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)Lcom/trendyol/stove/mongodb/MongodbExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mongodb/MongodbExposedConfiguration;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/mongodb/MongodbExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getConnectionString ()Ljava/lang/String;\n\tpublic final fun getHost ()Ljava/lang/String;\n\tpublic final fun getPort ()I\n\tpublic final fun getReplicaSetUrl ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mongodb/MongodbMigrationContext {\n\tpublic fun <init> (Lcom/mongodb/kotlin/client/coroutine/MongoClient;Lcom/trendyol/stove/mongodb/MongodbSystemOptions;)V\n\tpublic final fun component1 ()Lcom/mongodb/kotlin/client/coroutine/MongoClient;\n\tpublic final fun component2 ()Lcom/trendyol/stove/mongodb/MongodbSystemOptions;\n\tpublic final fun copy (Lcom/mongodb/kotlin/client/coroutine/MongoClient;Lcom/trendyol/stove/mongodb/MongodbSystemOptions;)Lcom/trendyol/stove/mongodb/MongodbMigrationContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mongodb/MongodbMigrationContext;Lcom/mongodb/kotlin/client/coroutine/MongoClient;Lcom/trendyol/stove/mongodb/MongodbSystemOptions;ILjava/lang/Object;)Lcom/trendyol/stove/mongodb/MongodbMigrationContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getClient ()Lcom/mongodb/kotlin/client/coroutine/MongoClient;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/mongodb/MongodbSystemOptions;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mongodb/MongodbSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field Companion Lcom/trendyol/stove/mongodb/MongodbSystem$Companion;\n\tpublic static final field RESERVED_ID Ljava/lang/String;\n\tpublic field mongoClient Lcom/mongodb/kotlin/client/coroutine/MongoClient;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun getContext ()Lcom/trendyol/stove/mongodb/MongodbContext;\n\tpublic final fun getMongoClient ()Lcom/mongodb/kotlin/client/coroutine/MongoClient;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic final fun pause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun setMongoClient (Lcom/mongodb/kotlin/client/coroutine/MongoClient;)V\n\tpublic final fun shouldDelete (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun shouldDelete$default (Lcom/trendyol/stove/mongodb/MongodbSystem;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun shouldNotExist (Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun shouldNotExist$default (Lcom/trendyol/stove/mongodb/MongodbSystem;Ljava/lang/String;Ljava/lang/String;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/mongodb/MongodbSystem$Companion {\n\tpublic final fun client (Lcom/trendyol/stove/mongodb/MongodbSystem;)Lcom/mongodb/kotlin/client/coroutine/MongoClient;\n\tpublic final fun filterById (Ljava/lang/String;)Lorg/bson/conversions/Bson;\n}\n\npublic class com/trendyol/stove/mongodb/MongodbSystemOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/mongodb/MongodbSystemOptions$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/mongodb/DatabaseOptions;Lcom/trendyol/stove/mongodb/MongoContainerOptions;Lkotlin/jvm/functions/Function1;Lcom/trendyol/stove/serialization/StoveSerde;Lorg/bson/json/JsonWriterSettings;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/mongodb/DatabaseOptions;Lcom/trendyol/stove/mongodb/MongoContainerOptions;Lkotlin/jvm/functions/Function1;Lcom/trendyol/stove/serialization/StoveSerde;Lorg/bson/json/JsonWriterSettings;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureClient ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainer ()Lcom/trendyol/stove/mongodb/MongoContainerOptions;\n\tpublic fun getDatabaseOptions ()Lcom/trendyol/stove/mongodb/DatabaseOptions;\n\tpublic fun getJsonWriterSettings ()Lorg/bson/json/JsonWriterSettings;\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun getSerde ()Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/mongodb/MongodbSystemOptions;\n}\n\npublic final class com/trendyol/stove/mongodb/MongodbSystemOptions$Companion {\n\tpublic final fun provided (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Lcom/trendyol/stove/mongodb/DatabaseOptions;Lkotlin/jvm/functions/Function1;Lcom/trendyol/stove/serialization/StoveSerde;Lorg/bson/json/JsonWriterSettings;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/mongodb/ProvidedMongodbSystemOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/mongodb/MongodbSystemOptions$Companion;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Lcom/trendyol/stove/mongodb/DatabaseOptions;Lkotlin/jvm/functions/Function1;Lcom/trendyol/stove/serialization/StoveSerde;Lorg/bson/json/JsonWriterSettings;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/mongodb/ProvidedMongodbSystemOptions;\n}\n\npublic final class com/trendyol/stove/mongodb/ObjectIdDeserializer : com/fasterxml/jackson/databind/deser/std/StdDeserializer {\n\tpublic fun <init> ()V\n\tpublic synthetic fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/lang/Object;\n\tpublic fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Lorg/bson/types/ObjectId;\n}\n\npublic final class com/trendyol/stove/mongodb/ObjectIdModule : com/fasterxml/jackson/databind/module/SimpleModule {\n\tpublic fun <init> ()V\n}\n\npublic final class com/trendyol/stove/mongodb/ObjectIdSerializer : com/fasterxml/jackson/databind/ser/std/StdSerializer {\n\tpublic fun <init> ()V\n\tpublic synthetic fun serialize (Ljava/lang/Object;Lcom/fasterxml/jackson/core/JsonGenerator;Lcom/fasterxml/jackson/databind/SerializerProvider;)V\n\tpublic fun serialize (Lorg/bson/types/ObjectId;Lcom/fasterxml/jackson/core/JsonGenerator;Lcom/fasterxml/jackson/databind/SerializerProvider;)V\n}\n\npublic final class com/trendyol/stove/mongodb/OptionsKt {\n\tpublic static final fun mongodb-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun mongodb-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun mongodb-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun mongodb-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/mongodb/ProvidedMongodbSystemOptions : com/trendyol/stove/mongodb/MongodbSystemOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/mongodb/MongodbExposedConfiguration;Lcom/trendyol/stove/mongodb/DatabaseOptions;Lkotlin/jvm/functions/Function1;Lcom/trendyol/stove/serialization/StoveSerde;Lorg/bson/json/JsonWriterSettings;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/mongodb/MongodbExposedConfiguration;Lcom/trendyol/stove/mongodb/DatabaseOptions;Lkotlin/jvm/functions/Function1;Lcom/trendyol/stove/serialization/StoveSerde;Lorg/bson/json/JsonWriterSettings;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/mongodb/MongodbExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/mongodb/MongodbExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic class com/trendyol/stove/mongodb/StoveMongoContainer : org/testcontainers/mongodb/MongoDBContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\npublic final class com/trendyol/stove/mongodb/StoveMongoJsonWriterSettings {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/mongodb/StoveMongoJsonWriterSettings;\n\tpublic final fun getObjectIdAsString ()Lorg/bson/json/JsonWriterSettings;\n}\n\n"
  },
  {
    "path": "lib/stove-mongodb/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(libs.testcontainers.mongodb)\n  implementation(libs.mongodb.kotlin.coroutine)\n  implementation(libs.kotlinx.io.reactor.extensions)\n  implementation(libs.kotlinx.reactive)\n  implementation(libs.kotlinx.jdk8)\n  implementation(libs.kotlinx.core)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.logback.classic)\n}\n\nval testWithProvided = tasks.register<Test>(\"testWithProvided\") {\n  group = \"verification\"\n  description = \"Runs tests with an externally provided MongoDB instance\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useProvided\", \"true\")\n  doFirst {\n    println(\"Starting MongoDB tests with provided instance...\")\n  }\n}\n\ntasks.test.configure {\n  dependsOn(testWithProvided)\n}\n"
  },
  {
    "path": "lib/stove-mongodb/src/main/kotlin/com/trendyol/stove/mongodb/MongoDsl.kt",
    "content": "package com.trendyol.stove.mongodb\n\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class MongoDsl\n"
  },
  {
    "path": "lib/stove-mongodb/src/main/kotlin/com/trendyol/stove/mongodb/MongodbSystem.kt",
    "content": "package com.trendyol.stove.mongodb\n\nimport com.mongodb.*\nimport com.mongodb.client.model.Filters.eq\nimport com.mongodb.kotlin.client.coroutine.MongoClient\nimport com.trendyol.stove.containers.StoveContainerInspectInformation\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport kotlinx.coroutines.flow.*\nimport kotlinx.coroutines.runBlocking\nimport org.bson.*\nimport org.bson.conversions.Bson\nimport org.bson.types.ObjectId\nimport org.slf4j.*\n\n/**\n * MongoDB document database system for testing document storage operations.\n *\n * Provides a DSL for testing MongoDB operations:\n * - Document CRUD operations (save, get, delete)\n * - MongoDB queries with JSON syntax\n * - Collection management\n * - Aggregation pipelines\n *\n * ## Saving Documents\n *\n * ```kotlin\n * mongodb {\n *     // Save to default collection\n *     save(\"user-123\", User(id = \"123\", name = \"John\"))\n *\n *     // Save to specific collection\n *     save(\"user-123\", User(id = \"123\", name = \"John\"), collection = \"users\")\n * }\n * ```\n *\n * ## Retrieving Documents\n *\n * ```kotlin\n * mongodb {\n *     // Get by ObjectId and assert\n *     shouldGet<User>(\"507f1f77bcf86cd799439011\") { user ->\n *         user.name shouldBe \"John\"\n *         user.email shouldBe \"john@example.com\"\n *     }\n *\n *     // Get from specific collection\n *     shouldGet<User>(\"507f1f77bcf86cd799439011\", collection = \"users\") { user ->\n *         user.name shouldBe \"John\"\n *     }\n * }\n * ```\n *\n * ## Querying Documents\n *\n * ```kotlin\n * mongodb {\n *     // Query with MongoDB JSON syntax\n *     shouldQuery<User>(\n *         query = \"\"\"{ \"status\": \"active\", \"age\": { \"${\"$\"}gte\": 18 } }\"\"\",\n *         collection = \"users\"\n *     ) { users ->\n *         users.size shouldBeGreaterThan 0\n *         users.all { it.status == \"active\" } shouldBe true\n *     }\n *\n *     // Complex queries\n *     shouldQuery<Order>(\n *         query = \"\"\"{\n *             \"userId\": \"user-123\",\n *             \"total\": { \"${\"$\"}gt\": 100 },\n *             \"status\": { \"${\"$\"}in\": [\"pending\", \"confirmed\"] }\n *         }\"\"\",\n *         collection = \"orders\"\n *     ) { orders ->\n *         orders shouldHaveSize 2\n *     }\n * }\n * ```\n *\n * ## Deleting Documents\n *\n * ```kotlin\n * mongodb {\n *     shouldDelete(\"507f1f77bcf86cd799439011\")\n *     shouldDelete(\"507f1f77bcf86cd799439011\", collection = \"users\")\n * }\n * ```\n *\n * ## Test Workflow Example\n *\n * ```kotlin\n * test(\"should create user via API and store in MongoDB\") {\n *     stove {\n *         // Create user via API\n *         val userId: String\n *         http {\n *             postAndExpectBody<UserResponse>(\n *                 uri = \"/users\",\n *                 body = CreateUserRequest(name = \"John\").some()\n *             ) { response ->\n *                 response.status shouldBe 201\n *                 userId = response.body().id\n *             }\n *         }\n *\n *         // Verify in MongoDB\n *         mongodb {\n *             shouldGet<User>(userId, collection = \"users\") { user ->\n *                 user.name shouldBe \"John\"\n *                 user.createdAt shouldNotBe null\n *             }\n *         }\n *     }\n * }\n * ```\n *\n * ## Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         mongodb {\n *             MongodbSystemOptions(\n *                 databaseOptions = DatabaseOptions(\n *                     default = DefaultDatabase(\n *                         name = \"my_database\",\n *                         collection = \"default_collection\"\n *                     )\n *                 ),\n *                 configureExposedConfiguration = { cfg ->\n *                     listOf(\n *                         \"spring.data.mongodb.uri=${cfg.connectionString}\"\n *                     )\n *                 }\n *             ).migrations {\n *                 register<CreateIndexesMigration>()\n *             }\n *         }\n *     }\n * ```\n *\n * @property stove The parent test system.\n * @property context MongoDB context containing database options.\n * @see MongodbSystemOptions\n * @see MongodbExposedConfiguration\n */\n@MongoDsl\nclass MongodbSystem internal constructor(\n  override val stove: Stove,\n  val context: MongodbContext\n) : PluggedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  @PublishedApi\n  internal lateinit var mongoClient: MongoClient\n\n  override val reportSystemName: String = \"MongoDB\" + (context.keyName?.let { \" [$it]\" } ?: \"\")\n  private lateinit var exposedConfiguration: MongodbExposedConfiguration\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val state: StateStorage<MongodbExposedConfiguration> =\n    stove.createStateStorage<MongodbExposedConfiguration, MongodbSystem>(context.keyName)\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n    mongoClient = createClient(exposedConfiguration)\n    runMigrationsIfNeeded()\n  }\n\n  override suspend fun stop(): Unit = whenContainer { it.stop() }\n\n  override fun configuration(): List<String> = context.options.configureExposedConfiguration(exposedConfiguration)\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      context.options.cleanup(mongoClient)\n      mongoClient.close()\n      executeWithReuseCheck { stop() }\n    }.recover {\n      logger.warn(\"Closing mongodb got an error: $it\")\n    }\n  }\n\n  suspend inline fun <reified T : Any> shouldQuery(\n    query: String,\n    collection: String = context.options.databaseOptions.default.collection,\n    crossinline assertion: (List<T>) -> Unit\n  ): MongodbSystem {\n    report(\n      action = \"Query '$collection'\",\n      input = arrow.core.Some(mapOf(\"collection\" to collection, \"filter\" to query))\n    ) {\n      val results = mongoClient\n        .getDatabase(context.options.databaseOptions.default.name)\n        .getCollection<Document>(collection)\n        .find(BsonDocument.parse(query))\n        .map { context.options.serde.deserialize(it.toJson(context.options.jsonWriterSettings), T::class.java) }\n        .toList()\n      assertion(results)\n      results\n    }\n    return this\n  }\n\n  suspend inline fun <reified T : Any> shouldGet(\n    objectId: String,\n    collection: String = context.options.databaseOptions.default.collection,\n    crossinline assertion: (T) -> Unit\n  ): MongodbSystem {\n    report(\n      action = \"Get document\",\n      input = arrow.core.Some(mapOf(\"collection\" to collection, \"_id\" to objectId))\n    ) {\n      val document = mongoClient\n        .getDatabase(context.options.databaseOptions.default.name)\n        .getCollection<Document>(collection)\n        .find(filterById(objectId))\n        .map { context.options.serde.deserialize(it.toJson(context.options.jsonWriterSettings), T::class.java) }\n        .first()\n      assertion(document)\n      document\n    }\n    return this\n  }\n\n  /**\n   * Saves the [instance] with given [objectId] to the [collection]\n   */\n  suspend inline fun <reified T : Any> save(\n    instance: T,\n    objectId: String = ObjectId().toHexString(),\n    collection: String = context.options.databaseOptions.default.collection\n  ): MongodbSystem {\n    report(\n      action = \"Insert document\",\n      input = arrow.core.Some(instance),\n      metadata = mapOf(\"collection\" to collection, \"_id\" to objectId)\n    ) {\n      mongoClient\n        .getDatabase(context.options.databaseOptions.default.name)\n        .getCollection<Document>(collection)\n        .also { coll ->\n          context.options.serde\n            .serialize(instance)\n            .let { BsonDocument.parse(it) }\n            .let { doc -> Document(doc) }\n            .append(RESERVED_ID, ObjectId(objectId))\n            .let { coll.insertOne(it) }\n        }\n    }\n    return this\n  }\n\n  suspend fun shouldNotExist(\n    objectId: String,\n    collection: String = context.options.databaseOptions.default.collection\n  ): MongodbSystem {\n    report(\n      action = \"Document should not exist\",\n      input = arrow.core.Some(mapOf(\"collection\" to collection, \"_id\" to objectId)),\n      expected = arrow.core.Some(\"Document not found\")\n    ) {\n      val exists = mongoClient\n        .getDatabase(context.options.databaseOptions.default.name)\n        .getCollection<Document>(collection)\n        .find(filterById(objectId))\n        .firstOrNull() != null\n      if (exists) throw AssertionError(\"The document with the given id($objectId) was not expected, but found!\")\n    }\n    return this\n  }\n\n  suspend fun shouldDelete(\n    objectId: String,\n    collection: String = context.options.databaseOptions.default.collection\n  ): MongodbSystem {\n    report(\n      action = \"Delete document\",\n      metadata = mapOf(\"collection\" to collection, \"_id\" to objectId)\n    ) {\n      mongoClient\n        .getDatabase(context.options.databaseOptions.default.name)\n        .getCollection<Document>(collection)\n        .deleteOne(filterById(objectId))\n    }\n    return this\n  }\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return MongodbSystem\n   */\n  suspend fun pause(): MongodbSystem {\n    report(\n      action = \"Pause container\",\n      metadata = mapOf(\"operation\" to \"fault-injection\")\n    ) {\n      withContainerOrWarn(\"pause\") { it.pause() }\n    }\n    return this\n  }\n\n  /**\n   * Unpauses the container. Use with care, as it will unpause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return MongodbSystem\n   */\n  suspend fun unpause(): MongodbSystem {\n    report(action = \"Unpause container\") {\n      withContainerOrWarn(\"unpause\") { it.unpause() }\n    }\n    return this\n  }\n\n  /**\n   * Inspects the container. This operation is not supported when using a provided instance.\n   */\n  fun inspect(): StoveContainerInspectInformation? = when (val runtime = context.runtime) {\n    is StoveMongoContainer -> {\n      runtime.inspect()\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"inspect() is not supported when using a provided instance\")\n      null\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private suspend fun obtainExposedConfiguration(): MongodbExposedConfiguration =\n    when {\n      context.options is ProvidedMongodbSystemOptions -> context.options.config\n      context.runtime is StoveMongoContainer -> startMongoContainer(context.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n    }\n\n  private suspend fun startMongoContainer(container: StoveMongoContainer): MongodbExposedConfiguration =\n    state.capture {\n      container.start()\n      MongodbExposedConfiguration(\n        connectionString = container.connectionString,\n        host = container.host,\n        port = container.firstMappedPort,\n        replicaSetUrl = container.replicaSetUrl\n      )\n    }\n\n  private fun createClient(config: MongodbExposedConfiguration): MongoClient =\n    MongoClientSettings\n      .builder()\n      .applyConnectionString(ConnectionString(config.connectionString))\n      .retryWrites(true)\n      .readConcern(ReadConcern.MAJORITY)\n      .writeConcern(WriteConcern.MAJORITY)\n      .apply(context.options.configureClient)\n      .build()\n      .let { MongoClient.create(it) }\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StoveMongoContainer) -> Unit\n  ): MongodbSystem = when (val runtime = context.runtime) {\n    is StoveMongoContainer -> {\n      action(runtime)\n      this\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"$operation() is not supported when using a provided instance\")\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private inline fun whenContainer(action: (StoveMongoContainer) -> Unit) {\n    if (context.runtime is StoveMongoContainer) {\n      action(context.runtime)\n    }\n  }\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (shouldRunMigrations()) {\n      context.options.migrationCollection.run(MongodbMigrationContext(mongoClient, context.options))\n    }\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    context.options is ProvidedMongodbSystemOptions -> context.options.runMigrations\n    context.runtime is StoveMongoContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n  }\n\n  companion object {\n    const val RESERVED_ID = \"_id\"\n\n    @PublishedApi\n    internal fun filterById(key: String): Bson = eq(RESERVED_ID, ObjectId(key))\n\n    /**\n     * Exposes the [MongoClient] to the [MongodbSystem].\n     * Use this for advanced MongoDB operations not covered by the DSL.\n     */\n    @Suppress(\"unused\")\n    fun MongodbSystem.client(): MongoClient = mongoClient\n  }\n}\n"
  },
  {
    "path": "lib/stove-mongodb/src/main/kotlin/com/trendyol/stove/mongodb/MongodbSystemOptions.kt",
    "content": "package com.trendyol.stove.mongodb\n\nimport com.fasterxml.jackson.databind.*\nimport com.fasterxml.jackson.module.kotlin.KotlinModule\nimport com.mongodb.MongoClientSettings\nimport com.mongodb.kotlin.client.coroutine.MongoClient\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.bson.json.JsonWriterSettings\n\n/**\n * Context provided to MongoDB migrations.\n * Contains the MongoDB client and options for performing setup operations.\n *\n * @property client The MongoDB client for executing operations\n * @property options The MongoDB system options\n */\n@StoveDsl\ndata class MongodbMigrationContext(\n  val client: MongoClient,\n  val options: MongodbSystemOptions\n)\n\n/**\n * Convenience type alias for MongoDB migrations.\n *\n * Instead of writing `DatabaseMigration<MongodbMigrationContext>`, use `MongodbMigration`:\n * ```kotlin\n * class MyMigration : MongodbMigration {\n *   override val order: Int = 1\n *   override suspend fun execute(connection: MongodbMigrationContext) { ... }\n * }\n * ```\n */\ntypealias MongodbMigration = DatabaseMigration<MongodbMigrationContext>\n\n/**\n * Options for configuring the MongoDB system in container mode.\n */\n@StoveDsl\nopen class MongodbSystemOptions(\n  open val databaseOptions: DatabaseOptions = DatabaseOptions(),\n  open val container: MongoContainerOptions = MongoContainerOptions(),\n  open val configureClient: (MongoClientSettings.Builder) -> Unit = { },\n  open val serde: StoveSerde<Any, String> = StoveSerde.jackson.anyJsonStringSerde(\n    StoveSerde.jackson.byConfiguring {\n      disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n      enable(MapperFeature.DEFAULT_VIEW_INCLUSION)\n      addModule(ObjectIdModule())\n      addModule(KotlinModule.Builder().build())\n    }\n  ),\n  open val jsonWriterSettings: JsonWriterSettings = StoveMongoJsonWriterSettings.objectIdAsString,\n  open val cleanup: suspend (MongoClient) -> Unit = {},\n  override val configureExposedConfiguration: (MongodbExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<MongodbExposedConfiguration>,\n  SupportsMigrations<MongodbMigrationContext, MongodbSystemOptions> {\n  override val migrationCollection: MigrationCollection<MongodbMigrationContext> = MigrationCollection()\n\n  companion object {\n    /**\n     * Creates options configured to use an externally provided MongoDB instance\n     * instead of a testcontainer.\n     *\n     * @param connectionString The MongoDB connection string\n     * @param host The MongoDB host\n     * @param port The MongoDB port\n     * @param replicaSetUrl The MongoDB replica set URL (defaults to connectionString)\n     * @param databaseOptions Database options configuration\n     * @param configureClient Client configuration function\n     * @param serde Serialization/deserialization configuration\n     * @param jsonWriterSettings JSON writer settings\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      connectionString: String,\n      host: String,\n      port: Int,\n      replicaSetUrl: String = connectionString,\n      databaseOptions: DatabaseOptions = DatabaseOptions(),\n      configureClient: (MongoClientSettings.Builder) -> Unit = { },\n      serde: StoveSerde<Any, String> = StoveSerde.jackson.anyJsonStringSerde(\n        StoveSerde.jackson.byConfiguring {\n          disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n          enable(MapperFeature.DEFAULT_VIEW_INCLUSION)\n          addModule(ObjectIdModule())\n          addModule(KotlinModule.Builder().build())\n        }\n      ),\n      jsonWriterSettings: JsonWriterSettings = StoveMongoJsonWriterSettings.objectIdAsString,\n      runMigrations: Boolean = true,\n      cleanup: suspend (MongoClient) -> Unit = {},\n      configureExposedConfiguration: (MongodbExposedConfiguration) -> List<String>\n    ): ProvidedMongodbSystemOptions = ProvidedMongodbSystemOptions(\n      config = MongodbExposedConfiguration(\n        connectionString = connectionString,\n        host = host,\n        port = port,\n        replicaSetUrl = replicaSetUrl\n      ),\n      databaseOptions = databaseOptions,\n      configureClient = configureClient,\n      serde = serde,\n      jsonWriterSettings = jsonWriterSettings,\n      runMigrations = runMigrations,\n      cleanup = cleanup,\n      configureExposedConfiguration = configureExposedConfiguration\n    )\n  }\n}\n\n/**\n * Options for using an externally provided MongoDB instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedMongodbSystemOptions(\n  /**\n   * The configuration for the provided MongoDB instance.\n   */\n  val config: MongodbExposedConfiguration,\n  databaseOptions: DatabaseOptions = DatabaseOptions(),\n  configureClient: (MongoClientSettings.Builder) -> Unit = { },\n  serde: StoveSerde<Any, String> = StoveSerde.jackson.anyJsonStringSerde(\n    StoveSerde.jackson.byConfiguring {\n      disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n      enable(MapperFeature.DEFAULT_VIEW_INCLUSION)\n      addModule(ObjectIdModule())\n      addModule(KotlinModule.Builder().build())\n    }\n  ),\n  jsonWriterSettings: JsonWriterSettings = StoveMongoJsonWriterSettings.objectIdAsString,\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  cleanup: suspend (MongoClient) -> Unit = {},\n  configureExposedConfiguration: (MongodbExposedConfiguration) -> List<String>\n) : MongodbSystemOptions(\n  databaseOptions = databaseOptions,\n  container = MongoContainerOptions(),\n  configureClient = configureClient,\n  serde = serde,\n  jsonWriterSettings = jsonWriterSettings,\n  cleanup = cleanup,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<MongodbExposedConfiguration> {\n  override val providedConfig: MongodbExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n\nobject StoveMongoJsonWriterSettings {\n  val objectIdAsString: JsonWriterSettings = JsonWriterSettings\n    .builder()\n    .objectIdConverter { value, writer -> writer.writeString(value.toHexString()) }\n    .build()\n}\n"
  },
  {
    "path": "lib/stove-mongodb/src/main/kotlin/com/trendyol/stove/mongodb/ObjectIdJsonOperations.kt",
    "content": "package com.trendyol.stove.mongodb\n\nimport com.fasterxml.jackson.core.JsonGenerator\nimport com.fasterxml.jackson.core.JsonParser\nimport com.fasterxml.jackson.databind.DeserializationContext\nimport com.fasterxml.jackson.databind.JsonNode\nimport com.fasterxml.jackson.databind.SerializerProvider\nimport com.fasterxml.jackson.databind.deser.std.StdDeserializer\nimport com.fasterxml.jackson.databind.module.SimpleModule\nimport com.fasterxml.jackson.databind.node.TextNode\nimport com.fasterxml.jackson.databind.ser.std.StdSerializer\nimport org.bson.types.ObjectId\n\nclass ObjectIdSerializer : StdSerializer<ObjectId>(ObjectId::class.java) {\n  override fun serialize(\n    value: ObjectId,\n    gen: JsonGenerator,\n    provider: SerializerProvider\n  ): Unit = gen.writeString(value.toHexString())\n}\n\nclass ObjectIdDeserializer : StdDeserializer<ObjectId>(ObjectId::class.java) {\n  override fun deserialize(\n    parser: JsonParser,\n    context: DeserializationContext\n  ): ObjectId =\n    when (val node = context.parser.codec.readValue(parser, JsonNode::class.java)) {\n      is TextNode -> node.textValue().removeSurrounding(\"\\\"\")\n\n      is JsonNode -> node[\"\\$oid\"].textValue().removeSurrounding(\"\\\"\")\n\n      else -> throw IllegalArgumentException(\n        \"ObjectId (\\$oid) could not be deserialized, this is because JsonNode is not properly recognized.\"\n      )\n    }.let { ObjectId(it) }\n}\n\nclass ObjectIdModule : SimpleModule() {\n  init {\n    addSerializer(ObjectId::class.java, ObjectIdSerializer())\n    addDeserializer(ObjectId::class.java, ObjectIdDeserializer())\n  }\n}\n"
  },
  {
    "path": "lib/stove-mongodb/src/main/kotlin/com/trendyol/stove/mongodb/Options.kt",
    "content": "package com.trendyol.stove.mongodb\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.containers.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.testcontainers.mongodb.MongoDBContainer\nimport org.testcontainers.utility.DockerImageName\n\n@StoveDsl\ndata class MongodbExposedConfiguration(\n  val connectionString: String,\n  val host: String,\n  val port: Int,\n  val replicaSetUrl: String\n) : ExposedConfiguration\n\n@StoveDsl\ndata class MongodbContext(\n  val runtime: SystemRuntime,\n  val options: MongodbSystemOptions,\n  val keyName: String? = null\n)\n\nopen class StoveMongoContainer(\n  override val imageNameAccess: DockerImageName\n) : MongoDBContainer(imageNameAccess),\n  StoveContainer\n\n@StoveDsl\ndata class MongoContainerOptions(\n  override val registry: String = DEFAULT_REGISTRY,\n  override val image: String = \"mongo\",\n  override val tag: String = \"latest\",\n  override val compatibleSubstitute: String? = null,\n  override val useContainerFn: UseContainerFn<StoveMongoContainer> = { StoveMongoContainer(it) },\n  override val containerFn: ContainerFn<StoveMongoContainer> = { }\n) : ContainerOptions<StoveMongoContainer>\n\n@StoveDsl\ndata class DatabaseOptions(\n  val default: DefaultDatabase = DefaultDatabase()\n) {\n  data class DefaultDatabase(\n    val name: String = \"stove\",\n    val collection: String = \"stoveCollection\"\n  )\n}\n\ninternal fun Stove.withMongodb(\n  options: MongodbSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(MongodbSystem(this, MongodbContext(runtime, options)))\n  return this\n}\n\ninternal fun Stove.withMongodb(\n  key: SystemKey,\n  options: MongodbSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(key, MongodbSystem(this, MongodbContext(runtime, options, keyName = keyDisplayName(key))))\n  return this\n}\n\ninternal fun Stove.mongodb(): MongodbSystem =\n  getOrNone<MongodbSystem>().getOrElse {\n    throw SystemNotRegisteredException(MongodbSystem::class)\n  }\n\ninternal fun Stove.mongodb(key: SystemKey): MongodbSystem =\n  getOrNone<MongodbSystem>(key).getOrElse {\n    throw SystemNotRegisteredException(MongodbSystem::class, \"No MongodbSystem registered with key '${keyDisplayName(key)}'\")\n  }\n\n/**\n * Configures MongoDB system.\n *\n * For container-based setup:\n * ```kotlin\n * mongodb {\n *   MongodbSystemOptions(\n *     cleanup = { client -> client.getDatabase(\"mydb\").drop() },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * mongodb {\n *   MongodbSystemOptions.provided(\n *     connectionString = \"mongodb://localhost:27017\",\n *     host = \"localhost\",\n *     port = 27017,\n *     cleanup = { client -> client.getDatabase(\"mydb\").drop() },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n */\nfun WithDsl.mongodb(\n  configure: () -> MongodbSystemOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedMongodbSystemOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveMongoContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withMongodb(options, runtime)\n}\n\nfun WithDsl.mongodb(\n  key: SystemKey,\n  configure: () -> MongodbSystemOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedMongodbSystemOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveMongoContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withMongodb(key, options, runtime)\n}\n\nsuspend fun ValidationDsl.mongodb(\n  validation: @MongoDsl suspend MongodbSystem.() -> Unit\n): Unit = validation(this.stove.mongodb())\n\nsuspend fun ValidationDsl.mongodb(\n  key: SystemKey,\n  validation: @MongoDsl suspend MongodbSystem.() -> Unit\n): Unit = validation(this.stove.mongodb(key))\n"
  },
  {
    "path": "lib/stove-mongodb/src/test/kotlin/com/trendyol/stove/mongodb/MongodbOptionsTests.kt",
    "content": "package com.trendyol.stove.mongodb\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\n\nclass MongodbOptionsTests :\n  FunSpec({\n\n    test(\"MongodbExposedConfiguration should hold connection details\") {\n      val cfg = MongodbExposedConfiguration(\n        connectionString = \"mongodb://localhost:27017\",\n        host = \"localhost\",\n        port = 27017,\n        replicaSetUrl = \"mongodb://localhost:27017/replicaSet\"\n      )\n\n      cfg.connectionString shouldBe \"mongodb://localhost:27017\"\n      cfg.host shouldBe \"localhost\"\n      cfg.port shouldBe 27017\n      cfg.replicaSetUrl shouldBe \"mongodb://localhost:27017/replicaSet\"\n    }\n\n    test(\"MongodbSystemOptions.provided should create ProvidedMongodbSystemOptions\") {\n      val options = MongodbSystemOptions.provided(\n        connectionString = \"mongodb://localhost:27017\",\n        host = \"localhost\",\n        port = 27017,\n        configureExposedConfiguration = { cfg ->\n          listOf(\"mongo.uri=${cfg.connectionString}\")\n        }\n      )\n\n      options.providedConfig.connectionString shouldBe \"mongodb://localhost:27017\"\n      options.providedConfig.host shouldBe \"localhost\"\n      options.providedConfig.port shouldBe 27017\n      options.providedConfig.replicaSetUrl shouldBe \"mongodb://localhost:27017\"\n      options.runMigrationsForProvided shouldBe true\n    }\n\n    test(\"ProvidedMongodbSystemOptions should expose correct properties\") {\n      val config = MongodbExposedConfiguration(\n        connectionString = \"mongodb://remote:27017\",\n        host = \"remote\",\n        port = 27017,\n        replicaSetUrl = \"mongodb://remote:27017\"\n      )\n      val options = ProvidedMongodbSystemOptions(\n        config = config,\n        runMigrations = false,\n        configureExposedConfiguration = { _ -> listOf(\"test=true\") }\n      )\n\n      options.config shouldBe config\n      options.providedConfig shouldBe config\n      options.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"MongodbSystemOptions should have sensible defaults\") {\n      val options = object : MongodbSystemOptions(\n        configureExposedConfiguration = { _ -> listOf() }\n      ) {}\n\n      options.databaseOptions shouldNotBe null\n      options.container shouldNotBe null\n      options.serde shouldNotBe null\n      options.jsonWriterSettings shouldNotBe null\n    }\n\n    test(\"StoveMongoJsonWriterSettings.objectIdAsString should be configured\") {\n      StoveMongoJsonWriterSettings.objectIdAsString shouldNotBe null\n    }\n  })\n"
  },
  {
    "path": "lib/stove-mongodb/src/test/kotlin/com/trendyol/stove/mongodb/MongodbTestSystemTests.kt",
    "content": "package com.trendyol.stove.mongodb\n\nimport com.fasterxml.jackson.annotation.JsonAlias\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.inspectors.forAny\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.delay\nimport org.bson.codecs.pojo.annotations.*\nimport org.bson.types.ObjectId\nimport org.junit.jupiter.api.assertThrows\nimport org.slf4j.*\nimport org.testcontainers.mongodb.MongoDBContainer\nimport org.testcontainers.utility.DockerImageName\n\n// ============================================================================\n// Shared components\n// ============================================================================\n\nclass NoOpApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) = Unit\n\n  override suspend fun stop() = Unit\n}\n\n// ============================================================================\n// Strategy interface\n// ============================================================================\n\nsealed interface MongodbTestStrategy {\n  val logger: Logger\n\n  suspend fun start()\n\n  suspend fun stop()\n\n  companion object {\n    fun select(): MongodbTestStrategy {\n      val useProvided = System.getenv(\"USE_PROVIDED\")?.toBoolean()\n        ?: System.getProperty(\"useProvided\")?.toBoolean()\n        ?: false\n\n      return if (useProvided) ProvidedMongodbStrategy() else ContainerMongodbStrategy()\n    }\n  }\n}\n\n// ============================================================================\n// Container-based strategy (default)\n// ============================================================================\n\nclass ContainerMongodbStrategy : MongodbTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun start() {\n    logger.info(\"Starting MongoDB tests with container mode\")\n\n    val options = MongodbSystemOptions {\n      listOf()\n    }\n\n    Stove()\n      .with {\n        mongodb { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n\n    // Test pause/unpause functionality\n    stove {\n      mongodb {\n        logger.info(\"pausing...\")\n        pause()\n\n        delay(1000)\n\n        logger.info(\"unpausing...\")\n        unpause()\n\n        delay(1000)\n\n        logger.info(\"operating normally...\")\n        logger.info(inspect().toString())\n      }\n    }\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    logger.info(\"MongoDB container tests completed\")\n  }\n}\n\n// ============================================================================\n// Provided instance strategy\n// ============================================================================\n\nclass ProvidedMongodbStrategy : MongodbTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  private lateinit var externalContainer: MongoDBContainer\n\n  override suspend fun start() {\n    logger.info(\"Starting MongoDB tests with provided mode\")\n\n    // Start an external container to simulate a provided instance\n    externalContainer = MongoDBContainer(DockerImageName.parse(\"mongo:7.0\"))\n      .apply { start() }\n\n    logger.info(\"External MongoDB container started at ${externalContainer.connectionString}\")\n\n    val options = MongodbSystemOptions\n      .provided(\n        connectionString = externalContainer.connectionString,\n        host = externalContainer.host,\n        port = externalContainer.firstMappedPort,\n        runMigrations = true,\n        cleanup = { _ ->\n          logger.info(\"Running cleanup on provided instance\")\n          // Clean up test data if needed\n        },\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n    Stove()\n      .with {\n        mongodb { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    externalContainer.stop()\n    logger.info(\"MongoDB provided tests completed\")\n  }\n}\n\n// ============================================================================\n// Kotest project config - selects strategy based on environment\n// ============================================================================\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val strategy = MongodbTestStrategy.select()\n\n  override suspend fun beforeProject() = strategy.start()\n\n  override suspend fun afterProject() = strategy.stop()\n}\n\n// ============================================================================\n// Tests\n// ============================================================================\n\nclass MongodbTestSystemTests :\n  FunSpec({\n    data class ExampleInstanceWithObjectId(\n      @param:BsonId\n      @param:JsonAlias(\"_id\")\n      val id: ObjectId,\n      @param:BsonProperty(\"aggregateId\") val aggregateId: String,\n      @param:BsonProperty(\"description\") val description: String\n    )\n\n    data class ExampleInstanceWithStringObjectId(\n      @param:JsonAlias(\"_id\")\n      val id: String,\n      @param:BsonProperty(\"aggregateId\") val aggregateId: String,\n      @param:BsonProperty(\"description\") val description: String\n    )\n\n    test(\"should save and get with objectId\") {\n      val id = ObjectId()\n      stove {\n        mongodb {\n          save(\n            ExampleInstanceWithObjectId(\n              id = id,\n              aggregateId = id.toHexString(),\n              description = testCase.name.name\n            ),\n            id.toHexString()\n          )\n          shouldGet<ExampleInstanceWithObjectId>(id.toHexString()) { actual ->\n            actual.aggregateId shouldBe id.toHexString()\n            actual.description shouldBe testCase.name.name\n          }\n        }\n      }\n    }\n\n    test(\"should save and get with string objectId\") {\n      val id = ObjectId()\n      stove {\n        mongodb {\n          save(\n            ExampleInstanceWithStringObjectId(\n              id = id.toHexString(),\n              aggregateId = id.toHexString(),\n              description = testCase.name.name\n            ),\n            id.toHexString()\n          )\n          shouldGet<ExampleInstanceWithStringObjectId>(id.toHexString()) { actual ->\n            actual.aggregateId shouldBe id.toHexString()\n            actual.description shouldBe testCase.name.name\n          }\n        }\n      }\n    }\n\n    data class ExampleInstanceWithObjectIdForQuery(\n      val id: String,\n      val description: String\n    )\n    test(\"Get with query should work\") {\n      val id1 = ObjectId()\n      val id2 = ObjectId()\n      val id3 = ObjectId()\n      val firstDesc = \"same description\"\n      val secondDesc = \"different description\"\n      stove {\n        mongodb {\n          save(\n            ExampleInstanceWithObjectId(\n              id = id1,\n              aggregateId = id1.toHexString(),\n              description = firstDesc\n            ),\n            id1.toHexString()\n          )\n          save(\n            ExampleInstanceWithObjectId(\n              id = id2,\n              aggregateId = id2.toHexString(),\n              description = secondDesc\n            ),\n            id2.toHexString()\n          )\n          save(\n            ExampleInstanceWithObjectId(\n              id = id3,\n              aggregateId = id3.toHexString(),\n              description = secondDesc\n            ),\n            id3.toHexString()\n          )\n          shouldQuery<ExampleInstanceWithObjectIdForQuery>(\"{\\\"description\\\": \\\"$secondDesc\\\"}\") { actual ->\n            actual.count() shouldBe 2\n            actual.forAny { it.id shouldBe id2.toHexString() }\n            actual.forAny { it.id shouldBe id3.toHexString() }\n          }\n          shouldQuery<ExampleInstanceWithObjectId>(\"{\\\"description\\\": \\\"$firstDesc\\\"}\") { actual ->\n            actual.count() shouldBe 1\n            actual.first().id shouldBe id1\n          }\n        }\n      }\n    }\n\n    test(\"should throw assertion error when document does exist\") {\n      val id1 = ObjectId()\n      stove {\n        mongodb {\n          save(\n            ExampleInstanceWithObjectId(\n              id = id1,\n              aggregateId = id1.toHexString(),\n              description = testCase.name.name + \"1\"\n            ),\n            id1.toHexString()\n          )\n          shouldGet<ExampleInstanceWithStringObjectId>(id1.toHexString()) { actual ->\n            actual.aggregateId shouldBe id1.toHexString()\n          }\n          assertThrows<AssertionError> { shouldNotExist(id1.toHexString()) }\n        }\n      }\n    }\n\n    test(\"should not throw exception when given does not exist id\") {\n      val notExistDocId = ObjectId()\n      stove {\n        mongodb {\n          shouldNotExist(notExistDocId.toHexString())\n        }\n      }\n    }\n\n    test(\"should delete\") {\n      val id = ObjectId()\n      stove {\n        mongodb {\n          save(\n            ExampleInstanceWithObjectId(\n              id = id,\n              aggregateId = id.toHexString(),\n              description = testCase.name.name\n            ),\n            id.toHexString()\n          )\n          shouldQuery<ExampleInstanceWithObjectId>(\"{\\\"aggregateId\\\": \\\"${id.toHexString()}\\\"}\") { actual ->\n            actual.size shouldBe 1\n          }\n          shouldDelete(id.toHexString())\n          shouldQuery<ExampleInstanceWithObjectId>(\"{\\\"aggregateId\\\": \\\"${id.toHexString()}\\\"}\") { actual ->\n            actual.size shouldBe 0\n          }\n        }\n      }\n    }\n\n    test(\"complex type\") {\n      data class Nested(\n        val id: String,\n        val name: String\n      )\n\n      data class ComplexType(\n        val id: String,\n        val name: String,\n        val nested: Nested\n      )\n\n      val id = ObjectId()\n      val nestedId = ObjectId()\n      stove {\n        mongodb {\n          save(\n            ComplexType(\n              id = id.toHexString(),\n              name = \"name\",\n              nested = Nested(\n                id = nestedId.toHexString(),\n                name = \"nested\"\n              )\n            ),\n            id.toHexString()\n          )\n          shouldGet<ComplexType>(id.toHexString()) { actual ->\n            actual.id shouldBe id.toHexString()\n            actual.name shouldBe \"name\"\n            actual.nested.id shouldBe actual.nested.id\n            actual.nested.name shouldBe \"nested\"\n          }\n\n          shouldQuery<ComplexType>(\n            query = \"{\\\"nested.id\\\": \\\"${nestedId.toHexString()}\\\"}\"\n          ) { actual ->\n            actual.size shouldBe 1\n            actual.first().id shouldBe id.toHexString()\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-mongodb/src/test/kotlin/com/trendyol/stove/mongodb/ObjectIdJsonOperationsTest.kt",
    "content": "package com.trendyol.stove.mongodb\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.kotlin.KotlinModule\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport org.bson.types.ObjectId\n\nclass ObjectIdJsonOperationsTest :\n  FunSpec({\n\n    val mapper = ObjectMapper()\n      .registerModule(KotlinModule.Builder().build())\n      .registerModule(ObjectIdModule())\n\n    test(\"ObjectIdSerializer should serialize ObjectId to hex string\") {\n      val objectId = ObjectId(\"507f1f77bcf86cd799439011\")\n      val container = ObjectIdContainer(objectId)\n\n      val json = mapper.writeValueAsString(container)\n\n      json shouldBe \"\"\"{\"id\":\"507f1f77bcf86cd799439011\"}\"\"\"\n    }\n\n    test(\"ObjectIdDeserializer should deserialize hex string to ObjectId\") {\n      val json = \"\"\"{\"id\":\"507f1f77bcf86cd799439011\"}\"\"\"\n\n      val container = mapper.readValue<ObjectIdContainer>(json)\n\n      container.id shouldBe ObjectId(\"507f1f77bcf86cd799439011\")\n    }\n\n    test(\"ObjectIdDeserializer should deserialize MongoDB extended JSON format\") {\n      val json = \"{\\\"id\\\":{\\\"\\$oid\\\":\\\"507f1f77bcf86cd799439011\\\"}}\"\n\n      val container = mapper.readValue<ObjectIdContainer>(json)\n\n      container.id shouldBe ObjectId(\"507f1f77bcf86cd799439011\")\n    }\n\n    test(\"round-trip serialization should preserve ObjectId\") {\n      val original = ObjectIdContainer(ObjectId(\"507f1f77bcf86cd799439011\"))\n\n      val json = mapper.writeValueAsString(original)\n      val deserialized = mapper.readValue<ObjectIdContainer>(json)\n\n      deserialized.id shouldBe original.id\n    }\n\n    test(\"ObjectIdModule should register both serializer and deserializer\") {\n      val freshMapper = ObjectMapper()\n        .registerModule(KotlinModule.Builder().build())\n        .registerModule(ObjectIdModule())\n\n      val objectId = ObjectId()\n      val json = freshMapper.writeValueAsString(ObjectIdContainer(objectId))\n      val result = freshMapper.readValue<ObjectIdContainer>(json)\n\n      result.id shouldBe objectId\n    }\n\n    test(\"should handle multiple ObjectId fields\") {\n      val id1 = ObjectId(\"507f1f77bcf86cd799439011\")\n      val id2 = ObjectId(\"507f191e810c19729de860ea\")\n      val container = MultiObjectIdContainer(id1, id2)\n\n      val json = mapper.writeValueAsString(container)\n      val result = mapper.readValue<MultiObjectIdContainer>(json)\n\n      result.first shouldBe id1\n      result.second shouldBe id2\n    }\n  })\n\ndata class ObjectIdContainer(\n  val id: ObjectId\n)\n\ndata class MultiObjectIdContainer(\n  val first: ObjectId,\n  val second: ObjectId\n)\n"
  },
  {
    "path": "lib/stove-mongodb/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.mongodb.StoveConfig\n"
  },
  {
    "path": "lib/stove-mongodb/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "lib/stove-mssql/api/stove-mssql.api",
    "content": "public final class com/trendyol/stove/mssql/MsSqlContext {\n\tpublic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/mssql/MsSqlOptions;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/mssql/MsSqlOptions;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic final fun component2 ()Lcom/trendyol/stove/mssql/MsSqlOptions;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/mssql/MsSqlOptions;Ljava/lang/String;)Lcom/trendyol/stove/mssql/MsSqlContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mssql/MsSqlContext;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/mssql/MsSqlOptions;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/mssql/MsSqlContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getKeyName ()Ljava/lang/String;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/mssql/MsSqlOptions;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic class com/trendyol/stove/mssql/MsSqlOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/mssql/MsSqlOptions$Companion;\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/mssql/MssqlContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/mssql/MssqlContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getApplicationName ()Ljava/lang/String;\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainer ()Lcom/trendyol/stove/mssql/MssqlContainerOptions;\n\tpublic fun getDatabaseName ()Ljava/lang/String;\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun getPassword ()Ljava/lang/String;\n\tpublic fun getUserName ()Ljava/lang/String;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/mssql/MsSqlOptions;\n}\n\npublic final class com/trendyol/stove/mssql/MsSqlOptions$Companion {\n\tpublic final fun provided (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/mssql/ProvidedMsSqlOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/mssql/MsSqlOptions$Companion;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/mssql/ProvidedMsSqlOptions;\n}\n\npublic final class com/trendyol/stove/mssql/MsSqlOptionsKt {\n\tpublic static final fun mssql-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun mssql-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun mssql-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun mssql-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/mssql/MsSqlSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field Companion Lcom/trendyol/stove/mssql/MsSqlSystem$Companion;\n\tpublic field sqlOperations Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic final fun getSqlOperations ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun ops (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun pause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun setSqlOperations (Lcom/trendyol/stove/rdbms/NativeSqlOperations;)V\n\tpublic final fun shouldExecute (Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun shouldExecute$default (Lcom/trendyol/stove/mssql/MsSqlSystem;Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/mssql/MsSqlSystem$Companion {\n\tpublic final fun operations (Lcom/trendyol/stove/mssql/MsSqlSystem;)Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n}\n\npublic final class com/trendyol/stove/mssql/MssqlContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/mssql/ToolsPath;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/mssql/ToolsPath;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lcom/trendyol/stove/mssql/ToolsPath;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component7 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/mssql/ToolsPath;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/mssql/MssqlContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mssql/MssqlContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/mssql/ToolsPath;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/mssql/MssqlContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic final fun getToolsPath ()Lcom/trendyol/stove/mssql/ToolsPath;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mssql/ProvidedMsSqlOptions : com/trendyol/stove/mssql/MsSqlOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic final class com/trendyol/stove/mssql/SqlMigrationContext {\n\tpublic fun <init> (Lcom/trendyol/stove/mssql/MsSqlOptions;Lcom/trendyol/stove/rdbms/NativeSqlOperations;Lkotlin/jvm/functions/Function2;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/mssql/MsSqlOptions;\n\tpublic final fun component2 ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic final fun component3 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun copy (Lcom/trendyol/stove/mssql/MsSqlOptions;Lcom/trendyol/stove/rdbms/NativeSqlOperations;Lkotlin/jvm/functions/Function2;)Lcom/trendyol/stove/mssql/SqlMigrationContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mssql/SqlMigrationContext;Lcom/trendyol/stove/mssql/MsSqlOptions;Lcom/trendyol/stove/rdbms/NativeSqlOperations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/trendyol/stove/mssql/SqlMigrationContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getExecuteAsRoot ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getOperations ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/mssql/MsSqlOptions;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic class com/trendyol/stove/mssql/StoveMsSqlContainer : org/testcontainers/mssqlserver/MSSQLServerContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\npublic abstract class com/trendyol/stove/mssql/ToolsPath {\n\tpublic synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getPath ()Ljava/lang/String;\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mssql/ToolsPath$After2019 : com/trendyol/stove/mssql/ToolsPath {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/mssql/ToolsPath$After2019;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mssql/ToolsPath$Before2019 : com/trendyol/stove/mssql/ToolsPath {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/mssql/ToolsPath$Before2019;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mssql/ToolsPath$Custom : com/trendyol/stove/mssql/ToolsPath {\n\tpublic fun <init> (Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;)Lcom/trendyol/stove/mssql/ToolsPath$Custom;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mssql/ToolsPath$Custom;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/mssql/ToolsPath$Custom;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getPath ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\n"
  },
  {
    "path": "lib/stove-mssql/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stoveRdbms)\n  api(libs.testcontainers.mssql)\n  api(libs.microsoft.sqlserver.jdbc)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.logback.classic)\n}\n\nval testWithProvided = tasks.register<Test>(\"testWithProvided\") {\n  group = \"verification\"\n  description = \"Runs tests with an externally provided MSSQL instance\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useProvided\", \"true\")\n  doFirst {\n    println(\"Starting MSSQL tests with provided instance...\")\n  }\n}\n\ntasks.test.configure {\n  dependsOn(testWithProvided)\n}\n"
  },
  {
    "path": "lib/stove-mssql/src/main/kotlin/com/trendyol/stove/mssql/MsSqlOptions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.mssql\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.containers.*\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.rdbms.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.testcontainers.utility.DockerImageName\n\nsealed class ToolsPath(\n  open val path: String\n) {\n  data object Before2019 : ToolsPath(\"mssql-tools\")\n\n  data object After2019 : ToolsPath(\"mssql-tools18\")\n\n  data class Custom(\n    override val path: String\n  ) : ToolsPath(path)\n\n  override fun toString(): String = path\n}\n\nopen class StoveMsSqlContainer(\n  override val imageNameAccess: DockerImageName\n) : org.testcontainers.mssqlserver.MSSQLServerContainer(imageNameAccess),\n  StoveContainer\n\ndata class MssqlContainerOptions(\n  override val registry: String = DEFAULT_REGISTRY,\n  override val image: String = org.testcontainers.mssqlserver.MSSQLServerContainer.IMAGE,\n  override val tag: String = \"2022-latest\",\n  override val compatibleSubstitute: String? = null,\n  /**\n   * There is a breaking change introduced in the mssql-tools path after 2019.\n   * Depending on your tag, you may need to set this value.\n   */\n  val toolsPath: ToolsPath = ToolsPath.After2019,\n  override val useContainerFn: UseContainerFn<StoveMsSqlContainer> = { StoveMsSqlContainer(it) },\n  override val containerFn: ContainerFn<StoveMsSqlContainer> = { }\n) : ContainerOptions<StoveMsSqlContainer>\n\n/**\n * Options for configuring the MSSQL system in container mode.\n */\n@StoveDsl\nopen class MsSqlOptions(\n  open val applicationName: String,\n  open val databaseName: String,\n  open val userName: String,\n  open val password: String,\n  open val container: MssqlContainerOptions = MssqlContainerOptions(),\n  open val cleanup: suspend (NativeSqlOperations) -> Unit = {},\n  override val configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<RelationalDatabaseExposedConfiguration>,\n  SupportsMigrations<SqlMigrationContext, MsSqlOptions> {\n  override val migrationCollection: MigrationCollection<SqlMigrationContext> = MigrationCollection()\n\n  companion object {\n    /**\n     * Creates options configured to use an externally provided MSSQL instance\n     * instead of a testcontainer.\n     *\n     * @param jdbcUrl The JDBC URL for the MSSQL instance\n     * @param host The host of the MSSQL instance\n     * @param port The port of the MSSQL instance\n     * @param applicationName The application name\n     * @param databaseName The database name\n     * @param userName The username for authentication\n     * @param password The password for authentication\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      jdbcUrl: String,\n      host: String,\n      port: Int,\n      applicationName: String,\n      databaseName: String,\n      userName: String,\n      password: String,\n      runMigrations: Boolean = true,\n      cleanup: suspend (NativeSqlOperations) -> Unit = {},\n      configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n    ): ProvidedMsSqlOptions = ProvidedMsSqlOptions(\n      config = RelationalDatabaseExposedConfiguration(\n        jdbcUrl = jdbcUrl,\n        host = host,\n        port = port,\n        username = userName,\n        password = password\n      ),\n      applicationName = applicationName,\n      databaseName = databaseName,\n      userName = userName,\n      password = password,\n      runMigrations = runMigrations,\n      cleanup = cleanup,\n      configureExposedConfiguration = configureExposedConfiguration\n    )\n  }\n}\n\n/**\n * Options for using an externally provided MSSQL instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedMsSqlOptions(\n  /**\n   * The configuration for the provided MSSQL instance.\n   */\n  val config: RelationalDatabaseExposedConfiguration,\n  applicationName: String,\n  databaseName: String,\n  userName: String,\n  password: String,\n  cleanup: suspend (NativeSqlOperations) -> Unit = {},\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n) : MsSqlOptions(\n  applicationName = applicationName,\n  databaseName = databaseName,\n  userName = userName,\n  password = password,\n  container = MssqlContainerOptions(),\n  cleanup = cleanup,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<RelationalDatabaseExposedConfiguration> {\n  override val providedConfig: RelationalDatabaseExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n\n@StoveDsl\ndata class SqlMigrationContext(\n  val options: MsSqlOptions,\n  val operations: NativeSqlOperations,\n  val executeAsRoot: suspend (String) -> Unit\n)\n\n/**\n * Convenience type alias for MSSQL migrations.\n *\n * Instead of writing `DatabaseMigration<SqlMigrationContext>`, use `MsSqlMigration`:\n * ```kotlin\n * class MyMigration : MsSqlMigration {\n *   override val order: Int = 1\n *   override suspend fun execute(connection: SqlMigrationContext) { ... }\n * }\n * ```\n */\ntypealias MsSqlMigration = DatabaseMigration<SqlMigrationContext>\n\n@StoveDsl\ndata class MsSqlContext(\n  val runtime: SystemRuntime,\n  val options: MsSqlOptions,\n  val keyName: String? = null\n)\n\ninternal fun Stove.withMsSql(\n  options: MsSqlOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(MsSqlSystem(this, MsSqlContext(runtime, options)))\n  return this\n}\n\ninternal fun Stove.withMsSql(\n  key: SystemKey,\n  options: MsSqlOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(key, MsSqlSystem(this, MsSqlContext(runtime, options, keyName = keyDisplayName(key))))\n  return this\n}\n\ninternal fun Stove.mssql(): MsSqlSystem =\n  getOrNone<MsSqlSystem>().getOrElse {\n    throw SystemNotRegisteredException(MsSqlSystem::class)\n  }\n\ninternal fun Stove.mssql(key: SystemKey): MsSqlSystem =\n  getOrNone<MsSqlSystem>(key).getOrElse {\n    throw SystemNotRegisteredException(MsSqlSystem::class, \"No MsSqlSystem registered with key '${keyDisplayName(key)}'\")\n  }\n\n/**\n * Configures MSSQL system.\n *\n * For container-based setup:\n * ```kotlin\n * mssql {\n *   MsSqlOptions(\n *     applicationName = \"myapp\",\n *     databaseName = \"mydb\",\n *     userName = \"sa\",\n *     password = \"password\",\n *     cleanup = { ops -> ops.execute(\"TRUNCATE TABLE ...\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * mssql {\n *   MsSqlOptions.provided(\n *     jdbcUrl = \"jdbc:sqlserver://localhost:1433;databaseName=mydb\",\n *     host = \"localhost\",\n *     port = 1433,\n *     applicationName = \"myapp\",\n *     databaseName = \"mydb\",\n *     userName = \"sa\",\n *     password = \"password\",\n *     runMigrations = true,\n *     cleanup = { ops -> ops.execute(\"TRUNCATE TABLE ...\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n */\nfun WithDsl.mssql(\n  configure: () -> MsSqlOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedMsSqlOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .acceptLicense()\n        .withEnv(\"MSSQL_USER\", options.userName)\n        .withEnv(\"MSSQL_SA_PASSWORD\", options.password)\n        .withEnv(\"MSSQL_DB\", options.databaseName)\n        .withPassword(options.password)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveMsSqlContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withMsSql(options, runtime)\n}\n\nfun WithDsl.mssql(\n  key: SystemKey,\n  configure: () -> MsSqlOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedMsSqlOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .acceptLicense()\n        .withEnv(\"MSSQL_USER\", options.userName)\n        .withEnv(\"MSSQL_SA_PASSWORD\", options.password)\n        .withEnv(\"MSSQL_DB\", options.databaseName)\n        .withPassword(options.password)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveMsSqlContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withMsSql(key, options, runtime)\n}\n\nsuspend fun ValidationDsl.mssql(\n  validation: @StoveDsl suspend MsSqlSystem.() -> Unit\n): Unit = validation(this.stove.mssql())\n\nsuspend fun ValidationDsl.mssql(\n  key: SystemKey,\n  validation: @StoveDsl suspend MsSqlSystem.() -> Unit\n): Unit = validation(this.stove.mssql(key))\n"
  },
  {
    "path": "lib/stove-mssql/src/main/kotlin/com/trendyol/stove/mssql/MsSqlSystem.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.mssql\n\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.rdbms.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport kotlinx.coroutines.runBlocking\nimport kotliquery.*\nimport org.slf4j.*\n\n@StoveDsl\nclass MsSqlSystem internal constructor(\n  override val stove: Stove,\n  private val mssqlContext: MsSqlContext\n) : PluggedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  @PublishedApi\n  internal lateinit var sqlOperations: NativeSqlOperations\n\n  override val reportSystemName: String = \"MSSQL\" + (mssqlContext.keyName?.let { \" [$it]\" } ?: \"\")\n\n  private lateinit var exposedConfiguration: RelationalDatabaseExposedConfiguration\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val state: StateStorage<RelationalDatabaseExposedConfiguration> =\n    stove.createStateStorage<RelationalDatabaseExposedConfiguration, MsSqlSystem>(mssqlContext.keyName)\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n    sqlOperations = NativeSqlOperations(database(exposedConfiguration))\n    runMigrationsIfNeeded()\n  }\n\n  override suspend fun stop(): Unit = whenContainer { it.stop() }\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      if (::sqlOperations.isInitialized) {\n        mssqlContext.options.cleanup(sqlOperations)\n        sqlOperations.close()\n      }\n      executeWithReuseCheck { stop() }\n    }.recover {\n      logger.warn(\"got an error while stopping the MSSQL system\")\n    }.let { }\n  }\n\n  override fun configuration(): List<String> =\n    mssqlContext.options.configureExposedConfiguration(exposedConfiguration)\n\n  suspend inline fun <reified T : Any> shouldQuery(\n    query: String,\n    parameters: List<Parameter<*>> = emptyList(),\n    crossinline mapper: (Row) -> T,\n    crossinline assertion: (List<T>) -> Unit\n  ): MsSqlSystem {\n    report(\n      action = \"Query\",\n      input = arrow.core.Some(query.trim()),\n      metadata = mapOf(\"sql\" to query.trim())\n    ) {\n      val results = sqlOperations.select(sql = query, parameters = parameters) { mapper(it) }\n      assertion(results)\n      results\n    }\n    return this\n  }\n\n  suspend fun shouldExecute(sql: String, parameters: List<Parameter<*>> = emptyList()): MsSqlSystem {\n    report(\n      action = \"Execute SQL\",\n      input = arrow.core.Some(sql.trim()),\n      metadata = mapOf(\"sql\" to sql.trim())\n    ) {\n      val affectedRows = sqlOperations.execute(sql = sql, parameters = parameters)\n      check(affectedRows >= 0) { \"Failed to execute sql: $sql\" }\n      \"$affectedRows row(s) affected\"\n    }\n    return this\n  }\n\n  suspend fun ops(operations: suspend NativeSqlOperations.() -> Unit) {\n    operations(sqlOperations)\n  }\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return MsSqlSystem\n   */\n  suspend fun pause(): MsSqlSystem {\n    report(\n      action = \"Pause container\",\n      metadata = mapOf(\"operation\" to \"fault-injection\")\n    ) {\n      withContainerOrWarn(\"pause\") { it.pause() }\n    }\n    return this\n  }\n\n  /**\n   * Unpauses the container. Use with care, as it will unpause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return MsSqlSystem\n   */\n  suspend fun unpause(): MsSqlSystem {\n    report(action = \"Unpause container\") {\n      withContainerOrWarn(\"unpause\") { it.unpause() }\n    }\n    return this\n  }\n\n  private suspend fun obtainExposedConfiguration(): RelationalDatabaseExposedConfiguration =\n    when {\n      mssqlContext.options is ProvidedMsSqlOptions -> mssqlContext.options.config\n      mssqlContext.runtime is StoveMsSqlContainer -> startMsSqlContainer(mssqlContext.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${mssqlContext.runtime::class}\")\n    }\n\n  private suspend fun startMsSqlContainer(container: StoveMsSqlContainer): RelationalDatabaseExposedConfiguration =\n    state.capture {\n      container.start()\n      RelationalDatabaseExposedConfiguration(\n        jdbcUrl = container.jdbcUrl,\n        host = container.host,\n        port = container.firstMappedPort,\n        username = container.username,\n        password = container.password\n      )\n    }\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (!shouldRunMigrations()) return\n\n    val executeAsRoot = createExecuteAsRootFn()\n    createDatabaseIfNeeded(executeAsRoot)\n\n    mssqlContext.options.migrationCollection.run(\n      SqlMigrationContext(mssqlContext.options, sqlOperations) { executeAsRoot(it) }\n    )\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    mssqlContext.options is ProvidedMsSqlOptions -> mssqlContext.options.runMigrations\n    mssqlContext.runtime is StoveMsSqlContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${mssqlContext.runtime::class}\")\n  }\n\n  private fun createExecuteAsRootFn(): suspend (String) -> Unit = when {\n    mssqlContext.options is ProvidedMsSqlOptions -> { sql: String -> sqlOperations.execute(sql) }\n    mssqlContext.runtime is StoveMsSqlContainer -> containerExecuteAsRoot(mssqlContext.runtime)\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${mssqlContext.runtime::class}\")\n  }\n\n  private suspend fun createDatabaseIfNeeded(executeAsRoot: suspend (String) -> Unit) {\n    if (mssqlContext.runtime is StoveMsSqlContainer) {\n      executeAsRoot(\"CREATE DATABASE ${mssqlContext.options.databaseName}\")\n    }\n  }\n\n  private fun containerExecuteAsRoot(container: StoveMsSqlContainer): suspend (String) -> Unit = { sql: String ->\n    // Use execCommand which works via Docker client directly, supporting both\n    // fresh starts and subsequent runs with reuse (where container isn't \"started\" by testcontainers)\n    container\n      .execCommand(\n        \"/opt/${mssqlContext.options.container.toolsPath.path}/bin/sqlcmd\",\n        \"-S\",\n        \"localhost\",\n        \"-U\",\n        mssqlContext.options.userName,\n        \"-C\",\n        \"-P\",\n        mssqlContext.options.password,\n        \"-Q\",\n        sql\n      ).let {\n        check(it.exitCode == 0) {\n          \"\"\"\n          Failed to execute sql: $sql\n          Reason: ${it.stderr}\n          ToolsPath: ${mssqlContext.options.container.toolsPath}\n          ContainerTag: ${mssqlContext.options.container.imageWithTag}\n          Exit code: ${it.exitCode}\n          Recommendation: Try setting the toolsPath to ToolsPath.Before2019 or ToolsPath.After2019 depending on your mssql version.\n          \"\"\".trimIndent()\n        }\n      }\n  }\n\n  private fun database(exposedConfiguration: RelationalDatabaseExposedConfiguration): Session = sessionOf(\n    exposedConfiguration.jdbcUrl,\n    exposedConfiguration.username,\n    exposedConfiguration.password\n  )\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StoveMsSqlContainer) -> Unit\n  ): MsSqlSystem = when (val runtime = mssqlContext.runtime) {\n    is StoveMsSqlContainer -> {\n      action(runtime)\n      this\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"$operation() is not supported when using a provided instance\")\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private inline fun whenContainer(action: (StoveMsSqlContainer) -> Unit) {\n    if (mssqlContext.runtime is StoveMsSqlContainer) {\n      action(mssqlContext.runtime)\n    }\n  }\n\n  companion object {\n    /**\n     * Exposes the [NativeSqlOperations] to the [MsSqlSystem].\n     * Use this for advanced SQL operations not covered by the DSL.\n     */\n    fun MsSqlSystem.operations(): NativeSqlOperations = sqlOperations\n  }\n}\n"
  },
  {
    "path": "lib/stove-mssql/src/test/kotlin/com/trendyol/stove/mssql/MsSqlOptionsTest.kt",
    "content": "package com.trendyol.stove.mssql\n\nimport com.trendyol.stove.rdbms.RelationalDatabaseExposedConfiguration\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\n\nclass MsSqlOptionsTest :\n  FunSpec({\n\n    test(\"MsSqlOptions.provided should create ProvidedMsSqlOptions with correct config\") {\n      val options = MsSqlOptions.provided(\n        jdbcUrl = \"jdbc:sqlserver://localhost:1433;databaseName=testdb\",\n        host = \"localhost\",\n        port = 1433,\n        applicationName = \"myapp\",\n        databaseName = \"testdb\",\n        userName = \"sa\",\n        password = \"sapass\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\"spring.datasource.url=${cfg.jdbcUrl}\")\n        }\n      )\n\n      options.providedConfig.jdbcUrl shouldBe \"jdbc:sqlserver://localhost:1433;databaseName=testdb\"\n      options.providedConfig.host shouldBe \"localhost\"\n      options.providedConfig.port shouldBe 1433\n      options.providedConfig.username shouldBe \"sa\"\n      options.providedConfig.password shouldBe \"sapass\"\n      options.runMigrationsForProvided shouldBe true\n    }\n\n    test(\"ProvidedMsSqlOptions should expose correct properties\") {\n      val config = RelationalDatabaseExposedConfiguration(\n        jdbcUrl = \"jdbc:sqlserver://remote:1433\",\n        host = \"remote\",\n        port = 1433,\n        username = \"user\",\n        password = \"pass\"\n      )\n      val options = ProvidedMsSqlOptions(\n        config = config,\n        applicationName = \"app\",\n        databaseName = \"db\",\n        userName = \"user\",\n        password = \"pass\",\n        runMigrations = false,\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.config shouldBe config\n      options.providedConfig shouldBe config\n      options.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"MssqlContainerOptions should have defaults\") {\n      val opts = MssqlContainerOptions()\n      opts.tag shouldBe \"2022-latest\"\n      opts.toolsPath shouldBe ToolsPath.After2019\n    }\n\n    test(\"ToolsPath sealed class variants\") {\n      ToolsPath.Before2019.path shouldBe \"mssql-tools\"\n      ToolsPath.After2019.path shouldBe \"mssql-tools18\"\n      ToolsPath.Custom(\"custom-path\").path shouldBe \"custom-path\"\n    }\n\n    test(\"MsSqlOptions should require application and database name\") {\n      val options = object : MsSqlOptions(\n        applicationName = \"test-app\",\n        databaseName = \"test-db\",\n        userName = \"sa\",\n        password = \"sapass\",\n        configureExposedConfiguration = { _ -> listOf() }\n      ) {}\n\n      options.applicationName shouldBe \"test-app\"\n      options.databaseName shouldBe \"test-db\"\n      options.userName shouldBe \"sa\"\n      options.password shouldBe \"sapass\"\n      options.container shouldNotBe null\n    }\n  })\n"
  },
  {
    "path": "lib/stove-mssql/src/test/kotlin/com/trendyol/stove/mssql/MssqlSystemTest.kt",
    "content": "package com.trendyol.stove.mssql\n\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport io.kotest.core.spec.style.ShouldSpec\nimport io.kotest.matchers.shouldBe\nimport kotliquery.param\nimport org.slf4j.*\nimport org.testcontainers.mssqlserver.MSSQLServerContainer\nimport org.testcontainers.utility.DockerImageName\n\n// ============================================================================\n// Shared components\n// ============================================================================\n\nclass NoOpApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) = Unit\n\n  override suspend fun stop() = Unit\n}\n\nclass InitialMigration : MsSqlMigration {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override val order: Int = MigrationPriority.HIGHEST.value + 1\n\n  override suspend fun execute(connection: SqlMigrationContext) {\n    val sql = \"\"\"\n     CREATE TABLE Person (\n        PersonID int,\n        LastName varchar(255),\n        FirstName varchar(255),\n        Address varchar(255),\n        City varchar(255)\n      );\n    \"\"\".trimIndent()\n    logger.info(\"Executing migration: $sql\")\n    Try {\n      connection.executeAsRoot(sql)\n    }.recover {\n      logger.error(\"Migration failed\", it)\n      throw it\n    }\n    logger.info(\"Migration executed successfully\")\n  }\n}\n\ndata class Person(\n  val personId: Int,\n  val lastName: String,\n  val firstName: String,\n  val address: String,\n  val city: String\n)\n\n// ============================================================================\n// Strategy interface\n// ============================================================================\n\nsealed interface MsSqlTestStrategy {\n  val logger: Logger\n\n  suspend fun start()\n\n  suspend fun stop()\n\n  companion object {\n    fun select(): MsSqlTestStrategy {\n      val useProvided = System.getenv(\"USE_PROVIDED\")?.toBoolean()\n        ?: System.getProperty(\"useProvided\")?.toBoolean()\n        ?: false\n\n      return if (useProvided) ProvidedMsSqlStrategy() else ContainerMsSqlStrategy()\n    }\n  }\n}\n\n// ============================================================================\n// Container-based strategy (default)\n// ============================================================================\n\nclass ContainerMsSqlStrategy : MsSqlTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun start() {\n    logger.info(\"Starting MSSQL tests with container mode\")\n\n    val options = MsSqlOptions(\n      applicationName = \"test\",\n      databaseName = \"test\",\n      userName = \"sa\",\n      password = \"Password12!\",\n      container = MssqlContainerOptions(\n        toolsPath = ToolsPath.After2019,\n        image = \"mcr.microsoft.com/mssql/server\",\n        tag = \"2022-CU16-ubuntu-22.04\"\n      ) {\n        withStartupAttempts(3)\n      },\n      configureExposedConfiguration = { _ -> listOf() }\n    ).migrations {\n      register<InitialMigration>()\n    }\n\n    Stove()\n      .with {\n        mssql { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    logger.info(\"MSSQL container tests completed\")\n  }\n}\n\n// ============================================================================\n// Provided instance strategy\n// ============================================================================\n\nclass ProvidedMsSqlStrategy : MsSqlTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  private lateinit var externalContainer: MSSQLServerContainer\n\n  override suspend fun start() {\n    logger.info(\"Starting MSSQL tests with provided mode\")\n\n    // Start an external container to simulate a provided instance\n    externalContainer = MSSQLServerContainer(\n      DockerImageName.parse(\"mcr.microsoft.com/mssql/server:2022-CU16-ubuntu-22.04\")\n    ).apply {\n      acceptLicense()\n      withPassword(\"Password12!\")\n      start()\n    }\n\n    logger.info(\"External MSSQL container started at ${externalContainer.jdbcUrl}\")\n\n    val options = MsSqlOptions\n      .provided(\n        jdbcUrl = externalContainer.jdbcUrl,\n        host = externalContainer.host,\n        port = externalContainer.firstMappedPort,\n        applicationName = \"test\",\n        databaseName = \"master\", // Use master for provided since we can't easily create DB\n        userName = externalContainer.username,\n        password = externalContainer.password,\n        runMigrations = true,\n        cleanup = { sqlOps ->\n          logger.info(\"Running cleanup on provided instance\")\n          Try { sqlOps.execute(\"DROP TABLE IF EXISTS Person\") }\n        },\n        configureExposedConfiguration = { _ -> listOf() }\n      ).migrations {\n        register<InitialMigration>()\n      }\n\n    Stove()\n      .with {\n        mssql { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    externalContainer.stop()\n    logger.info(\"MSSQL provided tests completed\")\n  }\n}\n\n// ============================================================================\n// Kotest project config - selects strategy based on environment\n// ============================================================================\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val strategy = MsSqlTestStrategy.select()\n\n  override suspend fun beforeProject() = strategy.start()\n\n  override suspend fun afterProject() = strategy.stop()\n}\n\n// ============================================================================\n// Tests\n// ============================================================================\n\nclass MssqlSystemTests :\n  ShouldSpec({\n    should(\"work\") {\n      stove {\n        mssql {\n          ops {\n            val result = select(\"SELECT 1\") {\n              it.int(1)\n            }\n            result.first() shouldBe 1\n          }\n\n          shouldExecute(\"insert into Person values (1, 'Doe', 'John', '123 Main St', 'Springfield')\")\n\n          shouldQuery<Person>(\n            query = \"select * from Person\",\n            mapper = {\n              Person(\n                it.int(1),\n                it.string(2),\n                it.string(3),\n                it.string(4),\n                it.string(5)\n              )\n            }\n          ) { result ->\n            result.size shouldBe 1\n            result.first().apply {\n              personId shouldBe 1\n              lastName shouldBe \"Doe\"\n              firstName shouldBe \"John\"\n              address shouldBe \"123 Main St\"\n              city shouldBe \"Springfield\"\n            }\n          }\n        }\n      }\n    }\n\n    should(\"work with parameterized queries\") {\n      stove {\n        mssql {\n          // Insert with parameters\n          shouldExecute(\n            sql = \"insert into Person values (?, ?, ?, ?, ?)\",\n            parameters = listOf(\n              2.param(),\n              \"Smith\".param(),\n              \"Jane\".param(),\n              \"456 Oak Ave\".param(),\n              \"Boston\".param()\n            )\n          )\n\n          shouldExecute(\n            sql = \"insert into Person values (?, ?, ?, ?, ?)\",\n            parameters = listOf(\n              3.param(),\n              \"Johnson\".param(),\n              \"Mike\".param(),\n              \"789 Pine Rd\".param(),\n              \"Boston\".param()\n            )\n          )\n\n          // Query with parameters\n          shouldQuery<Person>(\n            query = \"select * from Person where City = ? order by PersonID\",\n            parameters = listOf(\"Boston\".param()),\n            mapper = {\n              Person(\n                it.int(1),\n                it.string(2),\n                it.string(3),\n                it.string(4),\n                it.string(5)\n              )\n            }\n          ) { result ->\n            result.size shouldBe 2\n            result.first().apply {\n              personId shouldBe 2\n              lastName shouldBe \"Smith\"\n              firstName shouldBe \"Jane\"\n              city shouldBe \"Boston\"\n            }\n            result.last().apply {\n              personId shouldBe 3\n              lastName shouldBe \"Johnson\"\n              firstName shouldBe \"Mike\"\n              city shouldBe \"Boston\"\n            }\n          }\n\n          // Query with multiple parameters\n          shouldQuery<Person>(\n            query = \"select * from Person where LastName = ? and FirstName = ?\",\n            parameters = listOf(\"Smith\".param(), \"Jane\".param()),\n            mapper = {\n              Person(\n                it.int(1),\n                it.string(2),\n                it.string(3),\n                it.string(4),\n                it.string(5)\n              )\n            }\n          ) { result ->\n            result.size shouldBe 1\n            result.first().apply {\n              lastName shouldBe \"Smith\"\n              firstName shouldBe \"Jane\"\n              address shouldBe \"456 Oak Ave\"\n            }\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-mssql/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.mssql.StoveConfig\n"
  },
  {
    "path": "lib/stove-mssql/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "lib/stove-mysql/api/stove-mysql.api",
    "content": "public final class com/trendyol/stove/mysql/MySqlContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/mysql/MySqlContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mysql/MySqlContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/mysql/MySqlContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/mysql/MySqlMigrationContext {\n\tpublic fun <init> (Lcom/trendyol/stove/mysql/MySqlOptions;Lcom/trendyol/stove/rdbms/NativeSqlOperations;Lkotlin/jvm/functions/Function2;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/mysql/MySqlOptions;\n\tpublic final fun component2 ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic final fun component3 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun copy (Lcom/trendyol/stove/mysql/MySqlOptions;Lcom/trendyol/stove/rdbms/NativeSqlOperations;Lkotlin/jvm/functions/Function2;)Lcom/trendyol/stove/mysql/MySqlMigrationContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/mysql/MySqlMigrationContext;Lcom/trendyol/stove/mysql/MySqlOptions;Lcom/trendyol/stove/rdbms/NativeSqlOperations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/trendyol/stove/mysql/MySqlMigrationContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getExecuteAsRoot ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getOperations ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/mysql/MySqlOptions;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic class com/trendyol/stove/mysql/MySqlOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/mysql/MySqlOptions$Companion;\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/mysql/MySqlContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/mysql/MySqlContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainer ()Lcom/trendyol/stove/mysql/MySqlContainerOptions;\n\tpublic fun getDatabaseName ()Ljava/lang/String;\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun getPassword ()Ljava/lang/String;\n\tpublic fun getUsername ()Ljava/lang/String;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/mysql/MySqlOptions;\n}\n\npublic final class com/trendyol/stove/mysql/MySqlOptions$Companion {\n\tpublic final fun provided (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/mysql/ProvidedMySqlOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/mysql/MySqlOptions$Companion;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/mysql/ProvidedMySqlOptions;\n}\n\npublic final class com/trendyol/stove/mysql/MySqlSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field Companion Lcom/trendyol/stove/mysql/MySqlSystem$Companion;\n\tpublic field sqlOperations Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic final fun getSqlOperations ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun pause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun setSqlOperations (Lcom/trendyol/stove/rdbms/NativeSqlOperations;)V\n\tpublic final fun shouldExecute (Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun shouldExecute$default (Lcom/trendyol/stove/mysql/MySqlSystem;Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/mysql/MySqlSystem$Companion {\n\tpublic final fun operations (Lcom/trendyol/stove/mysql/MySqlSystem;)Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n}\n\npublic final class com/trendyol/stove/mysql/OptionsKt {\n\tpublic static final field DEFAULT_MYSQL_IMAGE_NAME Ljava/lang/String;\n\tpublic static final fun mysql-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun mysql-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun mysql-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun mysql-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/mysql/ProvidedMySqlOptions : com/trendyol/stove/mysql/MySqlOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic class com/trendyol/stove/mysql/StoveMySqlContainer : org/testcontainers/mysql/MySQLContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\n"
  },
  {
    "path": "lib/stove-mysql/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stoveRdbms)\n  api(libs.testcontainers.mysql)\n  api(libs.mysql.connector)\n  testImplementation(projects.testExtensions.stoveExtensionsKotest)\n  testImplementation(libs.logback.classic)\n}\n\nval testWithProvided = tasks.register<Test>(\"testWithProvided\") {\n  group = \"verification\"\n  description = \"Runs tests with an externally provided MySQL instance\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useProvided\", \"true\")\n  doFirst {\n    println(\"Starting MySQL tests with provided instance...\")\n  }\n}\n\ntasks.test.configure {\n  dependsOn(testWithProvided)\n}\n"
  },
  {
    "path": "lib/stove-mysql/src/main/kotlin/com/trendyol/stove/mysql/MySqlSystem.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.mysql\n\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.rdbms.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport kotlinx.coroutines.runBlocking\nimport kotliquery.*\nimport org.slf4j.*\n\n/**\n * MySQL database system for testing relational data operations.\n */\n@StoveDsl\nclass MySqlSystem internal constructor(\n  override val stove: Stove,\n  private val mysqlContext: MySqlContext\n) : PluggedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  @PublishedApi\n  internal lateinit var sqlOperations: NativeSqlOperations\n\n  override val reportSystemName: String = \"MySQL\" + (mysqlContext.keyName?.let { \" [$it]\" } ?: \"\")\n\n  private lateinit var exposedConfiguration: RelationalDatabaseExposedConfiguration\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val state: StateStorage<RelationalDatabaseExposedConfiguration> =\n    stove.createStateStorage<RelationalDatabaseExposedConfiguration, MySqlSystem>(mysqlContext.keyName)\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n    sqlOperations = NativeSqlOperations(database(exposedConfiguration))\n    runMigrationsIfNeeded()\n  }\n\n  override suspend fun stop(): Unit = whenContainer { it.stop() }\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      if (::sqlOperations.isInitialized) {\n        mysqlContext.options.cleanup(sqlOperations)\n        sqlOperations.close()\n      }\n      executeWithReuseCheck { stop() }\n    }.recover { logger.warn(\"MySQL stop failed\", it) }\n  }\n\n  override fun configuration(): List<String> =\n    mysqlContext.options.configureExposedConfiguration(exposedConfiguration)\n\n  suspend inline fun <reified T : Any> shouldQuery(\n    query: String,\n    parameters: List<Parameter<*>> = emptyList(),\n    crossinline mapper: (Row) -> T,\n    crossinline assertion: (List<T>) -> Unit\n  ): MySqlSystem {\n    report(\n      action = \"Query\",\n      input = arrow.core.Some(query.trim()),\n      metadata = mapOf(\"sql\" to query.trim())\n    ) {\n      val results = sqlOperations.select(sql = query, parameters = parameters) { mapper(it) }\n      assertion(results)\n      results\n    }\n    return this\n  }\n\n  suspend fun shouldExecute(sql: String, parameters: List<Parameter<*>> = emptyList()): MySqlSystem {\n    report(\n      action = \"Execute SQL\",\n      input = arrow.core.Some(sql.trim()),\n      metadata = mapOf(\"sql\" to sql.trim())\n    ) {\n      val affectedRows = sqlOperations.execute(sql = sql, parameters = parameters)\n      check(affectedRows >= 0) { \"Failed to execute sql: $sql\" }\n      \"$affectedRows row(s) affected\"\n    }\n    return this\n  }\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return MySqlSystem\n   */\n  suspend fun pause(): MySqlSystem {\n    report(\n      action = \"Pause container\",\n      metadata = mapOf(\"operation\" to \"fault-injection\")\n    ) {\n      withContainerOrWarn(\"pause\") { it.pause() }\n    }\n    return this\n  }\n\n  /**\n   * Unpauses the container. Use with care, as it will unpause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return MySqlSystem\n   */\n  suspend fun unpause(): MySqlSystem {\n    report(action = \"Unpause container\") {\n      withContainerOrWarn(\"unpause\") { it.unpause() }\n    }\n    return this\n  }\n\n  private suspend fun obtainExposedConfiguration(): RelationalDatabaseExposedConfiguration =\n    when {\n      mysqlContext.options is ProvidedMySqlOptions -> mysqlContext.options.config\n      mysqlContext.runtime is StoveMySqlContainer -> startMySqlContainer(mysqlContext.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${mysqlContext.runtime::class}\")\n    }\n\n  private suspend fun startMySqlContainer(container: StoveMySqlContainer): RelationalDatabaseExposedConfiguration =\n    state.capture {\n      container.start()\n      RelationalDatabaseExposedConfiguration(\n        jdbcUrl = container.jdbcUrl,\n        host = container.host,\n        port = container.firstMappedPort,\n        username = container.username,\n        password = container.password\n      )\n    }\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (shouldRunMigrations()) {\n      val executeAsRoot = createExecuteAsRootFn()\n      mysqlContext.options.migrationCollection.run(\n        MySqlMigrationContext(mysqlContext.options, sqlOperations) { executeAsRoot(it) }\n      )\n    }\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    mysqlContext.options is ProvidedMySqlOptions -> mysqlContext.options.runMigrations\n    mysqlContext.runtime is StoveMySqlContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${mysqlContext.runtime::class}\")\n  }\n\n  private fun createExecuteAsRootFn(): suspend (String) -> Unit = when {\n    mysqlContext.options is ProvidedMySqlOptions -> { sql: String -> sqlOperations.execute(sql) }\n\n    mysqlContext.runtime is StoveMySqlContainer -> { sql: String ->\n      val container = mysqlContext.runtime\n      // Use execCommand which works via Docker client directly, supporting both\n      // fresh starts and subsequent runs with reuse (where container isn't \"started\" by testcontainers)\n      container\n        .execCommand(\n          \"/bin/bash\",\n          \"-c\",\n          \"mysql -u ${container.username} -p${container.password} ${container.databaseName} -e \\\"$sql\\\"\"\n        ).let {\n          check(it.exitCode == 0) { \"Failed to execute sql: $sql, reason: ${it.stderr}\" }\n        }\n    }\n\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${mysqlContext.runtime::class}\")\n  }\n\n  private fun database(exposedConfiguration: RelationalDatabaseExposedConfiguration): Session = sessionOf(\n    url = exposedConfiguration.jdbcUrl,\n    user = exposedConfiguration.username,\n    password = exposedConfiguration.password\n  )\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StoveMySqlContainer) -> Unit\n  ): MySqlSystem = when (val runtime = mysqlContext.runtime) {\n    is StoveMySqlContainer -> {\n      action(runtime)\n      this\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"$operation() is not supported when using a provided instance\")\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private inline fun whenContainer(action: (StoveMySqlContainer) -> Unit) {\n    if (mysqlContext.runtime is StoveMySqlContainer) {\n      action(mysqlContext.runtime)\n    }\n  }\n\n  companion object {\n    /**\n     * Exposes the [NativeSqlOperations] to the [MySqlSystem].\n     * Use this for advanced SQL operations not covered by the DSL.\n     */\n    fun MySqlSystem.operations(): NativeSqlOperations = sqlOperations\n  }\n}\n"
  },
  {
    "path": "lib/stove-mysql/src/main/kotlin/com/trendyol/stove/mysql/Options.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.mysql\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.containers.*\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.rdbms.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.testcontainers.mysql.MySQLContainer\nimport org.testcontainers.utility.DockerImageName\n\nconst val DEFAULT_MYSQL_IMAGE_NAME = \"mysql\"\n\nopen class StoveMySqlContainer(\n  override val imageNameAccess: DockerImageName\n) : MySQLContainer(imageNameAccess),\n  StoveContainer\n\n/**\n * Container options for MySQL.\n */\ndata class MySqlContainerOptions(\n  override val registry: String = DEFAULT_REGISTRY,\n  override val image: String = DEFAULT_MYSQL_IMAGE_NAME,\n  override val tag: String = \"8.4\",\n  override val compatibleSubstitute: String? = null,\n  override val useContainerFn: UseContainerFn<StoveMySqlContainer> = { StoveMySqlContainer(it) },\n  override val containerFn: ContainerFn<StoveMySqlContainer> = { }\n) : ContainerOptions<StoveMySqlContainer>\n\n/**\n * Options for configuring the MySQL system in container mode.\n */\n@StoveDsl\nopen class MySqlOptions(\n  open val databaseName: String = \"stove\",\n  open val username: String = \"sa\",\n  open val password: String = \"sa\",\n  open val container: MySqlContainerOptions = MySqlContainerOptions(),\n  open val cleanup: suspend (NativeSqlOperations) -> Unit = {},\n  override val configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<RelationalDatabaseExposedConfiguration>,\n  SupportsMigrations<MySqlMigrationContext, MySqlOptions> {\n  override val migrationCollection: MigrationCollection<MySqlMigrationContext> = MigrationCollection()\n\n  companion object {\n    /**\n     * Creates options configured to use an externally provided MySQL instance\n     * instead of a testcontainer.\n     *\n     * @param jdbcUrl The JDBC URL for the MySQL instance\n     * @param host The host of the MySQL instance\n     * @param port The port of the MySQL instance\n     * @param databaseName The database name\n     * @param username The username for authentication\n     * @param password The password for authentication\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      jdbcUrl: String,\n      host: String,\n      port: Int,\n      databaseName: String = \"stove\",\n      username: String = \"sa\",\n      password: String = \"sa\",\n      runMigrations: Boolean = true,\n      cleanup: suspend (NativeSqlOperations) -> Unit = {},\n      configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n    ): ProvidedMySqlOptions = ProvidedMySqlOptions(\n      config = RelationalDatabaseExposedConfiguration(\n        jdbcUrl = jdbcUrl,\n        host = host,\n        port = port,\n        username = username,\n        password = password\n      ),\n      databaseName = databaseName,\n      username = username,\n      password = password,\n      runMigrations = runMigrations,\n      cleanup = cleanup,\n      configureExposedConfiguration = configureExposedConfiguration\n    )\n  }\n}\n\n/**\n * Options for using an externally provided MySQL instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedMySqlOptions(\n  /**\n   * The configuration for the provided MySQL instance.\n   */\n  val config: RelationalDatabaseExposedConfiguration,\n  databaseName: String = \"stove\",\n  username: String = \"sa\",\n  password: String = \"sa\",\n  cleanup: suspend (NativeSqlOperations) -> Unit = {},\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n) : MySqlOptions(\n  databaseName = databaseName,\n  username = username,\n  password = password,\n  container = MySqlContainerOptions(),\n  cleanup = cleanup,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<RelationalDatabaseExposedConfiguration> {\n  override val providedConfig: RelationalDatabaseExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n\n@StoveDsl\ndata class MySqlMigrationContext(\n  val options: MySqlOptions,\n  val operations: NativeSqlOperations,\n  val executeAsRoot: suspend (String) -> Unit\n)\n\n/**\n * Convenience type alias for MySQL migrations.\n *\n * Instead of writing `DatabaseMigration<MySqlMigrationContext>`, use `MySqlMigration`:\n * ```kotlin\n * class MyMigration : MySqlMigration {\n *   override val order: Int = 1\n *   override suspend fun execute(connection: MySqlMigrationContext) { ... }\n * }\n * ```\n */\ntypealias MySqlMigration = DatabaseMigration<MySqlMigrationContext>\n\ninternal class MySqlContext(\n  val runtime: SystemRuntime,\n  val options: MySqlOptions,\n  val keyName: String? = null\n)\n\ninternal fun Stove.withMySql(\n  options: MySqlOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(MySqlSystem(this, MySqlContext(runtime, options)))\n  return this\n}\n\ninternal fun Stove.withMySql(\n  key: SystemKey,\n  options: MySqlOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(key, MySqlSystem(this, MySqlContext(runtime, options, keyName = keyDisplayName(key))))\n  return this\n}\n\ninternal fun Stove.mysql(): MySqlSystem =\n  getOrNone<MySqlSystem>().getOrElse {\n    throw SystemNotRegisteredException(MySqlSystem::class)\n  }\n\ninternal fun Stove.mysql(key: SystemKey): MySqlSystem =\n  getOrNone<MySqlSystem>(key).getOrElse {\n    throw SystemNotRegisteredException(MySqlSystem::class, \"No MySqlSystem registered with key '${keyDisplayName(key)}'\")\n  }\n\n/**\n * Configures MySQL system.\n *\n * For container-based setup:\n * ```kotlin\n * mysql {\n *   MySqlOptions(\n *     databaseName = \"mydb\",\n *     cleanup = { ops -> ops.execute(\"TRUNCATE TABLE ...\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * mysql {\n *   MySqlOptions.provided(\n *     jdbcUrl = \"jdbc:mysql://localhost:3306/mydb\",\n *     host = \"localhost\",\n *     port = 3306,\n *     username = \"user\",\n *     password = \"pass\",\n *     runMigrations = true,\n *     cleanup = { ops -> ops.execute(\"TRUNCATE TABLE ...\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n */\nfun WithDsl.mysql(\n  configure: () -> MySqlOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedMySqlOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withDatabaseName(options.databaseName)\n        .withUsername(options.username)\n        .withPassword(options.password)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveMySqlContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withMySql(options, runtime)\n}\n\nfun WithDsl.mysql(\n  key: SystemKey,\n  configure: () -> MySqlOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedMySqlOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withDatabaseName(options.databaseName)\n        .withUsername(options.username)\n        .withPassword(options.password)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveMySqlContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withMySql(key, options, runtime)\n}\n\nsuspend fun ValidationDsl.mysql(validation: @StoveDsl suspend MySqlSystem.() -> Unit): Unit =\n  validation(this.stove.mysql())\n\nsuspend fun ValidationDsl.mysql(key: SystemKey, validation: @StoveDsl suspend MySqlSystem.() -> Unit): Unit =\n  validation(this.stove.mysql(key))\n"
  },
  {
    "path": "lib/stove-mysql/src/test/kotlin/com/trendyol/stove/mysql/MySqlOptionsTest.kt",
    "content": "package com.trendyol.stove.mysql\n\nimport com.trendyol.stove.rdbms.RelationalDatabaseExposedConfiguration\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\n\nclass MySqlOptionsTest :\n  FunSpec({\n\n    test(\"MySqlOptions.provided should create ProvidedMySqlOptions with correct config\") {\n      val options = MySqlOptions.provided(\n        jdbcUrl = \"jdbc:mysql://localhost:3306/testdb\",\n        host = \"localhost\",\n        port = 3306,\n        databaseName = \"testdb\",\n        username = \"root\",\n        password = \"rootpass\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\"spring.datasource.url=${cfg.jdbcUrl}\")\n        }\n      )\n\n      options.providedConfig.jdbcUrl shouldBe \"jdbc:mysql://localhost:3306/testdb\"\n      options.providedConfig.host shouldBe \"localhost\"\n      options.providedConfig.port shouldBe 3306\n      options.providedConfig.username shouldBe \"root\"\n      options.providedConfig.password shouldBe \"rootpass\"\n      options.runMigrationsForProvided shouldBe true\n    }\n\n    test(\"ProvidedMySqlOptions should expose correct properties\") {\n      val config = RelationalDatabaseExposedConfiguration(\n        jdbcUrl = \"jdbc:mysql://remote:3306/db\",\n        host = \"remote\",\n        port = 3306,\n        username = \"user\",\n        password = \"pass\"\n      )\n      val options = ProvidedMySqlOptions(\n        config = config,\n        databaseName = \"db\",\n        runMigrations = false,\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.config shouldBe config\n      options.providedConfig shouldBe config\n      options.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"MySqlOptions should have sensible defaults\") {\n      val options = object : MySqlOptions(\n        configureExposedConfiguration = { _ -> listOf() }\n      ) {}\n\n      options.databaseName shouldBe \"stove\"\n      options.username shouldBe \"sa\"\n      options.password shouldBe \"sa\"\n      options.container shouldNotBe null\n    }\n\n    test(\"MySqlContainerOptions should have defaults\") {\n      val opts = MySqlContainerOptions()\n      opts.image shouldBe DEFAULT_MYSQL_IMAGE_NAME\n      opts.tag shouldBe \"8.4\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove-mysql/src/test/kotlin/com/trendyol/stove/mysql/MySqlSystemTests.kt",
    "content": "package com.trendyol.stove.mysql\n\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.ints.shouldBeGreaterThan\nimport io.kotest.matchers.shouldBe\nimport kotliquery.param\n\n/**\n * MySQL system tests that run against both container-based and provided instances.\n */\nclass MySqlSystemTests :\n  FunSpec({\n    data class IdAndDescription(\n      val id: Long,\n      val description: String\n    )\n\n    test(\"migration should create MigrationHistory table\") {\n      stove {\n        mysql {\n          shouldQuery<IdAndDescription>(\n            \"SELECT * FROM MigrationHistory\",\n            mapper = { row ->\n              IdAndDescription(row.long(\"id\"), row.string(\"description\"))\n            }\n          ) { actual ->\n            actual.size shouldBeGreaterThan 0\n            actual.first() shouldBe IdAndDescription(1, \"InitialMigration\")\n          }\n        }\n      }\n    }\n\n    test(\"should execute DDL and DML statements\") {\n      stove {\n        mysql {\n          shouldExecute(\"DROP TABLE IF EXISTS Dummies\")\n          shouldExecute(\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS Dummies (\n              id INT AUTO_INCREMENT PRIMARY KEY,\n              description VARCHAR (50) NOT NULL\n            )\n            \"\"\".trimIndent()\n          )\n          shouldExecute(\"INSERT INTO Dummies (description) VALUES ('${testCase.name.name}')\")\n          shouldQuery<IdAndDescription>(\n            \"SELECT * FROM Dummies\",\n            mapper = {\n              IdAndDescription(it.long(\"id\"), it.string(\"description\"))\n            }\n          ) { actual ->\n            actual.size shouldBeGreaterThan 0\n            actual.first().description shouldBe testCase.name.name\n          }\n        }\n      }\n    }\n\n    test(\"should handle multiple inserts and queries\") {\n      stove {\n        mysql {\n          shouldExecute(\"DROP TABLE IF EXISTS TestItems\")\n          shouldExecute(\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS TestItems (\n              id INT AUTO_INCREMENT PRIMARY KEY,\n              name VARCHAR (100) NOT NULL,\n              value INT NOT NULL\n            )\n            \"\"\".trimIndent()\n          )\n\n          repeat(5) { i ->\n            shouldExecute(\"INSERT INTO TestItems (name, value) VALUES ('item_$i', $i)\")\n          }\n\n          data class TestItem(\n            val id: Long,\n            val name: String,\n            val value: Int\n          )\n\n          shouldQuery<TestItem>(\n            \"SELECT * FROM TestItems ORDER BY value\",\n            mapper = { row ->\n              TestItem(row.long(\"id\"), row.string(\"name\"), row.int(\"value\"))\n            }\n          ) { actual ->\n            actual.size shouldBe 5\n            actual.forEachIndexed { index, item ->\n              item.name shouldBe \"item_$index\"\n              item.value shouldBe index\n            }\n          }\n        }\n      }\n    }\n\n    class NullableIdAndDescription {\n      var id: Long? = null\n      var description: String? = null\n    }\n\n    test(\"should work with mutable classes\") {\n      stove {\n        mysql {\n          shouldExecute(\"DROP TABLE IF EXISTS Dummies\")\n          shouldExecute(\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS Dummies (\n              id INT AUTO_INCREMENT PRIMARY KEY,\n              description VARCHAR (50) NOT NULL\n            )\n            \"\"\".trimIndent()\n          )\n          shouldExecute(\"INSERT INTO Dummies (description) VALUES ('${testCase.name.name}')\")\n          shouldQuery<NullableIdAndDescription>(\n            \"SELECT * FROM Dummies\",\n            mapper = { row ->\n              val result = NullableIdAndDescription()\n              result.id = row.long(\"id\")\n              result.description = row.string(\"description\")\n              result\n            }\n          ) { actual ->\n            actual.size shouldBeGreaterThan 0\n            actual.first().description shouldBe testCase.name.name\n          }\n        }\n      }\n    }\n\n    test(\"should work with parameterized queries\") {\n      stove {\n        mysql {\n          shouldExecute(\"DROP TABLE IF EXISTS Products\")\n          shouldExecute(\n            \"\"\"\n            CREATE TABLE IF NOT EXISTS Products (\n              id INT AUTO_INCREMENT PRIMARY KEY,\n              name VARCHAR (100) NOT NULL,\n              price DECIMAL(10,2) NOT NULL,\n              category VARCHAR (50) NOT NULL\n            )\n            \"\"\".trimIndent()\n          )\n\n          // Insert with parameters\n          shouldExecute(\n            sql = \"INSERT INTO Products (name, price, category) VALUES (?, ?, ?)\",\n            parameters = listOf(\"Laptop\".param(), 999.99.param(), \"Electronics\".param())\n          )\n\n          shouldExecute(\n            sql = \"INSERT INTO Products (name, price, category) VALUES (?, ?, ?)\",\n            parameters = listOf(\"Mouse\".param(), 29.99.param(), \"Electronics\".param())\n          )\n\n          shouldExecute(\n            sql = \"INSERT INTO Products (name, price, category) VALUES (?, ?, ?)\",\n            parameters = listOf(\"Desk\".param(), 299.99.param(), \"Furniture\".param())\n          )\n\n          data class Product(\n            val id: Long,\n            val name: String,\n            val price: Double,\n            val category: String\n          )\n\n          // Query with parameters\n          shouldQuery<Product>(\n            query = \"SELECT * FROM Products WHERE category = ? ORDER BY price\",\n            parameters = listOf(\"Electronics\".param()),\n            mapper = { row ->\n              Product(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.double(\"price\"),\n                row.string(\"category\")\n              )\n            }\n          ) { actual ->\n            actual.size shouldBe 2\n            actual.first().name shouldBe \"Mouse\"\n            actual.last().name shouldBe \"Laptop\"\n          }\n\n          // Query with multiple parameters\n          shouldQuery<Product>(\n            query = \"SELECT * FROM Products WHERE category = ? AND price > ?\",\n            parameters = listOf(\"Electronics\".param(), 50.0.param()),\n            mapper = { row ->\n              Product(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.double(\"price\"),\n                row.string(\"category\")\n              )\n            }\n          ) { actual ->\n            actual.size shouldBe 1\n            actual.first().apply {\n              name shouldBe \"Laptop\"\n              price shouldBe 999.99\n              category shouldBe \"Electronics\"\n            }\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-mysql/src/test/kotlin/com/trendyol/stove/mysql/TestSystemConfig.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage com.trendyol.stove.mysql\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.slf4j.*\nimport org.testcontainers.containers.MySQLContainer\n\n// ============================================================================\n// Shared components\n// ============================================================================\n\nclass NoOpApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) = Unit\n\n  override suspend fun stop() = Unit\n}\n\nclass InitialMigration : MySqlMigration {\n  private val logger: Logger = LoggerFactory.getLogger(InitialMigration::class.java)\n\n  override val order: Int = 1\n\n  override suspend fun execute(connection: MySqlMigrationContext) {\n    logger.info(\"Executing InitialMigration\")\n    connection.operations.execute(\"DROP TABLE IF EXISTS MigrationHistory\")\n    connection.operations.execute(\n      \"\"\"\n      CREATE TABLE IF NOT EXISTS MigrationHistory (\n        id INT AUTO_INCREMENT PRIMARY KEY,\n        description VARCHAR (50) NOT NULL\n      )\n      \"\"\".trimIndent()\n    )\n    connection.operations.execute(\"INSERT INTO MigrationHistory (description) VALUES ('InitialMigration')\")\n    logger.info(\"InitialMigration executed\")\n  }\n}\n\n// ============================================================================\n// Strategy interface\n// ============================================================================\n\nsealed interface MySqlTestStrategy {\n  val logger: Logger\n\n  suspend fun start()\n\n  suspend fun stop()\n\n  companion object {\n    fun select(): MySqlTestStrategy {\n      val useProvided = System.getenv(\"USE_PROVIDED\")?.toBoolean()\n        ?: System.getProperty(\"useProvided\")?.toBoolean()\n        ?: false\n\n      return if (useProvided) ProvidedMySqlStrategy() else ContainerMySqlStrategy()\n    }\n  }\n}\n\n// ============================================================================\n// Container-based strategy (default)\n// ============================================================================\n\nclass ContainerMySqlStrategy : MySqlTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun start() {\n    logger.info(\"Starting MySQL tests with container mode\")\n\n    val options = MySqlOptions(\n      databaseName = \"testing\",\n      username = \"stove\",\n      password = \"Password12!\",\n      container = MySqlContainerOptions(\n        tag = \"9.6.0\"\n      ),\n      configureExposedConfiguration = { _ -> listOf() }\n    ).migrations {\n      register<InitialMigration>()\n    }\n\n    Stove()\n      .with {\n        mysql { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    Stove.stop()\n    logger.info(\"MySQL container tests completed\")\n  }\n}\n\n// ============================================================================\n// Provided instance strategy\n// ============================================================================\n\nclass ProvidedMySqlStrategy : MySqlTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  private lateinit var externalContainer: MySQLContainer<*>\n\n  override suspend fun start() {\n    logger.info(\"Starting MySQL tests with provided mode\")\n\n    // Start an external container to simulate a provided instance\n    externalContainer = MySQLContainer(\"mysql:9.6.0\").apply {\n      withDatabaseName(\"testing\")\n      withUsername(\"stove\")\n      withPassword(\"Password12!\")\n      start()\n    }\n\n    logger.info(\"External MySQL container started at ${externalContainer.jdbcUrl}\")\n\n    val options = MySqlOptions\n      .provided(\n        jdbcUrl = externalContainer.jdbcUrl,\n        host = externalContainer.host,\n        port = externalContainer.firstMappedPort,\n        databaseName = \"testing\",\n        username = externalContainer.username,\n        password = externalContainer.password,\n        runMigrations = true,\n        cleanup = { sqlOps ->\n          logger.info(\"Running cleanup on provided instance\")\n          sqlOps.execute(\"DROP TABLE IF EXISTS MigrationHistory\")\n        },\n        configureExposedConfiguration = { _ -> listOf() }\n      ).migrations {\n        register<InitialMigration>()\n      }\n\n    Stove()\n      .with {\n        mysql { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    Stove.stop()\n    externalContainer.stop()\n    logger.info(\"MySQL provided tests completed\")\n  }\n}\n\n// ============================================================================\n// Kotest project config - selects strategy based on environment\n// ============================================================================\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val strategy = MySqlTestStrategy.select()\n\n  override suspend fun beforeProject() = strategy.start()\n\n  override suspend fun afterProject() = strategy.stop()\n}\n"
  },
  {
    "path": "lib/stove-mysql/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.mysql.StoveConfig\n"
  },
  {
    "path": "lib/stove-mysql/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.eclipse.jetty\" level=\"INFO\"/>\n    <logger name=\"io.netty\" level=\"INFO\"/>\n</configuration>\n"
  },
  {
    "path": "lib/stove-postgres/api/stove-postgres.api",
    "content": "public final class com/trendyol/stove/postgres/OptionsKt {\n\tpublic static final field DEFAULT_POSTGRES_IMAGE_NAME Ljava/lang/String;\n\tpublic static final fun postgresql-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun postgresql-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun postgresql-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun postgresql-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/postgres/PostgresSqlMigrationContext {\n\tpublic fun <init> (Lcom/trendyol/stove/postgres/PostgresqlOptions;Lcom/trendyol/stove/rdbms/NativeSqlOperations;Lkotlin/jvm/functions/Function2;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/postgres/PostgresqlOptions;\n\tpublic final fun component2 ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic final fun component3 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun copy (Lcom/trendyol/stove/postgres/PostgresqlOptions;Lcom/trendyol/stove/rdbms/NativeSqlOperations;Lkotlin/jvm/functions/Function2;)Lcom/trendyol/stove/postgres/PostgresSqlMigrationContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/postgres/PostgresSqlMigrationContext;Lcom/trendyol/stove/postgres/PostgresqlOptions;Lcom/trendyol/stove/rdbms/NativeSqlOperations;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/trendyol/stove/postgres/PostgresSqlMigrationContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getExecuteAsRoot ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getOperations ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/postgres/PostgresqlOptions;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/postgres/PostgresqlContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/postgres/PostgresqlContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/postgres/PostgresqlContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/postgres/PostgresqlContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic class com/trendyol/stove/postgres/PostgresqlOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/postgres/PostgresqlOptions$Companion;\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/postgres/PostgresqlContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lcom/trendyol/stove/postgres/PostgresqlContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainer ()Lcom/trendyol/stove/postgres/PostgresqlContainerOptions;\n\tpublic fun getDatabaseName ()Ljava/lang/String;\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun getPassword ()Ljava/lang/String;\n\tpublic fun getUsername ()Ljava/lang/String;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/postgres/PostgresqlOptions;\n}\n\npublic final class com/trendyol/stove/postgres/PostgresqlOptions$Companion {\n\tpublic final fun provided (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/postgres/ProvidedPostgresqlOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/postgres/PostgresqlOptions$Companion;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/postgres/ProvidedPostgresqlOptions;\n}\n\npublic final class com/trendyol/stove/postgres/PostgresqlSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field Companion Lcom/trendyol/stove/postgres/PostgresqlSystem$Companion;\n\tpublic field sqlOperations Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic final fun getSqlOperations ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun pause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun setSqlOperations (Lcom/trendyol/stove/rdbms/NativeSqlOperations;)V\n\tpublic final fun shouldExecute (Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun shouldExecute$default (Lcom/trendyol/stove/postgres/PostgresqlSystem;Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/postgres/PostgresqlSystem$Companion {\n\tpublic final fun operations (Lcom/trendyol/stove/postgres/PostgresqlSystem;)Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n}\n\npublic final class com/trendyol/stove/postgres/ProvidedPostgresqlOptions : com/trendyol/stove/postgres/PostgresqlOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic class com/trendyol/stove/postgres/StovePostgresqlContainer : org/testcontainers/postgresql/PostgreSQLContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\n"
  },
  {
    "path": "lib/stove-postgres/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stoveRdbms)\n  api(libs.testcontainers.postgres)\n  api(libs.postgresql)\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.logback.classic)\n}\n\nval testWithProvided = tasks.register<Test>(\"testWithProvided\") {\n  group = \"verification\"\n  description = \"Runs tests with an externally provided PostgreSQL instance\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useProvided\", \"true\")\n  doFirst {\n    println(\"Starting PostgreSQL tests with provided instance...\")\n  }\n}\n\ntasks.test.configure {\n  dependsOn(testWithProvided)\n}\n"
  },
  {
    "path": "lib/stove-postgres/src/main/kotlin/com/trendyol/stove/postgres/Options.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.postgres\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.containers.*\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.rdbms.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.testcontainers.postgresql.PostgreSQLContainer\nimport org.testcontainers.utility.DockerImageName\n\nconst val DEFAULT_POSTGRES_IMAGE_NAME = \"postgres\"\n\nopen class StovePostgresqlContainer(\n  override val imageNameAccess: DockerImageName\n) : PostgreSQLContainer(imageNameAccess),\n  StoveContainer\n\ndata class PostgresqlContainerOptions(\n  override val registry: String = DEFAULT_REGISTRY,\n  override val image: String = DEFAULT_POSTGRES_IMAGE_NAME,\n  override val tag: String = \"latest\",\n  override val compatibleSubstitute: String? = null,\n  override val useContainerFn: UseContainerFn<StovePostgresqlContainer> = { StovePostgresqlContainer(it) },\n  override val containerFn: ContainerFn<StovePostgresqlContainer> = { }\n) : ContainerOptions<StovePostgresqlContainer>\n\n/**\n * Options for configuring the PostgreSQL system in container mode.\n */\n@StoveDsl\nopen class PostgresqlOptions(\n  open val databaseName: String = \"stove\",\n  open val username: String = \"sa\",\n  open val password: String = \"sa\",\n  open val container: PostgresqlContainerOptions = PostgresqlContainerOptions(),\n  open val cleanup: suspend (NativeSqlOperations) -> Unit = {},\n  override val configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<RelationalDatabaseExposedConfiguration>,\n  SupportsMigrations<PostgresSqlMigrationContext, PostgresqlOptions> {\n  override val migrationCollection: MigrationCollection<PostgresSqlMigrationContext> = MigrationCollection()\n\n  companion object {\n    /**\n     * Creates options configured to use an externally provided PostgreSQL instance\n     * instead of a testcontainer.\n     *\n     * @param jdbcUrl The JDBC URL for the PostgreSQL instance\n     * @param host The host of the PostgreSQL instance\n     * @param port The port of the PostgreSQL instance\n     * @param databaseName The database name\n     * @param username The username for authentication\n     * @param password The password for authentication\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      jdbcUrl: String,\n      host: String,\n      port: Int,\n      databaseName: String = \"stove\",\n      username: String = \"sa\",\n      password: String = \"sa\",\n      runMigrations: Boolean = true,\n      cleanup: suspend (NativeSqlOperations) -> Unit = {},\n      configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n    ): ProvidedPostgresqlOptions = ProvidedPostgresqlOptions(\n      config = RelationalDatabaseExposedConfiguration(\n        jdbcUrl = jdbcUrl,\n        host = host,\n        port = port,\n        username = username,\n        password = password\n      ),\n      databaseName = databaseName,\n      username = username,\n      password = password,\n      runMigrations = runMigrations,\n      cleanup = cleanup,\n      configureExposedConfiguration = configureExposedConfiguration\n    )\n  }\n}\n\n/**\n * Options for using an externally provided PostgreSQL instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedPostgresqlOptions(\n  /**\n   * The configuration for the provided PostgreSQL instance.\n   */\n  val config: RelationalDatabaseExposedConfiguration,\n  databaseName: String = \"stove\",\n  username: String = \"sa\",\n  password: String = \"sa\",\n  cleanup: suspend (NativeSqlOperations) -> Unit = {},\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n) : PostgresqlOptions(\n  databaseName = databaseName,\n  username = username,\n  password = password,\n  container = PostgresqlContainerOptions(),\n  cleanup = cleanup,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<RelationalDatabaseExposedConfiguration> {\n  override val providedConfig: RelationalDatabaseExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n\n@StoveDsl\ndata class PostgresSqlMigrationContext(\n  val options: PostgresqlOptions,\n  val operations: NativeSqlOperations,\n  val executeAsRoot: suspend (String) -> Unit\n)\n\n/**\n * Convenience type alias for PostgreSQL migrations.\n *\n * Instead of writing `DatabaseMigration<PostgresSqlMigrationContext>`, use `PostgresqlMigration`:\n * ```kotlin\n * class MyMigration : PostgresqlMigration {\n *   override val order: Int = 1\n *   override suspend fun execute(connection: PostgresSqlMigrationContext) { ... }\n * }\n * ```\n */\ntypealias PostgresqlMigration = DatabaseMigration<PostgresSqlMigrationContext>\n\ninternal class PostgresqlContext(\n  val runtime: SystemRuntime,\n  val options: PostgresqlOptions,\n  val keyName: String? = null\n)\n\ninternal fun Stove.withPostgresql(\n  options: PostgresqlOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(PostgresqlSystem(this, PostgresqlContext(runtime, options)))\n  return this\n}\n\ninternal fun Stove.withPostgresql(\n  key: SystemKey,\n  options: PostgresqlOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(key, PostgresqlSystem(this, PostgresqlContext(runtime, options, keyName = keyDisplayName(key))))\n  return this\n}\n\ninternal fun Stove.postgresql(): PostgresqlSystem =\n  getOrNone<PostgresqlSystem>().getOrElse {\n    throw SystemNotRegisteredException(PostgresqlSystem::class)\n  }\n\ninternal fun Stove.postgresql(key: SystemKey): PostgresqlSystem =\n  getOrNone<PostgresqlSystem>(key).getOrElse {\n    throw SystemNotRegisteredException(PostgresqlSystem::class, \"No PostgresqlSystem registered with key '${keyDisplayName(key)}'\")\n  }\n\n/**\n * Configures PostgreSQL system.\n *\n * For container-based setup:\n * ```kotlin\n * postgresql {\n *   PostgresqlOptions(\n *     databaseName = \"mydb\",\n *     cleanup = { ops -> ops.execute(\"TRUNCATE TABLE ...\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * postgresql {\n *   PostgresqlOptions.provided(\n *     jdbcUrl = \"jdbc:postgresql://localhost:5432/mydb\",\n *     host = \"localhost\",\n *     port = 5432,\n *     username = \"user\",\n *     password = \"pass\",\n *     runMigrations = true,\n *     cleanup = { ops -> ops.execute(\"TRUNCATE TABLE ...\") },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n */\nfun WithDsl.postgresql(\n  configure: () -> PostgresqlOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedPostgresqlOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withDatabaseName(options.databaseName)\n        .withUsername(options.username)\n        .withPassword(options.password)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StovePostgresqlContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withPostgresql(options, runtime)\n}\n\nfun WithDsl.postgresql(\n  key: SystemKey,\n  configure: () -> PostgresqlOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedPostgresqlOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.imageWithTag,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withDatabaseName(options.databaseName)\n        .withUsername(options.username)\n        .withPassword(options.password)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StovePostgresqlContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withPostgresql(key, options, runtime)\n}\n\nsuspend fun ValidationDsl.postgresql(validation: @StoveDsl suspend PostgresqlSystem.() -> Unit): Unit =\n  validation(this.stove.postgresql())\n\nsuspend fun ValidationDsl.postgresql(key: SystemKey, validation: @StoveDsl suspend PostgresqlSystem.() -> Unit): Unit =\n  validation(this.stove.postgresql(key))\n"
  },
  {
    "path": "lib/stove-postgres/src/main/kotlin/com/trendyol/stove/postgres/PostgresqlSystem.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.postgres\n\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.rdbms.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport kotlinx.coroutines.runBlocking\nimport kotliquery.*\nimport org.slf4j.*\n\n/**\n * PostgreSQL database system for testing relational data operations.\n *\n * Provides a DSL for testing PostgreSQL operations:\n * - SQL query execution with typed results\n * - DDL/DML statement execution\n * - Schema migrations\n * - Container pause/unpause for fault injection\n *\n * ## Querying Data\n *\n * ```kotlin\n * postgresql {\n *     // Query with row mapping\n *     shouldQuery(\n *         query = \"SELECT id, name, email FROM users WHERE status = 'active'\",\n *         mapper = { row ->\n *             User(\n *                 id = row.long(\"id\"),\n *                 name = row.string(\"name\"),\n *                 email = row.string(\"email\")\n *             )\n *         }\n *     ) { users ->\n *         users.size shouldBeGreaterThan 0\n *         users.all { it.email.contains(\"@\") } shouldBe true\n *     }\n * }\n * ```\n *\n * ## Executing SQL\n *\n * ```kotlin\n * postgresql {\n *     // Execute DML/DDL statements\n *     shouldExecute(\"INSERT INTO users (name, email) VALUES ('John', 'john@example.com')\")\n *     shouldExecute(\"UPDATE users SET status = 'active' WHERE id = 123\")\n *     shouldExecute(\"DELETE FROM users WHERE id = 123\")\n *\n *     // Create tables\n *     shouldExecute(\"\"\"\n *         CREATE TABLE IF NOT EXISTS orders (\n *             id SERIAL PRIMARY KEY,\n *             user_id BIGINT REFERENCES users(id),\n *             total DECIMAL(10,2),\n *             created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n *         )\n *     \"\"\")\n * }\n * ```\n *\n * ## Fault Injection Testing\n *\n * Test application behavior during database outages:\n *\n * ```kotlin\n * postgresql {\n *     // Pause the database container\n *     pause()\n * }\n *\n * // Test application behavior during outage\n * http {\n *     getResponse(\"/api/health\") { response ->\n *         response.status shouldBe 503\n *     }\n * }\n *\n * postgresql {\n *     // Resume the database\n *     unpause()\n * }\n *\n * // Verify recovery\n * http {\n *     getResponse(\"/api/health\") { response ->\n *         response.status shouldBe 200\n *     }\n * }\n * ```\n *\n * ## Test Workflow Example\n *\n * ```kotlin\n * test(\"should create order and store in database\") {\n *     stove {\n *         // Create order via API\n *         http {\n *             postAndExpectBody<OrderResponse>(\n *                 uri = \"/orders\",\n *                 body = CreateOrderRequest(userId = 123, amount = 99.99).some()\n *             ) { response ->\n *                 response.status shouldBe 201\n *             }\n *         }\n *\n *         // Verify in database\n *         postgresql {\n *             shouldQuery(\n *                 query = \"SELECT * FROM orders WHERE user_id = 123\",\n *                 mapper = { row -> Order(row.long(\"id\"), row.decimal(\"total\")) }\n *             ) { orders ->\n *                 orders shouldHaveSize 1\n *                 orders.first().total shouldBe BigDecimal(\"99.99\")\n *             }\n *         }\n *     }\n * }\n * ```\n *\n * ## Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         postgresql {\n *             PostgresqlOptions(\n *                 configureExposedConfiguration = { cfg ->\n *                     listOf(\n *                         \"spring.datasource.url=${cfg.jdbcUrl}\",\n *                         \"spring.datasource.username=${cfg.username}\",\n *                         \"spring.datasource.password=${cfg.password}\"\n *                     )\n *                 }\n *             ).migrations {\n *                 register<CreateTablesSchema>()\n *                 register<SeedTestData>()\n *             }\n *         }\n *     }\n * ```\n *\n * @property stove The parent test system.\n * @see PostgresqlOptions\n * @see RelationalDatabaseExposedConfiguration\n */\n@StoveDsl\nclass PostgresqlSystem internal constructor(\n  override val stove: Stove,\n  private val postgresContext: PostgresqlContext\n) : PluggedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  @PublishedApi\n  internal lateinit var sqlOperations: NativeSqlOperations\n\n  override val reportSystemName: String = \"PostgreSQL\" + (postgresContext.keyName?.let { \" [$it]\" } ?: \"\")\n\n  private lateinit var exposedConfiguration: RelationalDatabaseExposedConfiguration\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val state: StateStorage<RelationalDatabaseExposedConfiguration> =\n    stove.createStateStorage<RelationalDatabaseExposedConfiguration, PostgresqlSystem>(postgresContext.keyName)\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n    sqlOperations = NativeSqlOperations(database(exposedConfiguration))\n    runMigrationsIfNeeded()\n  }\n\n  override suspend fun stop(): Unit = whenContainer { it.stop() }\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      if (::sqlOperations.isInitialized) {\n        postgresContext.options.cleanup(sqlOperations)\n        sqlOperations.close()\n      }\n      executeWithReuseCheck { stop() }\n    }.recover { logger.warn(\"PostgreSQL stop failed\", it) }\n  }\n\n  override fun configuration(): List<String> =\n    postgresContext.options.configureExposedConfiguration(exposedConfiguration)\n\n  suspend inline fun <reified T : Any> shouldQuery(\n    query: String,\n    parameters: List<Parameter<*>> = emptyList(),\n    crossinline mapper: (Row) -> T,\n    crossinline assertion: (List<T>) -> Unit\n  ): PostgresqlSystem {\n    report(\n      action = \"Query\",\n      input = arrow.core.Some(query.trim()),\n      metadata = mapOf(\"sql\" to query.trim())\n    ) {\n      val results = sqlOperations.select(sql = query, parameters = parameters) { mapper(it) }\n      assertion(results)\n      results\n    }\n    return this\n  }\n\n  suspend fun shouldExecute(sql: String, parameters: List<Parameter<*>> = emptyList()): PostgresqlSystem {\n    report(\n      action = \"Execute SQL\",\n      input = arrow.core.Some(sql.trim()),\n      metadata = mapOf(\"sql\" to sql.trim())\n    ) {\n      val affectedRows = sqlOperations.execute(sql = sql, parameters = parameters)\n      check(affectedRows >= 0) { \"Failed to execute sql: $sql\" }\n      \"$affectedRows row(s) affected\"\n    }\n    return this\n  }\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return PostgresqlSystem\n   */\n  suspend fun pause(): PostgresqlSystem {\n    report(\n      action = \"Pause container\",\n      metadata = mapOf(\"operation\" to \"fault-injection\")\n    ) {\n      withContainerOrWarn(\"pause\") { it.pause() }\n    }\n    return this\n  }\n\n  /**\n   * Unpauses the container. Use with care, as it will unpause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return PostgresqlSystem\n   */\n  suspend fun unpause(): PostgresqlSystem {\n    report(action = \"Unpause container\") {\n      withContainerOrWarn(\"unpause\") { it.unpause() }\n    }\n    return this\n  }\n\n  private suspend fun obtainExposedConfiguration(): RelationalDatabaseExposedConfiguration =\n    when {\n      postgresContext.options is ProvidedPostgresqlOptions -> postgresContext.options.config\n      postgresContext.runtime is StovePostgresqlContainer -> startPostgresContainer(postgresContext.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${postgresContext.runtime::class}\")\n    }\n\n  private suspend fun startPostgresContainer(container: StovePostgresqlContainer): RelationalDatabaseExposedConfiguration =\n    state.capture {\n      container.start()\n      RelationalDatabaseExposedConfiguration(\n        jdbcUrl = container.jdbcUrl,\n        host = container.host,\n        port = container.firstMappedPort,\n        username = container.username,\n        password = container.password\n      )\n    }\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (shouldRunMigrations()) {\n      val executeAsRoot = createExecuteAsRootFn()\n      postgresContext.options.migrationCollection.run(\n        PostgresSqlMigrationContext(postgresContext.options, sqlOperations) { executeAsRoot(it) }\n      )\n    }\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    postgresContext.options is ProvidedPostgresqlOptions -> postgresContext.options.runMigrations\n    postgresContext.runtime is StovePostgresqlContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${postgresContext.runtime::class}\")\n  }\n\n  private fun createExecuteAsRootFn(): suspend (String) -> Unit = when {\n    postgresContext.options is ProvidedPostgresqlOptions -> { sql: String -> sqlOperations.execute(sql) }\n\n    postgresContext.runtime is StovePostgresqlContainer -> { sql: String ->\n      val container = postgresContext.runtime\n      // Use execCommand which works via Docker client directly, supporting both\n      // fresh starts and subsequent runs with reuse (where container isn't \"started\" by testcontainers)\n      container\n        .execCommand(\n          \"/bin/bash\",\n          \"-c\",\n          \"psql -U ${container.username} -d ${container.databaseName} -c \\\"$sql\\\"\"\n        ).let {\n          check(it.exitCode == 0) { \"Failed to execute sql: $sql, reason: ${it.stderr}\" }\n        }\n    }\n\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${postgresContext.runtime::class}\")\n  }\n\n  private fun database(exposedConfiguration: RelationalDatabaseExposedConfiguration): Session = sessionOf(\n    url = exposedConfiguration.jdbcUrl,\n    user = exposedConfiguration.username,\n    password = exposedConfiguration.password\n  )\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StovePostgresqlContainer) -> Unit\n  ): PostgresqlSystem = when (val runtime = postgresContext.runtime) {\n    is StovePostgresqlContainer -> {\n      action(runtime)\n      this\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"$operation() is not supported when using a provided instance\")\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private inline fun whenContainer(action: (StovePostgresqlContainer) -> Unit) {\n    if (postgresContext.runtime is StovePostgresqlContainer) {\n      action(postgresContext.runtime)\n    }\n  }\n\n  companion object {\n    /**\n     * Exposes the [NativeSqlOperations] to the [PostgresqlSystem].\n     * Use this for advanced SQL operations not covered by the DSL.\n     */\n    fun PostgresqlSystem.operations(): NativeSqlOperations = sqlOperations\n  }\n}\n"
  },
  {
    "path": "lib/stove-postgres/src/test/kotlin/com/trendyol/stove/postgres/PostgresqlOptionsTest.kt",
    "content": "package com.trendyol.stove.postgres\n\nimport com.trendyol.stove.rdbms.RelationalDatabaseExposedConfiguration\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\n\nclass PostgresqlOptionsTest :\n  FunSpec({\n\n    test(\"PostgresqlOptions.provided should create ProvidedPostgresqlOptions with correct config\") {\n      val options = PostgresqlOptions.provided(\n        jdbcUrl = \"jdbc:postgresql://localhost:5432/testdb\",\n        host = \"localhost\",\n        port = 5432,\n        databaseName = \"testdb\",\n        username = \"postgres\",\n        password = \"pgpass\",\n        configureExposedConfiguration = { cfg ->\n          listOf(\"spring.datasource.url=${cfg.jdbcUrl}\")\n        }\n      )\n\n      options.providedConfig.jdbcUrl shouldBe \"jdbc:postgresql://localhost:5432/testdb\"\n      options.providedConfig.host shouldBe \"localhost\"\n      options.providedConfig.port shouldBe 5432\n      options.providedConfig.username shouldBe \"postgres\"\n      options.providedConfig.password shouldBe \"pgpass\"\n      options.runMigrationsForProvided shouldBe true\n    }\n\n    test(\"ProvidedPostgresqlOptions should expose correct properties\") {\n      val config = RelationalDatabaseExposedConfiguration(\n        jdbcUrl = \"jdbc:postgresql://remote:5432/db\",\n        host = \"remote\",\n        port = 5432,\n        username = \"user\",\n        password = \"pass\"\n      )\n      val options = ProvidedPostgresqlOptions(\n        config = config,\n        databaseName = \"db\",\n        runMigrations = false,\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.config shouldBe config\n      options.providedConfig shouldBe config\n      options.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"PostgresqlOptions should have sensible defaults\") {\n      val options = object : PostgresqlOptions(\n        configureExposedConfiguration = { _ -> listOf() }\n      ) {}\n\n      options.databaseName shouldBe \"stove\"\n      options.username shouldBe \"sa\"\n      options.password shouldBe \"sa\"\n      options.container shouldNotBe null\n    }\n\n    test(\"PostgresqlContainerOptions should have defaults\") {\n      val opts = PostgresqlContainerOptions()\n      opts.image shouldBe DEFAULT_POSTGRES_IMAGE_NAME\n      opts.tag shouldBe \"latest\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove-postgres/src/test/kotlin/com/trendyol/stove/postgres/PostgresqlSystemTest.kt",
    "content": "package com.trendyol.stove.postgres\n\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.ints.shouldBeGreaterThan\nimport io.kotest.matchers.shouldBe\nimport kotliquery.param\n\n/**\n * PostgreSQL system tests that run against both container-based and provided instances.\n *\n * These tests verify:\n * - Basic CRUD operations work correctly\n * - Migrations are executed properly\n * - The same test code works for both container and provided modes\n *\n * To run with provided instance mode:\n * ```\n * ./gradlew :lib:stove-testing-e2e-rdbms-postgres:test -DuseProvided=true\n * ```\n */\nclass PostgresqlSystemTests :\n  FunSpec({\n\n    data class IdAndDescription(\n      val id: Long,\n      val description: String\n    )\n\n    test(\"migration should create MigrationHistory table\") {\n      stove {\n        postgresql {\n          shouldQuery<IdAndDescription>(\n            \"SELECT * FROM MigrationHistory\",\n            mapper = { row ->\n              IdAndDescription(row.long(\"id\"), row.string(\"description\"))\n            }\n          ) { actual ->\n            actual.size shouldBeGreaterThan 0\n            actual.first() shouldBe IdAndDescription(1, \"InitialMigration\")\n          }\n        }\n      }\n    }\n\n    test(\"should execute DDL and DML statements\") {\n      stove {\n        postgresql {\n          shouldExecute(\n            \"\"\"\n            DROP TABLE IF EXISTS Dummies;\n            CREATE TABLE IF NOT EXISTS Dummies (\n              id serial PRIMARY KEY,\n              description VARCHAR (50) NOT NULL\n            );\n            \"\"\".trimIndent()\n          )\n          shouldExecute(\"INSERT INTO Dummies (description) VALUES ('${testCase.name.name}')\")\n          shouldQuery<IdAndDescription>(\n            \"SELECT * FROM Dummies\",\n            mapper = {\n              IdAndDescription(it.long(\"id\"), it.string(\"description\"))\n            }\n          ) { actual ->\n            actual.size shouldBeGreaterThan 0\n            actual.first().description shouldBe testCase.name.name\n          }\n        }\n      }\n    }\n\n    test(\"should handle multiple inserts and queries\") {\n      stove {\n        postgresql {\n          shouldExecute(\n            \"\"\"\n            DROP TABLE IF EXISTS TestItems;\n            CREATE TABLE IF NOT EXISTS TestItems (\n              id serial PRIMARY KEY,\n              name VARCHAR (100) NOT NULL,\n              value INT NOT NULL\n            );\n            \"\"\".trimIndent()\n          )\n\n          // Insert multiple records\n          repeat(5) { i ->\n            shouldExecute(\"INSERT INTO TestItems (name, value) VALUES ('item_$i', $i)\")\n          }\n\n          // Query and verify\n          data class TestItem(\n            val id: Long,\n            val name: String,\n            val value: Int\n          )\n\n          shouldQuery<TestItem>(\n            \"SELECT * FROM TestItems ORDER BY value\",\n            mapper = { row ->\n              TestItem(row.long(\"id\"), row.string(\"name\"), row.int(\"value\"))\n            }\n          ) { actual ->\n            actual.size shouldBe 5\n            actual.forEachIndexed { index, item ->\n              item.name shouldBe \"item_$index\"\n              item.value shouldBe index\n            }\n          }\n        }\n      }\n    }\n\n    class NullableIdAndDescription {\n      var id: Long? = null\n      var description: String? = null\n    }\n\n    test(\"should work with mutable classes\") {\n      stove {\n        postgresql {\n          shouldExecute(\n            \"\"\"\n            DROP TABLE IF EXISTS Dummies;\n            CREATE TABLE IF NOT EXISTS Dummies (\n              id serial PRIMARY KEY,\n              description VARCHAR (50) NOT NULL\n            );\n            \"\"\".trimIndent()\n          )\n          shouldExecute(\"INSERT INTO Dummies (description) VALUES ('${testCase.name.name}')\")\n          shouldQuery<NullableIdAndDescription>(\n            \"SELECT * FROM Dummies\",\n            mapper = { row ->\n              val result = NullableIdAndDescription()\n              result.id = row.long(\"id\")\n              result.description = row.string(\"description\")\n              result\n            }\n          ) { actual ->\n            actual.size shouldBeGreaterThan 0\n            actual.first().description shouldBe testCase.name.name\n          }\n        }\n      }\n    }\n\n    test(\"should work with parameterized queries\") {\n      stove {\n        postgresql {\n          shouldExecute(\n            \"\"\"\n            DROP TABLE IF EXISTS Users;\n            CREATE TABLE IF NOT EXISTS Users (\n              id serial PRIMARY KEY,\n              name VARCHAR (100) NOT NULL,\n              age INT NOT NULL,\n              email VARCHAR (100) NOT NULL\n            );\n            \"\"\".trimIndent()\n          )\n\n          // Insert with parameters\n          shouldExecute(\n            sql = \"INSERT INTO Users (name, age, email) VALUES (?, ?, ?)\",\n            parameters = listOf(\"Alice\".param(), 30.param(), \"alice@example.com\".param())\n          )\n\n          shouldExecute(\n            sql = \"INSERT INTO Users (name, age, email) VALUES (?, ?, ?)\",\n            parameters = listOf(\"Bob\".param(), 25.param(), \"bob@example.com\".param())\n          )\n\n          data class User(\n            val id: Long,\n            val name: String,\n            val age: Int,\n            val email: String\n          )\n\n          // Query with parameters\n          shouldQuery<User>(\n            query = \"SELECT * FROM Users WHERE age > ? ORDER BY age\",\n            parameters = listOf(20.param()),\n            mapper = { row ->\n              User(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.int(\"age\"),\n                row.string(\"email\")\n              )\n            }\n          ) { actual ->\n            actual.size shouldBe 2\n            actual.first().name shouldBe \"Bob\"\n            actual.last().name shouldBe \"Alice\"\n          }\n\n          // Query with multiple parameters\n          shouldQuery<User>(\n            query = \"SELECT * FROM Users WHERE name = ? AND age = ?\",\n            parameters = listOf(\"Alice\".param(), 30.param()),\n            mapper = { row ->\n              User(\n                row.long(\"id\"),\n                row.string(\"name\"),\n                row.int(\"age\"),\n                row.string(\"email\")\n              )\n            }\n          ) { actual ->\n            actual.size shouldBe 1\n            actual.first().apply {\n              name shouldBe \"Alice\"\n              age shouldBe 30\n              email shouldBe \"alice@example.com\"\n            }\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-postgres/src/test/kotlin/com/trendyol/stove/postgres/TestSystemConfig.kt",
    "content": "package com.trendyol.stove.postgres\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.slf4j.*\nimport org.testcontainers.postgresql.PostgreSQLContainer\n\n// ============================================================================\n// Shared components\n// ============================================================================\n\nclass NoOpApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) = Unit\n\n  override suspend fun stop() = Unit\n}\n\nclass InitialMigration : PostgresqlMigration {\n  private val logger: Logger = LoggerFactory.getLogger(InitialMigration::class.java)\n\n  override val order: Int = 1\n\n  override suspend fun execute(connection: PostgresSqlMigrationContext) {\n    logger.info(\"Executing InitialMigration\")\n    connection.operations.execute(\n      \"\"\"\n      DROP TABLE IF EXISTS MigrationHistory;\n      CREATE TABLE IF NOT EXISTS MigrationHistory (\n        id serial PRIMARY KEY,\n        description VARCHAR (50) NOT NULL\n      );\n      INSERT INTO MigrationHistory (description) VALUES ('InitialMigration');\n      \"\"\".trimIndent()\n    )\n    logger.info(\"InitialMigration executed\")\n  }\n}\n\n// ============================================================================\n// Strategy interface\n// ============================================================================\n\nsealed interface PostgresTestStrategy {\n  val logger: Logger\n\n  suspend fun start()\n\n  suspend fun stop()\n\n  companion object {\n    fun select(): PostgresTestStrategy {\n      val useProvided = System.getenv(\"USE_PROVIDED\")?.toBoolean()\n        ?: System.getProperty(\"useProvided\")?.toBoolean()\n        ?: false\n\n      return if (useProvided) ProvidedPostgresStrategy() else ContainerPostgresStrategy()\n    }\n  }\n}\n\n// ============================================================================\n// Container-based strategy (default)\n// ============================================================================\n\nclass ContainerPostgresStrategy : PostgresTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun start() {\n    logger.info(\"Starting PostgreSQL tests with container mode\")\n\n    val options = PostgresqlOptions(\n      databaseName = \"testing\",\n      configureExposedConfiguration = { _ -> listOf() }\n    ).migrations {\n      register<InitialMigration>()\n    }\n\n    Stove()\n      .with {\n        postgresql { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    logger.info(\"PostgreSQL container tests completed\")\n  }\n}\n\n// ============================================================================\n// Provided instance strategy\n// ============================================================================\n\nclass ProvidedPostgresStrategy : PostgresTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  private lateinit var externalContainer: PostgreSQLContainer\n\n  override suspend fun start() {\n    logger.info(\"Starting PostgreSQL tests with provided mode\")\n\n    // Start an external container to simulate a provided instance\n    externalContainer = PostgreSQLContainer(\"postgres:15-alpine\").apply {\n      withDatabaseName(\"testing\")\n      withUsername(\"postgres\")\n      withPassword(\"postgres\")\n      start()\n    }\n\n    logger.info(\"External PostgreSQL container started at ${externalContainer.jdbcUrl}\")\n\n    val options = PostgresqlOptions\n      .provided(\n        jdbcUrl = externalContainer.jdbcUrl,\n        host = externalContainer.host,\n        port = externalContainer.firstMappedPort,\n        databaseName = \"testing\",\n        username = externalContainer.username,\n        password = externalContainer.password,\n        runMigrations = true,\n        cleanup = { sqlOps ->\n          logger.info(\"Running cleanup on provided instance\")\n          sqlOps.execute(\"DROP TABLE IF EXISTS MigrationHistory CASCADE\")\n        },\n        configureExposedConfiguration = { _ -> listOf() }\n      ).migrations {\n        register<InitialMigration>()\n      }\n\n    Stove()\n      .with {\n        postgresql { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    externalContainer.stop()\n    logger.info(\"PostgreSQL provided tests completed\")\n  }\n}\n\n// ============================================================================\n// Kotest project config - selects strategy based on environment\n// ============================================================================\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val strategy = PostgresTestStrategy.select()\n\n  override suspend fun beforeProject() = strategy.start()\n\n  override suspend fun afterProject() = strategy.stop()\n}\n"
  },
  {
    "path": "lib/stove-postgres/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.postgres.StoveConfig\n"
  },
  {
    "path": "lib/stove-postgres/src/test/resources/logback.xml",
    "content": "<configuration>\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>\n        </encoder>\n    </appender>\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.eclipse.jetty\" level=\"INFO\"/>\n    <logger name=\"io.netty\" level=\"INFO\"/>\n</configuration>\n"
  },
  {
    "path": "lib/stove-rdbms/api/stove-rdbms.api",
    "content": "public final class com/trendyol/stove/rdbms/NativeSqlOperations : java/lang/AutoCloseable {\n\tpublic fun <init> (Lkotliquery/Session;)V\n\tpublic fun close ()V\n\tpublic final fun execute (Ljava/lang/String;Ljava/util/List;)I\n\tpublic static synthetic fun execute$default (Lcom/trendyol/stove/rdbms/NativeSqlOperations;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)I\n\tpublic final fun select (Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function1;)Ljava/util/List;\n\tpublic static synthetic fun select$default (Lcom/trendyol/stove/rdbms/NativeSqlOperations;Ljava/lang/String;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/util/List;\n}\n\npublic abstract class com/trendyol/stove/rdbms/RelationalDatabaseContext {\n\tpublic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lkotlin/jvm/functions/Function1;)V\n\tpublic final fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n}\n\npublic final class com/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()I\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getHost ()Ljava/lang/String;\n\tpublic final fun getJdbcUrl ()Ljava/lang/String;\n\tpublic final fun getPassword ()Ljava/lang/String;\n\tpublic final fun getPort ()I\n\tpublic final fun getUsername ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract class com/trendyol/stove/rdbms/RelationalDatabaseSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field Companion Lcom/trendyol/stove/rdbms/RelationalDatabaseSystem$Companion;\n\tprotected field exposedConfiguration Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tprotected field sqlOperations Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tprotected fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/rdbms/RelationalDatabaseContext;)V\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tprotected abstract fun database (Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;)Lkotliquery/Session;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tprotected final fun getContext ()Lcom/trendyol/stove/rdbms/RelationalDatabaseContext;\n\tprotected final fun getExposedConfiguration ()Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic final fun getInternalSqlOperations ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tprotected abstract fun getProvidedConfig ()Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tprotected final fun getSqlOperations ()Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n\tpublic final fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tprotected final fun setExposedConfiguration (Lcom/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration;)V\n\tpublic final fun setInternalSqlOperations (Lcom/trendyol/stove/rdbms/NativeSqlOperations;)V\n\tprotected final fun setSqlOperations (Lcom/trendyol/stove/rdbms/NativeSqlOperations;)V\n\tpublic final fun shouldExecute (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/rdbms/RelationalDatabaseSystem$Companion {\n\tpublic final fun operations (Lcom/trendyol/stove/rdbms/RelationalDatabaseSystem;)Lcom/trendyol/stove/rdbms/NativeSqlOperations;\n}\n\n"
  },
  {
    "path": "lib/stove-rdbms/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(libs.testcontainers.jdbc)\n  api(libs.kotliquery)\n  api(libs.h2Database)\n  testImplementation(libs.mockito.kotlin)\n}\n"
  },
  {
    "path": "lib/stove-rdbms/src/main/kotlin/com/trendyol/stove/rdbms/NativeSqlOperations.kt",
    "content": "package com.trendyol.stove.rdbms\n\nimport kotliquery.*\n\nclass NativeSqlOperations(\n  private val session: Session\n) : AutoCloseable {\n  fun execute(\n    sql: String,\n    parameters: List<Parameter<*>> = emptyList()\n  ): Int = session\n    .run(queryOf(sql, *parameters.toTypedArray()).asUpdate)\n\n  fun <T> select(\n    sql: String,\n    parameters: List<Parameter<*>> = emptyList(),\n    rowMapper: (Row) -> T\n  ): List<T> = session\n    .run(queryOf(sql, *parameters.toTypedArray()).map(rowMapper).asList)\n\n  override fun close() {\n    session.close()\n  }\n}\n"
  },
  {
    "path": "lib/stove-rdbms/src/main/kotlin/com/trendyol/stove/rdbms/RelationalDatabaseContext.kt",
    "content": "package com.trendyol.stove.rdbms\n\nimport com.trendyol.stove.system.abstractions.SystemRuntime\n\nabstract class RelationalDatabaseContext(\n  val runtime: SystemRuntime,\n  val configureExposedConfiguration: (RelationalDatabaseExposedConfiguration) -> List<String>\n)\n"
  },
  {
    "path": "lib/stove-rdbms/src/main/kotlin/com/trendyol/stove/rdbms/RelationalDatabaseExposedConfiguration.kt",
    "content": "package com.trendyol.stove.rdbms\n\nimport com.trendyol.stove.system.abstractions.ExposedConfiguration\n\ndata class RelationalDatabaseExposedConfiguration(\n  val jdbcUrl: String,\n  val host: String,\n  val port: Int,\n  val password: String,\n  val username: String\n) : ExposedConfiguration\n"
  },
  {
    "path": "lib/stove-rdbms/src/main/kotlin/com/trendyol/stove/rdbms/RelationalDatabaseSystem.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.rdbms\n\nimport arrow.core.Some\nimport com.trendyol.stove.containers.StoveContainer\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.reporting.Reports\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport kotlinx.coroutines.runBlocking\nimport kotliquery.*\nimport org.slf4j.*\nimport org.testcontainers.containers.JdbcDatabaseContainer\n\n@Suppress(\"UNCHECKED_CAST\", \"MemberVisibilityCanBePrivate\")\n@StoveDsl\nabstract class RelationalDatabaseSystem<SELF : RelationalDatabaseSystem<SELF>> protected constructor(\n  final override val stove: Stove,\n  protected val context: RelationalDatabaseContext\n) : PluggedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  protected lateinit var exposedConfiguration: RelationalDatabaseExposedConfiguration\n\n  protected lateinit var sqlOperations: NativeSqlOperations\n  private val state: StateStorage<RelationalDatabaseExposedConfiguration> =\n    stove.createStateStorage<RelationalDatabaseExposedConfiguration, RelationalDatabaseSystem<SELF>>()\n\n  protected abstract fun database(exposedConfiguration: RelationalDatabaseExposedConfiguration): Session\n\n  override suspend fun run() {\n    exposedConfiguration = when (val runtime = context.runtime) {\n      is StoveContainer -> {\n        val jdbcContainer = runtime as JdbcDatabaseContainer<*>\n        state.capture {\n          jdbcContainer.start()\n          RelationalDatabaseExposedConfiguration(\n            jdbcUrl = jdbcContainer.jdbcUrl,\n            host = jdbcContainer.host,\n            port = jdbcContainer.firstMappedPort,\n            password = jdbcContainer.password,\n            username = jdbcContainer.username\n          )\n        }\n      }\n\n      is ProvidedRuntime -> {\n        getProvidedConfig()\n      }\n\n      else -> {\n        throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n      }\n    }\n    sqlOperations = NativeSqlOperations(database(exposedConfiguration))\n  }\n\n  /**\n   * Gets the provided configuration from subclass options.\n   * Subclasses should override this to provide their specific provided config.\n   */\n  protected abstract fun getProvidedConfig(): RelationalDatabaseExposedConfiguration\n\n  override fun configuration(): List<String> = context.configureExposedConfiguration(exposedConfiguration)\n\n  suspend inline fun <reified T : Any> shouldQuery(\n    query: String,\n    crossinline mapper: (Row) -> T,\n    crossinline assertion: (List<T>) -> Unit\n  ): SELF {\n    report(\n      action = \"Query\",\n      input = Some(query.trim()),\n      metadata = mapOf(\"sql\" to query.trim())\n    ) {\n      val results = internalSqlOperations.select(query) { mapper(it) }\n      assertion(results)\n      \"${results.size} row(s) returned\"\n    }\n    return this as SELF\n  }\n\n  suspend fun shouldExecute(sql: String): SELF {\n    report(\n      action = \"Execute SQL\",\n      input = Some(sql.trim()),\n      metadata = mapOf(\"sql\" to sql.trim())\n    ) {\n      val affectedRows = internalSqlOperations.execute(sql)\n      check(affectedRows >= 0) { \"Failed to execute sql: $sql\" }\n      \"$affectedRows row(s) affected\"\n    }\n    return this as SELF\n  }\n\n  override suspend fun stop() {\n    if (context.runtime is StoveContainer) {\n      (context.runtime as JdbcDatabaseContainer<*>).stop()\n    }\n  }\n\n  override fun close(): Unit =\n    runBlocking {\n      Try {\n        // Note: cleanup is handled in subclass via options\n        sqlOperations.close()\n        executeWithReuseCheck { stop() }\n      }.recover {\n        val containerInfo = when (val runtime = context.runtime) {\n          is JdbcDatabaseContainer<*> -> runtime.containerName\n          is ProvidedRuntime -> \"provided instance\"\n          else -> \"unknown runtime\"\n        }\n        logger.warn(\"got an error while stopping the container $containerInfo \")\n      }.let { }\n    }\n\n  @PublishedApi\n  internal var internalSqlOperations: NativeSqlOperations\n    get() = sqlOperations\n    set(value) {\n      sqlOperations = value\n    }\n\n  companion object {\n    /**\n     * Exposes the [NativeSqlOperations] to the [RelationalDatabaseSystem].\n     * Use this for advanced SQL operations not covered by the DSL.\n     */\n    @Suppress(\"unused\")\n    fun RelationalDatabaseSystem<*>.operations(): NativeSqlOperations = this.sqlOperations\n  }\n}\n"
  },
  {
    "path": "lib/stove-rdbms/src/test/kotlin/com/trendyol/stove/rdbms/RelationalDatabaseContextTest.kt",
    "content": "package com.trendyol.stove.rdbms\n\nimport com.trendyol.stove.system.abstractions.ProvidedRuntime\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass RelationalDatabaseContextTest :\n  FunSpec({\n    test(\"should hold runtime and configuration mapper\") {\n      val context = object : RelationalDatabaseContext(\n        runtime = ProvidedRuntime,\n        configureExposedConfiguration = { cfg -> listOf(\"jdbc=${cfg.jdbcUrl}\") }\n      ) {}\n\n      context.runtime shouldBe ProvidedRuntime\n      context.configureExposedConfiguration(\n        RelationalDatabaseExposedConfiguration(\"jdbc:h2:mem:test\", \"localhost\", 0, \"\", \"sa\")\n      ) shouldBe listOf(\"jdbc=jdbc:h2:mem:test\")\n    }\n  })\n"
  },
  {
    "path": "lib/stove-rdbms/src/test/kotlin/com/trendyol/stove/rdbms/RelationalDatabaseSystemTest.kt",
    "content": "package com.trendyol.stove.rdbms\n\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ProvidedRuntime\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.runBlocking\nimport kotliquery.sessionOf\n\nclass RelationalDatabaseSystemTest :\n  FunSpec({\n    test(\"run should initialize configuration and sql operations for provided runtime\") {\n      val stove = Stove()\n      val system = TestRelationalDatabaseSystem(stove)\n\n      runBlocking { system.run() }\n\n      system.configuration().joinToString() shouldContain \"jdbc:h2:mem:testdb\"\n      system.internalSqlOperations shouldNotBe null\n    }\n\n    test(\"shouldExecute and shouldQuery should use sql operations\") {\n      val stove = Stove()\n      val system = TestRelationalDatabaseSystem(stove)\n\n      runBlocking {\n        system.run()\n        system.shouldExecute(\n          \"\"\"\n          CREATE TABLE IF NOT EXISTS users (\n            id INT PRIMARY KEY,\n            name VARCHAR(50)\n          )\n          \"\"\".trimIndent()\n        )\n        system.shouldExecute(\"INSERT INTO users (id, name) VALUES (1, 'alice')\")\n\n        data class User(\n          val id: Int,\n          val name: String\n        )\n\n        system.shouldQuery<User>(\n          query = \"SELECT * FROM users\",\n          mapper = { row -> User(row.int(\"id\"), row.string(\"name\")) }\n        ) { users ->\n          users.size shouldBe 1\n          users.first().name shouldBe \"alice\"\n        }\n      }\n    }\n  })\n\nprivate class TestRelationalDatabaseSystem(\n  stove: Stove\n) : RelationalDatabaseSystem<TestRelationalDatabaseSystem>(\n  stove = stove,\n  context = object : RelationalDatabaseContext(\n    runtime = ProvidedRuntime,\n    configureExposedConfiguration = { cfg ->\n      listOf(\"jdbcUrl=${cfg.jdbcUrl}\")\n    }\n  ) {}\n) {\n  private val config = RelationalDatabaseExposedConfiguration(\n    jdbcUrl = \"jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1\",\n    host = \"localhost\",\n    port = 0,\n    username = \"sa\",\n    password = \"\"\n  )\n\n  override fun database(exposedConfiguration: RelationalDatabaseExposedConfiguration) = sessionOf(\n    url = exposedConfiguration.jdbcUrl,\n    user = exposedConfiguration.username,\n    password = exposedConfiguration.password\n  )\n\n  override fun getProvidedConfig(): RelationalDatabaseExposedConfiguration = config\n}\n"
  },
  {
    "path": "lib/stove-redis/api/stove-redis.api",
    "content": "public final class com/trendyol/stove/redis/ProvidedRedisOptions : com/trendyol/stove/redis/RedisOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/redis/RedisExposedConfiguration;ILjava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/redis/RedisExposedConfiguration;ILjava/lang/String;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/redis/RedisExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/redis/RedisExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic final class com/trendyol/stove/redis/RedisContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/redis/RedisContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/redis/RedisContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/redis/RedisContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/redis/RedisContext {\n\tpublic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/redis/RedisOptions;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/redis/RedisOptions;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic final fun component2 ()Lcom/trendyol/stove/redis/RedisOptions;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun copy (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/redis/RedisOptions;Ljava/lang/String;)Lcom/trendyol/stove/redis/RedisContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/redis/RedisContext;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/redis/RedisOptions;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/redis/RedisContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getKeyName ()Ljava/lang/String;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/redis/RedisOptions;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/redis/RedisExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()I\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/redis/RedisExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/redis/RedisExposedConfiguration;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/redis/RedisExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getDatabase ()Ljava/lang/String;\n\tpublic final fun getHost ()Ljava/lang/String;\n\tpublic final fun getPassword ()Ljava/lang/String;\n\tpublic final fun getPort ()I\n\tpublic final fun getRedisUri ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/redis/RedisMigrationContext {\n\tpublic fun <init> (Lio/lettuce/core/api/StatefulRedisConnection;Lcom/trendyol/stove/redis/RedisOptions;)V\n\tpublic final fun component1 ()Lio/lettuce/core/api/StatefulRedisConnection;\n\tpublic final fun component2 ()Lcom/trendyol/stove/redis/RedisOptions;\n\tpublic final fun copy (Lio/lettuce/core/api/StatefulRedisConnection;Lcom/trendyol/stove/redis/RedisOptions;)Lcom/trendyol/stove/redis/RedisMigrationContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/redis/RedisMigrationContext;Lio/lettuce/core/api/StatefulRedisConnection;Lcom/trendyol/stove/redis/RedisOptions;ILjava/lang/Object;)Lcom/trendyol/stove/redis/RedisMigrationContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getConnection ()Lio/lettuce/core/api/StatefulRedisConnection;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/redis/RedisOptions;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic class com/trendyol/stove/redis/RedisOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/redis/RedisOptions$Companion;\n\tpublic fun <init> (ILjava/lang/String;Lcom/trendyol/stove/redis/RedisContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (ILjava/lang/String;Lcom/trendyol/stove/redis/RedisContainerOptions;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainer ()Lcom/trendyol/stove/redis/RedisContainerOptions;\n\tpublic fun getDatabase ()I\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun getPassword ()Ljava/lang/String;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/redis/RedisOptions;\n}\n\npublic final class com/trendyol/stove/redis/RedisOptions$Companion {\n\tpublic final fun provided (Ljava/lang/String;ILjava/lang/String;IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/redis/ProvidedRedisOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/redis/RedisOptions$Companion;Ljava/lang/String;ILjava/lang/String;IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/redis/ProvidedRedisOptions;\n}\n\npublic final class com/trendyol/stove/redis/RedisOptionsKt {\n\tpublic static final fun redis-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun redis-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun redis-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun redis-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/redis/RedisSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic static final field Companion Lcom/trendyol/stove/redis/RedisSystem$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/redis/RedisContext;)V\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun pause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/redis/RedisSystem$Companion {\n\tpublic final fun client (Lcom/trendyol/stove/redis/RedisSystem;)Lio/lettuce/core/RedisClient;\n}\n\npublic class com/trendyol/stove/redis/StoveRedisContainer : com/redis/testcontainers/RedisContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\n"
  },
  {
    "path": "lib/stove-redis/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(libs.lettuce.core)\n  api(libs.testcontainers.redis)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n}\n\nval testWithProvided = tasks.register<Test>(\"testWithProvided\") {\n  group = \"verification\"\n  description = \"Runs tests with an externally provided Redis instance\"\n  testClassesDirs = sourceSets.test.get().output.classesDirs\n  classpath = sourceSets.test.get().runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"useProvided\", \"true\")\n  doFirst {\n    println(\"Starting Redis tests with provided instance...\")\n  }\n}\n\ntasks.test.configure {\n  dependsOn(testWithProvided)\n}\n"
  },
  {
    "path": "lib/stove-redis/src/main/kotlin/com/trendyol/stove/redis/RedisOptions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.redis\n\nimport arrow.core.getOrElse\nimport com.redis.testcontainers.RedisContainer\nimport com.trendyol.stove.containers.*\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport io.lettuce.core.RedisClient\nimport io.lettuce.core.api.StatefulRedisConnection\nimport org.testcontainers.utility.DockerImageName\n\nopen class StoveRedisContainer(\n  override val imageNameAccess: DockerImageName\n) : RedisContainer(imageNameAccess),\n  StoveContainer\n\ndata class RedisContainerOptions(\n  override val registry: String = DEFAULT_REGISTRY,\n  override val image: String = RedisContainer.DEFAULT_IMAGE_NAME.unversionedPart,\n  override val tag: String = RedisContainer.DEFAULT_TAG,\n  override val compatibleSubstitute: String? = null,\n  override val useContainerFn: UseContainerFn<StoveRedisContainer> = { StoveRedisContainer(it) },\n  override val containerFn: ContainerFn<StoveRedisContainer> = { }\n) : ContainerOptions<StoveRedisContainer>\n\n/**\n * Context provided to Redis migrations.\n * Contains the Redis connection and options for performing setup operations.\n *\n * @property connection The Redis connection for executing commands\n * @property options The Redis system options\n */\n@StoveDsl\ndata class RedisMigrationContext(\n  val connection: StatefulRedisConnection<String, String>,\n  val options: RedisOptions\n)\n\n/**\n * Convenience type alias for Redis migrations.\n *\n * Instead of writing `DatabaseMigration<RedisMigrationContext>`, use `RedisMigration`:\n * ```kotlin\n * class MyMigration : RedisMigration {\n *   override val order: Int = 1\n *   override suspend fun execute(connection: RedisMigrationContext) { ... }\n * }\n * ```\n */\ntypealias RedisMigration = DatabaseMigration<RedisMigrationContext>\n\n/**\n * Options for configuring the Redis system in container mode.\n */\n@StoveDsl\nopen class RedisOptions(\n  open val database: Int = 8,\n  open val password: String = \"password\",\n  open val container: RedisContainerOptions = RedisContainerOptions(),\n  open val cleanup: suspend (RedisClient) -> Unit = {},\n  override val configureExposedConfiguration: (RedisExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<RedisExposedConfiguration>,\n  SupportsMigrations<RedisMigrationContext, RedisOptions> {\n  override val migrationCollection: MigrationCollection<RedisMigrationContext> = MigrationCollection()\n\n  companion object {\n    /**\n     * Creates options configured to use an externally provided Redis instance\n     * instead of a testcontainer.\n     *\n     * @param host The Redis host\n     * @param port The Redis port\n     * @param password The Redis password\n     * @param database The Redis database number\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      host: String,\n      port: Int,\n      password: String,\n      database: Int = 8,\n      runMigrations: Boolean = true,\n      cleanup: suspend (RedisClient) -> Unit = {},\n      configureExposedConfiguration: (RedisExposedConfiguration) -> List<String>\n    ): ProvidedRedisOptions = ProvidedRedisOptions(\n      config = RedisExposedConfiguration(\n        host = host,\n        port = port,\n        redisUri = \"redis://$host:$port\",\n        database = database.toString(),\n        password = password\n      ),\n      database = database,\n      password = password,\n      runMigrations = runMigrations,\n      cleanup = cleanup,\n      configureExposedConfiguration = configureExposedConfiguration\n    )\n  }\n}\n\n/**\n * Options for using an externally provided Redis instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedRedisOptions(\n  /**\n   * The configuration for the provided Redis instance.\n   */\n  val config: RedisExposedConfiguration,\n  database: Int = 8,\n  password: String = \"password\",\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  cleanup: suspend (RedisClient) -> Unit = {},\n  configureExposedConfiguration: (RedisExposedConfiguration) -> List<String>\n) : RedisOptions(\n  database = database,\n  password = password,\n  container = RedisContainerOptions(),\n  cleanup = cleanup,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<RedisExposedConfiguration> {\n  override val providedConfig: RedisExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n\n@StoveDsl\ndata class RedisExposedConfiguration(\n  val host: String,\n  val port: Int,\n  val redisUri: String,\n  val database: String,\n  val password: String\n) : ExposedConfiguration\n\n@StoveDsl\ndata class RedisContext(\n  val runtime: SystemRuntime,\n  val options: RedisOptions,\n  val keyName: String? = null\n)\n\n/**\n * Configures Redis system.\n *\n * For container-based setup:\n * ```kotlin\n * redis {\n *   RedisOptions(\n *     database = 8,\n *     password = \"password\",\n *     cleanup = { client -> client.connect().sync().flushall() },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * redis {\n *   RedisOptions.provided(\n *     host = \"localhost\",\n *     port = 6379,\n *     password = \"password\",\n *     database = 8,\n *     cleanup = { client -> client.connect().sync().flushall() },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   )\n * }\n * ```\n */\nfun WithDsl.redis(\n  configure: () -> RedisOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedRedisOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.image,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withCommand(\"redis-server\", \"--requirepass\", options.password)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveRedisContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withRedis(options, runtime)\n}\n\nfun WithDsl.redis(\n  key: SystemKey,\n  configure: () -> RedisOptions\n): Stove {\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedRedisOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.container.image,\n      options.container.registry,\n      options.container.compatibleSubstitute\n    ) { dockerImageName ->\n      options.container\n        .useContainerFn(dockerImageName)\n        .withCommand(\"redis-server\", \"--requirepass\", options.password)\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveRedisContainer }\n        .apply(options.container.containerFn)\n    }\n  }\n  return stove.withRedis(key, options, runtime)\n}\n\nsuspend fun ValidationDsl.redis(validation: suspend RedisSystem.() -> Unit): Unit = validation(this.stove.redis())\n\nsuspend fun ValidationDsl.redis(key: SystemKey, validation: suspend RedisSystem.() -> Unit): Unit = validation(this.stove.redis(key))\n\ninternal fun Stove.redis(): RedisSystem =\n  getOrNone<RedisSystem>().getOrElse {\n    throw SystemNotRegisteredException(RedisSystem::class)\n  }\n\ninternal fun Stove.redis(key: SystemKey): RedisSystem =\n  getOrNone<RedisSystem>(key).getOrElse {\n    throw SystemNotRegisteredException(RedisSystem::class, \"No RedisSystem registered with key '${keyDisplayName(key)}'\")\n  }\n\ninternal fun Stove.withRedis(\n  options: RedisOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(RedisSystem(this, RedisContext(runtime, options)))\n  return this\n}\n\ninternal fun Stove.withRedis(\n  key: SystemKey,\n  options: RedisOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(key, RedisSystem(this, RedisContext(runtime, options, keyName = keyDisplayName(key))))\n  return this\n}\n"
  },
  {
    "path": "lib/stove-redis/src/main/kotlin/com/trendyol/stove/redis/RedisSystem.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.redis\n\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport io.lettuce.core.*\nimport kotlinx.coroutines.runBlocking\nimport org.slf4j.*\nimport reactor.core.publisher.Mono\n\n/**\n * Redis cache/data store system for testing caching operations.\n *\n * Provides access to a Lettuce Redis client for testing Redis operations.\n * Use [client] to access the underlying [RedisClient] for all Redis operations.\n *\n * ## Accessing Redis Client\n *\n * All Redis operations are performed through the Lettuce client:\n *\n * ```kotlin\n * redis {\n *     val conn = client().connect().sync()\n *\n *     // Set a simple string value\n *     conn.set(\"user:123\", \"John Doe\")\n *\n *     // Set with expiration (TTL in seconds)\n *     conn.setex(\"session:abc\", 3600, sessionData)\n *\n *     // Get a value\n *     val value = conn.get(\"user:123\")\n *     value shouldBe \"John Doe\"\n *\n *     // Check key existence\n *     val exists = conn.exists(\"user:123\")\n *     exists shouldBe 1L\n *\n *     // Delete a key\n *     conn.del(\"user:123\")\n *\n *     // Get TTL\n *     val ttl = conn.ttl(\"session:abc\")\n *     ttl shouldBeGreaterThan 0\n * }\n * ```\n *\n * ## Fault Injection Testing\n *\n * Test application behavior during cache outages:\n *\n * ```kotlin\n * redis {\n *     pause()  // Simulate Redis outage\n * }\n *\n * // Test application graceful degradation\n * http {\n *     get<UserResponse>(\"/users/123\") { user ->\n *         // Should still work (from database fallback)\n *         user.name shouldBe \"John\"\n *     }\n * }\n *\n * redis {\n *     unpause()  // Restore Redis\n * }\n * ```\n *\n * ## Test Workflow Example\n *\n * ```kotlin\n * test(\"should cache user after first request\") {\n *     stove {\n *         // Ensure cache is empty\n *         redis {\n *             val conn = client().connect().sync()\n *             conn.get(\"user:cache:123\") shouldBe null\n *         }\n *\n *         // First request - cache miss, loads from DB\n *         http {\n *             get<UserResponse>(\"/users/123\") { user ->\n *                 user.name shouldBe \"John\"\n *             }\n *         }\n *\n *         // Verify user is now cached\n *         redis {\n *             val conn = client().connect().sync()\n *             val cached = conn.get(\"user:cache:123\")\n *             cached shouldNotBe null\n *         }\n *     }\n * }\n * ```\n *\n * ## Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         redis {\n *             RedisSystemOptions(\n *                 database = 0,\n *                 configureExposedConfiguration = { cfg ->\n *                     listOf(\n *                         \"spring.redis.host=${cfg.host}\",\n *                         \"spring.redis.port=${cfg.port}\"\n *                     )\n *                 }\n *             )\n *         }\n *     }\n * ```\n *\n * @property stove The parent test system.\n * @see RedisSystemOptions\n * @see RedisExposedConfiguration\n */\n@StoveDsl\nclass RedisSystem(\n  override val stove: Stove,\n  private val context: RedisContext\n) : PluggedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  private lateinit var client: RedisClient\n\n  override val reportSystemName: String = \"Redis\" + (context.keyName?.let { \" [$it]\" } ?: \"\")\n\n  private lateinit var exposedConfiguration: RedisExposedConfiguration\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val state: StateStorage<RedisExposedConfiguration> =\n    stove.createStateStorage<RedisExposedConfiguration, RedisSystem>(context.keyName)\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n    client = createClient(exposedConfiguration)\n    runMigrationsIfNeeded()\n  }\n\n  override suspend fun stop(): Unit = whenContainer { it.stop() }\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      if (::client.isInitialized) {\n        context.options.cleanup(client)\n        client.shutdown()\n      }\n      executeWithReuseCheck { stop() }\n    }.recover { logger.warn(\"Redis client shutdown failed\", it) }\n  }\n\n  override fun configuration(): List<String> = context.options.configureExposedConfiguration(exposedConfiguration)\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return RedisSystem\n   */\n  suspend fun pause(): RedisSystem {\n    report(\n      action = \"Pause container\",\n      metadata = mapOf(\"operation\" to \"fault-injection\")\n    ) {\n      withContainerOrWarn(\"pause\") { it.pause() }\n    }\n    return this\n  }\n\n  /**\n   * Unpauses the container. Use with care, as it will unpause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return RedisSystem\n   */\n  suspend fun unpause(): RedisSystem {\n    report(action = \"Unpause container\") {\n      withContainerOrWarn(\"unpause\") { it.unpause() }\n    }\n    return this\n  }\n\n  private suspend fun obtainExposedConfiguration(): RedisExposedConfiguration =\n    when {\n      context.options is ProvidedRedisOptions -> context.options.config\n      context.runtime is StoveRedisContainer -> startRedisContainer(context.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n    }\n\n  private suspend fun startRedisContainer(container: StoveRedisContainer): RedisExposedConfiguration =\n    state.capture {\n      container.start()\n      RedisExposedConfiguration(\n        host = container.host,\n        port = container.firstMappedPort,\n        redisUri = container.redisURI,\n        database = context.options.database.toString(),\n        password = context.options.password\n      )\n    }\n\n  private fun createClient(config: RedisExposedConfiguration): RedisClient =\n    RedisClient.create(\n      RedisURI.create(config.host, config.port).apply {\n        setCredentialsProvider { Mono.just(RedisCredentials.just(null, config.password)) }\n      }\n    )\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StoveRedisContainer) -> Unit\n  ): RedisSystem = when (val runtime = context.runtime) {\n    is StoveRedisContainer -> {\n      action(runtime)\n      this\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"$operation() is not supported when using a provided instance\")\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private inline fun whenContainer(action: (StoveRedisContainer) -> Unit) {\n    if (context.runtime is StoveRedisContainer) {\n      action(context.runtime)\n    }\n  }\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (shouldRunMigrations()) {\n      client.connect().use { connection ->\n        context.options.migrationCollection.run(RedisMigrationContext(connection, context.options))\n      }\n    }\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    context.options is ProvidedRedisOptions -> context.options.runMigrations\n    context.runtime is StoveRedisContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n  }\n\n  private fun isInitialized(): Boolean = ::client.isInitialized\n\n  companion object {\n    fun RedisSystem.client(): RedisClient {\n      if (!isInitialized()) throw SystemNotInitializedException(RedisSystem::class)\n      return client\n    }\n  }\n}\n"
  },
  {
    "path": "lib/stove-redis/src/test/kotlin/com/trendyol/stove/redis/RedisOptionsTest.kt",
    "content": "package com.trendyol.stove.redis\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\n\nclass RedisOptionsTest :\n  FunSpec({\n\n    test(\"RedisExposedConfiguration should hold connection details\") {\n      val cfg = RedisExposedConfiguration(\n        host = \"localhost\",\n        port = 6379,\n        redisUri = \"redis://localhost:6379\",\n        database = \"8\",\n        password = \"secret\"\n      )\n\n      cfg.host shouldBe \"localhost\"\n      cfg.port shouldBe 6379\n      cfg.redisUri shouldBe \"redis://localhost:6379\"\n      cfg.database shouldBe \"8\"\n      cfg.password shouldBe \"secret\"\n    }\n\n    test(\"RedisOptions.provided should create ProvidedRedisOptions with correct config\") {\n      val options = RedisOptions.provided(\n        host = \"redis-host\",\n        port = 6379,\n        password = \"pass\",\n        database = 5,\n        configureExposedConfiguration = { cfg ->\n          listOf(\"redis.host=${cfg.host}\", \"redis.port=${cfg.port}\")\n        }\n      )\n\n      options.providedConfig.host shouldBe \"redis-host\"\n      options.providedConfig.port shouldBe 6379\n      options.providedConfig.redisUri shouldBe \"redis://redis-host:6379\"\n      options.providedConfig.database shouldBe \"5\"\n      options.providedConfig.password shouldBe \"pass\"\n      options.runMigrationsForProvided shouldBe true\n    }\n\n    test(\"ProvidedRedisOptions should expose correct properties\") {\n      val config = RedisExposedConfiguration(\n        host = \"remote\",\n        port = 6380,\n        redisUri = \"redis://remote:6380\",\n        database = \"3\",\n        password = \"p\"\n      )\n      val options = ProvidedRedisOptions(\n        config = config,\n        database = 3,\n        password = \"p\",\n        runMigrations = false,\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n      options.config shouldBe config\n      options.providedConfig shouldBe config\n      options.runMigrationsForProvided shouldBe false\n    }\n\n    test(\"RedisOptions should have sensible defaults\") {\n      val options = object : RedisOptions(\n        configureExposedConfiguration = { _ -> listOf() }\n      ) {}\n\n      options.database shouldBe 8\n      options.password shouldBe \"password\"\n      options.container shouldNotBe null\n    }\n  })\n"
  },
  {
    "path": "lib/stove-redis/src/test/kotlin/com/trendyol/stove/redis/RedisSystemTests.kt",
    "content": "package com.trendyol.stove.redis\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.redis.RedisSystem.Companion.client\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport io.kotest.core.spec.style.ShouldSpec\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.future.await\nimport org.slf4j.*\nimport org.testcontainers.containers.GenericContainer\nimport org.testcontainers.utility.DockerImageName\n\n// ============================================================================\n// Shared components\n// ============================================================================\n\nclass NoOpApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) = Unit\n\n  override suspend fun stop() = Unit\n}\n\n// ============================================================================\n// Strategy interface\n// ============================================================================\n\nsealed interface RedisTestStrategy {\n  val logger: Logger\n\n  suspend fun start()\n\n  suspend fun stop()\n\n  companion object {\n    fun select(): RedisTestStrategy {\n      val useProvided = System.getenv(\"USE_PROVIDED\")?.toBoolean()\n        ?: System.getProperty(\"useProvided\")?.toBoolean()\n        ?: false\n\n      return if (useProvided) ProvidedRedisStrategy() else ContainerRedisStrategy()\n    }\n  }\n}\n\n// ============================================================================\n// Container-based strategy (default)\n// ============================================================================\n\nclass ContainerRedisStrategy : RedisTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override suspend fun start() {\n    logger.info(\"Starting Redis tests with container mode\")\n\n    val options = RedisOptions(\n      container = RedisContainerOptions(),\n      configureExposedConfiguration = { _ -> listOf() }\n    )\n\n    Stove()\n      .with {\n        redis { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    logger.info(\"Redis container tests completed\")\n  }\n}\n\n// ============================================================================\n// Provided instance strategy\n// ============================================================================\n\nclass ProvidedRedisStrategy : RedisTestStrategy {\n  override val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  private lateinit var externalContainer: GenericContainer<*>\n\n  override suspend fun start() {\n    logger.info(\"Starting Redis tests with provided mode\")\n\n    // Start an external container to simulate a provided instance\n    externalContainer = GenericContainer(DockerImageName.parse(\"redis:7-alpine\"))\n      .withExposedPorts(6379)\n      .withCommand(\"redis-server\", \"--requirepass\", \"password\")\n      .apply { start() }\n\n    logger.info(\"External Redis container started at ${externalContainer.host}:${externalContainer.firstMappedPort}\")\n\n    val options = RedisOptions\n      .provided(\n        host = externalContainer.host,\n        port = externalContainer.firstMappedPort,\n        password = \"password\",\n        runMigrations = true,\n        cleanup = { client ->\n          logger.info(\"Running cleanup on provided instance\")\n          // Clean up test data if needed\n        },\n        configureExposedConfiguration = { _ -> listOf() }\n      )\n\n    Stove()\n      .with {\n        redis { options }\n        applicationUnderTest(NoOpApplication())\n      }.run()\n  }\n\n  override suspend fun stop() {\n    com.trendyol.stove.system.Stove\n      .stop()\n    externalContainer.stop()\n    logger.info(\"Redis provided tests completed\")\n  }\n}\n\n// ============================================================================\n// Kotest project config - selects strategy based on environment\n// ============================================================================\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  private val strategy = RedisTestStrategy.select()\n\n  override suspend fun beforeProject() = strategy.start()\n\n  override suspend fun afterProject() = strategy.stop()\n}\n\n// ============================================================================\n// Tests\n// ============================================================================\n\nclass RedisSystemTests :\n  ShouldSpec({\n\n    should(\"work\") {\n      stove {\n        redis {\n          client()\n            .connect()\n            .async()\n            .ping()\n            .await() shouldBe \"PONG\"\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "lib/stove-redis/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.redis.StoveConfig\n"
  },
  {
    "path": "lib/stove-tracing/api/stove-tracing.api",
    "content": "public abstract class com/trendyol/stove/tracing/OTLPReceiverError : java/lang/Exception {\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V\n}\n\npublic final class com/trendyol/stove/tracing/OTLPReceiverError$StartupFailed : com/trendyol/stove/tracing/OTLPReceiverError {\n\tpublic fun <init> (ILjava/io/IOException;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()Ljava/io/IOException;\n\tpublic final fun copy (ILjava/io/IOException;)Lcom/trendyol/stove/tracing/OTLPReceiverError$StartupFailed;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/tracing/OTLPReceiverError$StartupFailed;ILjava/io/IOException;ILjava/lang/Object;)Lcom/trendyol/stove/tracing/OTLPReceiverError$StartupFailed;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCause ()Ljava/io/IOException;\n\tpublic synthetic fun getCause ()Ljava/lang/Throwable;\n\tpublic final fun getPort ()I\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/tracing/OTLPSpanReceiver {\n\tpublic fun <init> (Lcom/trendyol/stove/tracing/StoveTraceCollector;I)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/tracing/StoveTraceCollector;IILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getEndpoint ()Ljava/lang/String;\n\tpublic final fun start ()Larrow/core/Either;\n\tpublic final fun stop ()V\n}\n\npublic final class com/trendyol/stove/tracing/StoveTraceCollector {\n\tpublic fun <init> ()V\n\tpublic final fun addSpanListener (Lcom/trendyol/stove/reporting/SpanEventListener;)V\n\tpublic final fun clear (Ljava/lang/String;)V\n\tpublic final fun clearAll ()V\n\tpublic final fun clearForTest (Ljava/lang/String;)V\n\tpublic final fun getAllTraces ()Ljava/util/Map;\n\tpublic final fun getFailedSpans (Ljava/lang/String;)Ljava/util/List;\n\tpublic final fun getTestId (Ljava/lang/String;)Ljava/lang/String;\n\tpublic final fun getTrace (Ljava/lang/String;)Ljava/util/List;\n\tpublic final fun getTraceTree (Ljava/lang/String;)Lcom/trendyol/stove/tracing/SpanNode;\n\tpublic final fun getTracesForTest (Ljava/lang/String;)Ljava/util/List;\n\tpublic final fun hasFailures (Ljava/lang/String;)Z\n\tpublic final fun record (Lcom/trendyol/stove/tracing/SpanInfo;)V\n\tpublic final fun recordAll (Ljava/util/Collection;)V\n\tpublic final fun registerTrace (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun removeSpanListener (Lcom/trendyol/stove/reporting/SpanEventListener;)V\n\tpublic final fun spanCount (Ljava/lang/String;)I\n\tpublic final fun totalSpanCount ()I\n\tpublic final fun traceCount ()I\n\tpublic final fun waitForSpans (Ljava/lang/String;IJ)Ljava/util/List;\n\tpublic static synthetic fun waitForSpans$default (Lcom/trendyol/stove/tracing/StoveTraceCollector;Ljava/lang/String;IJILjava/lang/Object;)Ljava/util/List;\n}\n\npublic final class com/trendyol/stove/tracing/TraceReportBuilder {\n\tpublic static final field DEFAULT_ERROR_MESSAGE Ljava/lang/String;\n\tpublic static final field INSTANCE Lcom/trendyol/stove/tracing/TraceReportBuilder;\n\tpublic final fun buildFullReport ()Ljava/lang/String;\n\tpublic final fun shouldEnrichFailures (Lcom/trendyol/stove/system/StoveOptions;)Z\n}\n\npublic final class com/trendyol/stove/tracing/TraceValidationDsl {\n\tpublic fun <init> (Lcom/trendyol/stove/tracing/StoveTraceCollector;Ljava/lang/String;)V\n\tpublic final fun executionTimeShouldBeGreaterThan-LRDsOJo (J)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun executionTimeShouldBeLessThan-LRDsOJo (J)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun findSpan (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/tracing/SpanInfo;\n\tpublic final fun findSpanByName (Ljava/lang/String;)Lcom/trendyol/stove/tracing/SpanInfo;\n\tpublic final fun getFailedSpanCount ()I\n\tpublic final fun getFailedSpans ()Ljava/util/List;\n\tpublic final fun getSpanCount ()I\n\tpublic final fun getTotalDuration-UwyO8pc ()J\n\tpublic final fun renderSummary ()Ljava/lang/String;\n\tpublic final fun renderTree ()Ljava/lang/String;\n\tpublic final fun shouldContainSpan (Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldContainSpanMatching (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldHaveFailedSpan (Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldHaveSpanWithAttribute (Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldHaveSpanWithAttributeContaining (Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldNotContainSpan (Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldNotHaveFailedSpans ()Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun spanCountShouldBe (I)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun spanCountShouldBeAtLeast (I)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun spanCountShouldBeAtMost (I)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun spanTree ()Lcom/trendyol/stove/tracing/SpanNode;\n}\n\npublic final class com/trendyol/stove/tracing/TracingConstants {\n\tpublic static final field DEFAULT_MAX_SPANS_PER_TRACE I\n\tpublic static final field DEFAULT_OTLP_GRPC_PORT I\n\tpublic static final field DEFAULT_OTLP_HTTP_PORT I\n\tpublic static final field DEFAULT_SPAN_POLL_INTERVAL_MS J\n\tpublic static final field DEFAULT_SPAN_WAIT_TIMEOUT_MS J\n\tpublic static final field INSTANCE Lcom/trendyol/stove/tracing/TracingConstants;\n\tpublic static final field MAX_STACK_TRACE_LINES I\n\tpublic static final field NANOS_TO_MILLIS J\n\tpublic static final field OTEL_EXCEPTION_EVENT_NAME Ljava/lang/String;\n\tpublic static final field OTEL_EXCEPTION_MESSAGE_ATTRIBUTE Ljava/lang/String;\n\tpublic static final field OTEL_EXCEPTION_STACKTRACE_ATTRIBUTE Ljava/lang/String;\n\tpublic static final field OTEL_EXCEPTION_TYPE_ATTRIBUTE Ljava/lang/String;\n\tpublic static final field OTEL_SERVICE_NAME_ATTRIBUTE Ljava/lang/String;\n\tpublic static final field OTEL_STATUS_CODE_ERROR I\n\tpublic static final field SERVER_SHUTDOWN_TIMEOUT_SECONDS J\n\tpublic static final field STOVE_TRACING_PORT_ENV Ljava/lang/String;\n\tpublic static final field STRAGGLER_WAIT_TIME_MS J\n\tpublic final fun getGRPC_INTERNAL_SPAN_PATTERNS ()Ljava/util/List;\n}\n\npublic abstract interface annotation class com/trendyol/stove/tracing/TracingDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/tracing/TracingOptions {\n\tpublic fun <init> ()V\n\tpublic final fun copy ()Lcom/trendyol/stove/tracing/TracingOptions;\n\tpublic final fun disabled ()Lcom/trendyol/stove/tracing/TracingOptions;\n\tpublic final fun enableSpanReceiver (Ljava/lang/Integer;)Lcom/trendyol/stove/tracing/TracingOptions;\n\tpublic static synthetic fun enableSpanReceiver$default (Lcom/trendyol/stove/tracing/TracingOptions;Ljava/lang/Integer;ILjava/lang/Object;)Lcom/trendyol/stove/tracing/TracingOptions;\n\tpublic final fun enabled ()Lcom/trendyol/stove/tracing/TracingOptions;\n\tpublic final fun getEnabled ()Z\n\tpublic final fun getMaxSpansPerTrace ()I\n\tpublic final fun getSpanCollectionTimeout-UwyO8pc ()J\n\tpublic final fun getSpanFilter ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getSpanReceiverEnabled ()Z\n\tpublic final fun getSpanReceiverPort ()I\n\tpublic final fun maxSpansPerTrace (I)Lcom/trendyol/stove/tracing/TracingOptions;\n\tpublic final fun spanCollectionTimeout-LRDsOJo (J)Lcom/trendyol/stove/tracing/TracingOptions;\n\tpublic final fun spanFilter (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/tracing/TracingOptions;\n}\n\npublic final class com/trendyol/stove/tracing/TracingSystem : com/trendyol/stove/reporting/SpanListenerRegistry, com/trendyol/stove/reporting/TraceProvider, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware {\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/tracing/TracingSystemOptions;)V\n\tpublic fun addSpanListener (Lcom/trendyol/stove/reporting/SpanEventListener;)V\n\tpublic fun close ()V\n\tpublic final fun currentContext ()Larrow/core/Option;\n\tpublic final fun endTrace ()V\n\tpublic final fun ensureTraceStarted (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic fun getTraceVisualizationForCurrentTest (J)Larrow/core/Option;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun validation ()Larrow/core/Option;\n\tpublic final fun validation (Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n}\n\npublic final class com/trendyol/stove/tracing/TracingSystemKt {\n\tpublic static final fun tracing-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun tracing-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/system/Stove;\n\tpublic static synthetic fun tracing-ypJx7X8$default (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun tracingSystem-71YW2E0 (Lcom/trendyol/stove/system/Stove;)Lcom/trendyol/stove/tracing/TracingSystem;\n}\n\npublic final class com/trendyol/stove/tracing/TracingSystemOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lcom/trendyol/stove/tracing/TracingOptions;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/tracing/TracingOptions;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/tracing/TracingOptions;\n\tpublic final fun copy (Lcom/trendyol/stove/tracing/TracingOptions;)Lcom/trendyol/stove/tracing/TracingSystemOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/tracing/TracingSystemOptions;Lcom/trendyol/stove/tracing/TracingOptions;ILjava/lang/Object;)Lcom/trendyol/stove/tracing/TracingSystemOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getTracingOptions ()Lcom/trendyol/stove/tracing/TracingOptions;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/tracing/TracingValidationScope {\n\tpublic fun <init> (Lcom/trendyol/stove/tracing/TraceContext;Lcom/trendyol/stove/tracing/TraceValidationDsl;Lcom/trendyol/stove/tracing/StoveTraceCollector;)V\n\tpublic final fun executionTimeShouldBeGreaterThan-LRDsOJo (J)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun executionTimeShouldBeLessThan-LRDsOJo (J)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun findSpan (Lkotlin/jvm/functions/Function1;)Larrow/core/Option;\n\tpublic final fun findSpanByName (Ljava/lang/String;)Larrow/core/Option;\n\tpublic final fun getAllTraceVisualizations ()Ljava/util/List;\n\tpublic final fun getCollector ()Lcom/trendyol/stove/tracing/StoveTraceCollector;\n\tpublic final fun getCtx ()Lcom/trendyol/stove/tracing/TraceContext;\n\tpublic final fun getFailedSpanCount ()I\n\tpublic final fun getFailedSpans ()Ljava/util/List;\n\tpublic final fun getRootSpanId ()Ljava/lang/String;\n\tpublic final fun getSpanCount ()I\n\tpublic final fun getTestId ()Ljava/lang/String;\n\tpublic final fun getTotalDuration-UwyO8pc ()J\n\tpublic final fun getTraceId ()Ljava/lang/String;\n\tpublic final fun getTraceVisualization ()Lcom/trendyol/stove/tracing/TraceVisualization;\n\tpublic final fun renderSummary ()Ljava/lang/String;\n\tpublic final fun renderTree ()Ljava/lang/String;\n\tpublic final fun shouldContainSpan (Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldContainSpanMatching (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldHaveFailedSpan (Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldHaveSpanWithAttribute (Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldHaveSpanWithAttributeContaining (Ljava/lang/String;Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldNotContainSpan (Ljava/lang/String;)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun shouldNotHaveFailedSpans ()Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun spanCountShouldBe (I)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun spanCountShouldBeAtLeast (I)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun spanCountShouldBeAtMost (I)Lcom/trendyol/stove/tracing/TraceValidationDsl;\n\tpublic final fun spanTree ()Larrow/core/Option;\n\tpublic final fun toTraceparent ()Ljava/lang/String;\n\tpublic final fun waitForSpans (IJ)Ljava/util/List;\n\tpublic static synthetic fun waitForSpans$default (Lcom/trendyol/stove/tracing/TracingValidationScope;IJILjava/lang/Object;)Ljava/util/List;\n}\n\n"
  },
  {
    "path": "lib/stove-tracing/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  implementation(libs.kotlinx.core)\n  implementation(libs.kotlinx.slf4j)\n\n  // OTLP gRPC protocol support for receiving spans from OTel Java Agent\n  implementation(libs.opentelemetry.proto)\n  implementation(libs.io.grpc)\n  implementation(libs.io.grpc.stub)\n  implementation(libs.io.grpc.protobuf)\n  implementation(libs.io.grpc.netty)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.logback.classic)\n}\n"
  },
  {
    "path": "lib/stove-tracing/src/main/kotlin/com/trendyol/stove/tracing/Constants.kt",
    "content": "package com.trendyol.stove.tracing\n\n/**\n * Constants used throughout the tracing module.\n * Centralizes magic numbers and configuration defaults.\n */\nobject TracingConstants {\n  /** Default gRPC port for OTLP protocol */\n  const val DEFAULT_OTLP_GRPC_PORT = 4317\n\n  /** Environment variable name for OTLP port (set by Gradle configuration) */\n  const val STOVE_TRACING_PORT_ENV = \"STOVE_TRACING_PORT\"\n\n  /** Default HTTP port for OTLP protocol */\n  const val DEFAULT_OTLP_HTTP_PORT = 4318\n\n  /** Nanoseconds to milliseconds conversion factor */\n  const val NANOS_TO_MILLIS = 1_000_000L\n\n  /** Default polling interval when waiting for spans (milliseconds) */\n  const val DEFAULT_SPAN_POLL_INTERVAL_MS = 50L\n\n  /** Default timeout when waiting for spans (milliseconds) */\n  const val DEFAULT_SPAN_WAIT_TIMEOUT_MS = 2000L\n\n  /** Additional wait time for straggler spans after first span arrives (milliseconds) */\n  const val STRAGGLER_WAIT_TIME_MS = 200L\n\n  /** Server shutdown grace period (seconds) */\n  const val SERVER_SHUTDOWN_TIMEOUT_SECONDS = 5L\n\n  /** Default maximum spans per trace to prevent memory issues */\n  const val DEFAULT_MAX_SPANS_PER_TRACE = 1000\n\n  /** Maximum stack trace lines to display in trace trees */\n  const val MAX_STACK_TRACE_LINES = 3\n\n  /** OpenTelemetry status code for ERROR */\n  const val OTEL_STATUS_CODE_ERROR = 2\n\n  /** Service name attribute key in OpenTelemetry */\n  const val OTEL_SERVICE_NAME_ATTRIBUTE = \"service.name\"\n\n  /** gRPC internal span patterns to filter out */\n  val GRPC_INTERNAL_SPAN_PATTERNS = listOf(\n    \"TraceService/Export\",\n    \"opentelemetry.proto.collector\"\n  )\n\n  /** OpenTelemetry exception event name */\n  const val OTEL_EXCEPTION_EVENT_NAME = \"exception\"\n\n  /** OpenTelemetry exception type attribute key */\n  const val OTEL_EXCEPTION_TYPE_ATTRIBUTE = \"exception.type\"\n\n  /** OpenTelemetry exception message attribute key */\n  const val OTEL_EXCEPTION_MESSAGE_ATTRIBUTE = \"exception.message\"\n\n  /** OpenTelemetry exception stacktrace attribute key */\n  const val OTEL_EXCEPTION_STACKTRACE_ATTRIBUTE = \"exception.stacktrace\"\n}\n"
  },
  {
    "path": "lib/stove-tracing/src/main/kotlin/com/trendyol/stove/tracing/OTLPSpanReceiver.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport arrow.core.Either\nimport arrow.core.left\nimport arrow.core.right\nimport com.google.protobuf.ByteString\nimport io.grpc.Server\nimport io.grpc.ServerBuilder\nimport io.grpc.Status\nimport io.grpc.stub.StreamObserver\nimport io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\nimport io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\nimport io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc\nimport io.opentelemetry.proto.trace.v1.ResourceSpans\nimport io.opentelemetry.proto.trace.v1.ScopeSpans\nimport org.slf4j.LoggerFactory\nimport java.io.IOException\nimport java.util.concurrent.TimeUnit\nimport java.util.concurrent.locks.ReentrantLock\nimport kotlin.concurrent.withLock\n\n/**\n * OTLP-compatible span receiver that collects spans from the application under test via gRPC.\n *\n * This uses gRPC protocol which is the default for OpenTelemetry Java Agent and avoids\n * classloader isolation issues since the agent communicates via protocol rather than shared classes.\n *\n * Example app configuration (OpenTelemetry Java Agent):\n * ```\n * -Dotel.traces.exporter=otlp\n * -Dotel.exporter.otlp.protocol=grpc\n * -Dotel.exporter.otlp.endpoint=http://localhost:4317\n * -Dotel.service.name=my-service\n * ```\n *\n * Inspired by TestSpanCollector from beholder-otel-extension.\n */\nclass OTLPSpanReceiver(\n  private val collector: StoveTraceCollector,\n  private val port: Int = TracingConstants.DEFAULT_OTLP_GRPC_PORT\n) {\n  private val logger = LoggerFactory.getLogger(OTLPSpanReceiver::class.java)\n  private val lock = ReentrantLock()\n  private var server: Server? = null\n\n  @Volatile private var running = false\n\n  val endpoint: String get() = \"http://localhost:$port\"\n\n  fun start(): Either<OTLPReceiverError, Unit> = lock.withLock {\n    try {\n      if (running) {\n        return Unit.right()\n      }\n\n      server = ServerBuilder\n        .forPort(port)\n        .addService(TraceServiceImpl(collector, logger))\n        .build()\n        .start()\n\n      running = true\n      logger.info(\"[StoveOtlp] Started OTLP gRPC collector on port {}\", port)\n      Unit.right()\n    } catch (e: IOException) {\n      OTLPReceiverError.StartupFailed(port, e).left()\n    }\n  }\n\n  fun stop(): Unit = lock.withLock {\n    if (!running || server == null) {\n      return\n    }\n\n    try {\n      server?.shutdown()\n      val terminated = server?.awaitTermination(\n        TracingConstants.SERVER_SHUTDOWN_TIMEOUT_SECONDS,\n        TimeUnit.SECONDS\n      ) ?: false\n\n      if (!terminated) {\n        server?.shutdownNow()\n      }\n    } catch (_: InterruptedException) {\n      server?.shutdownNow()\n      Thread.currentThread().interrupt()\n    }\n\n    running = false\n    logger.info(\"[StoveOtlp] Stopped OTLP gRPC collector\")\n  }\n}\n\n/**\n * gRPC service implementation that receives OTLP trace data.\n */\nprivate class TraceServiceImpl(\n  private val collector: StoveTraceCollector,\n  private val logger: org.slf4j.Logger\n) : TraceServiceGrpc.TraceServiceImplBase() {\n  override fun export(\n    request: ExportTraceServiceRequest,\n    responseObserver: StreamObserver<ExportTraceServiceResponse>\n  ) {\n    try {\n      val spans = extractSpansFromRequest(request)\n      spans.forEach { collector.record(it) }\n\n      if (spans.isNotEmpty()) {\n        logger.info(\n          \"[StoveOtlp] Received {} spans from service '{}'\",\n          spans.size,\n          spans.firstOrNull()?.serviceName ?: \"unknown\"\n        )\n      }\n      logger.debug(\"[StoveOtlp] Processed {} spans total\", spans.size)\n\n      responseObserver.onNext(ExportTraceServiceResponse.getDefaultInstance())\n      responseObserver.onCompleted()\n    } catch (e: IOException) {\n      logger.error(\"[StoveOtlp] IO error processing spans\", e)\n      responseObserver.onError(\n        Status.INTERNAL\n          .withDescription(\"Failed to process spans: ${e.message}\")\n          .withCause(e)\n          .asException()\n      )\n    } catch (e: IllegalArgumentException) {\n      logger.warn(\"[StoveOtlp] Invalid span data received\", e)\n      responseObserver.onError(\n        Status.INVALID_ARGUMENT\n          .withDescription(\"Invalid span data: ${e.message}\")\n          .withCause(e)\n          .asException()\n      )\n    } catch (e: IllegalStateException) {\n      logger.warn(\"[StoveOtlp] Unexpected state during span processing\", e)\n      responseObserver.onError(\n        Status.FAILED_PRECONDITION\n          .withDescription(\"Unexpected state: ${e.message}\")\n          .withCause(e)\n          .asException()\n      )\n    }\n  }\n\n  private fun extractSpansFromRequest(request: ExportTraceServiceRequest): List<SpanInfo> =\n    request.resourceSpansList\n      .flatMap { resourceSpans -> extractSpansFromResource(resourceSpans) }\n      .filterNot { isInternalGrpcSpan(it.operationName) }\n\n  private fun extractSpansFromResource(resourceSpans: ResourceSpans): List<SpanInfo> {\n    val serviceName = extractServiceName(resourceSpans)\n    return resourceSpans.scopeSpansList\n      .flatMap { scopeSpans -> extractSpansFromScope(scopeSpans, serviceName) }\n  }\n\n  private fun extractServiceName(resourceSpans: ResourceSpans): String =\n    resourceSpans.resource\n      ?.attributesList\n      ?.find { it.key == TracingConstants.OTEL_SERVICE_NAME_ATTRIBUTE }\n      ?.value\n      ?.stringValue\n      ?: \"unknown\"\n\n  private fun extractSpansFromScope(scopeSpans: ScopeSpans, serviceName: String): List<SpanInfo> =\n    scopeSpans.spansList.map { span ->\n      SpanInfo(\n        traceId = span.traceId.toHex(),\n        spanId = span.spanId.toHex(),\n        parentSpanId = if (span.parentSpanId.isEmpty) null else span.parentSpanId.toHex(),\n        operationName = span.name,\n        serviceName = serviceName,\n        startTimeNanos = span.startTimeUnixNano,\n        endTimeNanos = span.endTimeUnixNano,\n        status = when (span.status.codeValue) {\n          TracingConstants.OTEL_STATUS_CODE_ERROR -> SpanStatus.ERROR\n          else -> SpanStatus.OK\n        },\n        attributes = span.attributesList.associate { kv ->\n          kv.key to extractAttributeValue(kv.value)\n        },\n        exception = extractExceptionFromSpan(span)\n      )\n    }\n\n  private fun extractExceptionFromSpan(\n    span: io.opentelemetry.proto.trace.v1.Span\n  ): ExceptionInfo? {\n    // First try to extract exception from events (preferred - contains full details)\n    val exceptionEvent = span.eventsList.find { event ->\n      event.name == TracingConstants.OTEL_EXCEPTION_EVENT_NAME\n    }\n\n    if (exceptionEvent != null) {\n      val attributes = exceptionEvent.attributesList.associate { kv ->\n        kv.key to extractAttributeValue(kv.value)\n      }\n      val exceptionType = attributes[TracingConstants.OTEL_EXCEPTION_TYPE_ATTRIBUTE] ?: \"Exception\"\n      val exceptionMessage = attributes[TracingConstants.OTEL_EXCEPTION_MESSAGE_ATTRIBUTE] ?: \"\"\n      val stackTrace = attributes[TracingConstants.OTEL_EXCEPTION_STACKTRACE_ATTRIBUTE]\n        ?.split(\"\\n\")\n        ?.map { it.trim() }\n        ?.filter { it.isNotEmpty() }\n        ?: emptyList()\n\n      return ExceptionInfo(exceptionType, exceptionMessage, stackTrace)\n    }\n\n    // Fallback to status message if error status but no exception event\n    if (span.status.codeValue == TracingConstants.OTEL_STATUS_CODE_ERROR &&\n      span.status.message.isNotEmpty()\n    ) {\n      return ExceptionInfo(\"Error\", span.status.message, emptyList())\n    }\n\n    return null\n  }\n\n  private fun extractAttributeValue(value: io.opentelemetry.proto.common.v1.AnyValue): String = when {\n    value.hasStringValue() -> value.stringValue\n\n    value.hasIntValue() -> value.intValue.toString()\n\n    value.hasBoolValue() -> value.boolValue.toString()\n\n    value.hasDoubleValue() -> value.doubleValue.toString()\n\n    value.hasArrayValue() -> value.arrayValue.valuesList.joinToString(prefix = \"[\", postfix = \"]\") {\n      formatJsonValue(it)\n    }\n\n    value.hasKvlistValue() -> value.kvlistValue.valuesList.joinToString(prefix = \"{\", postfix = \"}\") { kv ->\n      \"\\\"${escapeJson(kv.key)}\\\":${formatJsonValue(kv.value)}\"\n    }\n\n    value.hasBytesValue() -> value.bytesValue.toHex()\n\n    else -> \"\"\n  }\n\n  private fun formatJsonValue(value: io.opentelemetry.proto.common.v1.AnyValue): String =\n    if (value.hasStringValue()) {\n      \"\\\"${escapeJson(value.stringValue)}\\\"\"\n    } else {\n      extractAttributeValue(value)\n    }\n\n  private fun escapeJson(value: String): String =\n    value.replace(\"\\\\\", \"\\\\\\\\\").replace(\"\\\"\", \"\\\\\\\"\")\n\n  private fun isInternalGrpcSpan(spanName: String): Boolean =\n    TracingConstants.GRPC_INTERNAL_SPAN_PATTERNS.any { pattern ->\n      spanName.contains(pattern)\n    }\n\n  private fun ByteString.toHex(): String {\n    if (isEmpty) return \"\"\n    return toByteArray().joinToString(\"\") { \"%02x\".format(it) }\n  }\n}\n\n/**\n * Errors that can occur during OTLP receiver operations.\n */\nsealed class OTLPReceiverError(\n  message: String,\n  cause: Throwable? = null\n) : Exception(message, cause) {\n  data class StartupFailed(\n    val port: Int,\n    override val cause: IOException\n  ) : OTLPReceiverError(\"Failed to start OTLP gRPC collector on port $port\", cause)\n}\n"
  },
  {
    "path": "lib/stove-tracing/src/main/kotlin/com/trendyol/stove/tracing/StoveTraceCollector.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport com.trendyol.stove.reporting.SpanEventListener\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.CopyOnWriteArrayList\n\nclass StoveTraceCollector {\n  private val logger = org.slf4j.LoggerFactory.getLogger(StoveTraceCollector::class.java)\n  private val spans = ConcurrentHashMap<String, CopyOnWriteArrayList<SpanInfo>>()\n  private val traceToTest = ConcurrentHashMap<String, String>()\n  private val spanListeners = CopyOnWriteArrayList<SpanEventListener>()\n\n  /** Register a listener to receive span events */\n  fun addSpanListener(listener: SpanEventListener) {\n    spanListeners.add(listener)\n  }\n\n  /** Remove a previously registered span listener */\n  fun removeSpanListener(listener: SpanEventListener) {\n    spanListeners.remove(listener)\n  }\n\n  fun registerTrace(traceId: String, testId: String) {\n    traceToTest[traceId] = testId\n    spans.computeIfAbsent(traceId) { CopyOnWriteArrayList() }\n  }\n\n  fun record(span: SpanInfo) {\n    spans.computeIfAbsent(span.traceId) { CopyOnWriteArrayList() }.add(span)\n    spanListeners.forEach {\n      runCatching { it.onSpanRecorded(span) }.onFailure { e -> logger.warn(\"Span listener failed on onSpanRecorded\", e) }\n    }\n  }\n\n  fun recordAll(spansToRecord: Collection<SpanInfo>) {\n    spansToRecord.forEach { record(it) }\n  }\n\n  fun getTrace(traceId: String): List<SpanInfo> =\n    spans[traceId]?.toList() ?: emptyList()\n\n  fun getTraceTree(traceId: String): SpanNode? =\n    SpanTree.build(getTrace(traceId))\n\n  fun getTracesForTest(testId: String): List<String> =\n    traceToTest.filterValues { it == testId }.keys.toList()\n\n  fun getTestId(traceId: String): String? =\n    traceToTest[traceId]\n\n  fun getAllTraces(): Map<String, List<SpanInfo>> =\n    spans.mapValues { it.value.toList() }\n\n  fun getFailedSpans(traceId: String): List<SpanInfo> =\n    getTrace(traceId).filter { it.isFailed }\n\n  fun hasFailures(traceId: String): Boolean =\n    getTrace(traceId).any { it.isFailed }\n\n  fun clear(traceId: String) {\n    spans.remove(traceId)\n    traceToTest.remove(traceId)\n  }\n\n  fun clearForTest(testId: String) {\n    val traceIds = getTracesForTest(testId)\n    traceIds.forEach { clear(it) }\n  }\n\n  fun clearAll() {\n    spans.clear()\n    traceToTest.clear()\n  }\n\n  fun spanCount(traceId: String): Int =\n    spans[traceId]?.size ?: 0\n\n  fun totalSpanCount(): Int =\n    spans.values.sumOf { it.size }\n\n  fun traceCount(): Int = spans.size\n\n  /**\n   * Waits for at least the expected number of spans to be collected.\n   * Inspired by beholder-otel-extension's TestSpanCollector.\n   *\n   * @param traceId the trace ID to wait for\n   * @param expectedCount minimum number of spans to wait for\n   * @param timeoutMs maximum wait time in milliseconds\n   * @return the collected spans for the trace\n   */\n  fun waitForSpans(\n    traceId: String,\n    expectedCount: Int,\n    timeoutMs: Long = TracingConstants.DEFAULT_SPAN_WAIT_TIMEOUT_MS\n  ): List<SpanInfo> {\n    val deadline = System.currentTimeMillis() + timeoutMs\n    while (System.currentTimeMillis() < deadline) {\n      val currentSpans = getTrace(traceId)\n      if (currentSpans.size >= expectedCount) {\n        return currentSpans\n      }\n      try {\n        Thread.sleep(TracingConstants.DEFAULT_SPAN_POLL_INTERVAL_MS)\n      } catch (_: InterruptedException) {\n        Thread.currentThread().interrupt()\n        break\n      }\n    }\n    return getTrace(traceId)\n  }\n}\n"
  },
  {
    "path": "lib/stove-tracing/src/main/kotlin/com/trendyol/stove/tracing/TraceReportBuilder.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport arrow.core.getOrElse\nimport arrow.core.toOption\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.StoveOptions\n\n/**\n * Builds trace-enriched failure reports for test extensions.\n *\n * This centralizes the common logic for building reports that include\n * both Stove's execution report and the execution trace tree.\n */\n@Suppress(\"TooManyFunctions\")\nobject TraceReportBuilder {\n  private const val SPAN_WAIT_TIME_MS = 500L\n  private const val NO_SPANS_MESSAGE = \"No spans in trace\"\n\n  // ANSI color codes for the header\n  private object Colors {\n    const val RESET = \"\\u001B[0m\"\n    const val BOLD = \"\\u001B[1m\"\n    const val CYAN = \"\\u001B[36m\"\n    const val BRIGHT_CYAN = \"\\u001B[96m\"\n  }\n\n  const val DEFAULT_ERROR_MESSAGE = \"Test failed\"\n\n  /**\n   * Builds the full report including Stove execution report and trace tree.\n   */\n  fun buildFullReport(): String {\n    val options = Stove.options()\n    val report = Stove.reporter().dumpIfFailed(options.failureRenderer)\n    val traceTree = getColoredTraceTreeIfEnabled()\n    return buildReport(report, traceTree)\n  }\n\n  /**\n   * Checks if failure enrichment should be performed based on options.\n   */\n  fun StoveOptions.shouldEnrichFailures(): Boolean =\n    dumpReportOnTestFailure && reportingEnabled\n\n  private fun getColoredTraceTreeIfEnabled(): String =\n    TraceContext\n      .current()\n      .toOption()\n      .flatMap { Stove.getSystemOrNone<TracingSystem>() }\n      .flatMap { it.getTraceVisualizationForCurrentTest(SPAN_WAIT_TIME_MS) }\n      .map { visualization ->\n        // Use the colored tree for terminal display\n        visualization.coloredTree.let { tree ->\n          if (tree.isNotEmpty() && tree != NO_SPANS_MESSAGE) tree else \"\"\n        }\n      }.getOrElse { \"\" }\n\n  private fun buildReport(stoveReport: String, traceTree: String): String = buildString {\n    if (stoveReport.isNotEmpty()) {\n      append(stoveReport)\n    }\n    if (traceTree.isNotEmpty() && traceTree != NO_SPANS_MESSAGE) {\n      if (isNotEmpty()) appendLine().appendLine()\n      appendLine(buildColoredHeader())\n      append(traceTree)\n    }\n  }\n\n  private fun buildColoredHeader(): String = buildString {\n    val headerLine = \"${Colors.CYAN}═══════════════════════════════════════════════════════════════${Colors.RESET}\"\n    val title = \"${Colors.BOLD}${Colors.BRIGHT_CYAN}EXECUTION TRACE${Colors.RESET} (Call Chain)\"\n    appendLine(headerLine)\n    appendLine(title)\n    appendLine(headerLine)\n  }\n}\n"
  },
  {
    "path": "lib/stove-tracing/src/main/kotlin/com/trendyol/stove/tracing/TraceValidation.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.milliseconds\n\n@DslMarker\nannotation class TracingDsl\n\n@TracingDsl\nclass TraceValidationDsl(\n  private val collector: StoveTraceCollector,\n  private val traceId: String\n) {\n  private val trace: List<SpanInfo> by lazy { collector.getTrace(traceId) }\n  private val tree: SpanNode? by lazy { collector.getTraceTree(traceId) }\n\n  fun shouldContainSpan(operationName: String): TraceValidationDsl {\n    require(trace.any { it.operationName.contains(operationName) }) {\n      \"Expected span containing '$operationName' but found: ${trace.map { it.operationName }}\"\n    }\n    return this\n  }\n\n  fun shouldContainSpanMatching(predicate: (SpanInfo) -> Boolean): TraceValidationDsl {\n    require(trace.any(predicate)) {\n      \"Expected span matching predicate but none found in: ${trace.map { it.operationName }}\"\n    }\n    return this\n  }\n\n  fun shouldNotContainSpan(operationName: String): TraceValidationDsl {\n    val matchingSpans = trace.filter { it.operationName.contains(operationName) }\n    require(matchingSpans.isEmpty()) {\n      \"Expected no span containing '$operationName' but found: ${matchingSpans.map { it.operationName }}\"\n    }\n    return this\n  }\n\n  fun shouldNotHaveFailedSpans(): TraceValidationDsl {\n    val failed = trace.filter { it.isFailed }\n    require(failed.isEmpty()) {\n      val failureMessages = failed.map { span ->\n        \"${span.operationName}: ${span.exception?.message ?: \"unknown error\"}\"\n      }\n      \"Expected no failed spans but found: $failureMessages\"\n    }\n    return this\n  }\n\n  fun shouldHaveFailedSpan(operationName: String): TraceValidationDsl {\n    val failedSpans = trace.filter { it.isFailed }\n    val failedMatching = failedSpans.filter { it.operationName.contains(operationName) }\n\n    require(failedMatching.isNotEmpty()) {\n      \"Expected failed span containing '$operationName' but found failed spans: ${failedSpans.map { it.operationName }}\"\n    }\n    return this\n  }\n\n  fun executionTimeShouldBeLessThan(duration: Duration): TraceValidationDsl {\n    val totalDuration = calculateTotalDuration()\n    require(totalDuration <= duration) {\n      \"Expected execution time <= $duration but was $totalDuration\"\n    }\n    return this\n  }\n\n  fun executionTimeShouldBeGreaterThan(duration: Duration): TraceValidationDsl {\n    val totalDuration = calculateTotalDuration()\n    require(totalDuration >= duration) {\n      \"Expected execution time >= $duration but was $totalDuration\"\n    }\n    return this\n  }\n\n  fun spanCountShouldBe(expected: Int): TraceValidationDsl {\n    require(trace.size == expected) {\n      \"Expected $expected spans but found ${trace.size}\"\n    }\n    return this\n  }\n\n  fun spanCountShouldBeAtLeast(minimum: Int): TraceValidationDsl {\n    require(trace.size >= minimum) {\n      \"Expected at least $minimum spans but found ${trace.size}\"\n    }\n    return this\n  }\n\n  fun spanCountShouldBeAtMost(maximum: Int): TraceValidationDsl {\n    require(trace.size <= maximum) {\n      \"Expected at most $maximum spans but found ${trace.size}\"\n    }\n    return this\n  }\n\n  fun shouldHaveSpanWithAttribute(key: String, value: String): TraceValidationDsl {\n    require(trace.any { it.attributes[key] == value }) {\n      \"Expected span with attribute '$key'='$value' but none found\"\n    }\n    return this\n  }\n\n  fun shouldHaveSpanWithAttributeContaining(key: String, substring: String): TraceValidationDsl {\n    require(trace.any { it.attributes[key]?.contains(substring) == true }) {\n      \"Expected span with attribute '$key' containing '$substring' but none found\"\n    }\n    return this\n  }\n\n  fun getSpanCount(): Int = trace.size\n\n  fun getFailedSpans(): List<SpanInfo> = trace.filter { it.isFailed }\n\n  fun getFailedSpanCount(): Int = getFailedSpans().size\n\n  fun findSpan(predicate: (SpanInfo) -> Boolean): SpanInfo? = trace.find(predicate)\n\n  fun findSpanByName(operationName: String): SpanInfo? =\n    trace.find { it.operationName.contains(operationName) }\n\n  fun spanTree(): SpanNode? = tree\n\n  fun getTotalDuration(): Duration = calculateTotalDuration()\n\n  private fun calculateTotalDuration(): Duration {\n    if (trace.isEmpty()) return 0.milliseconds\n\n    val minStart = trace.minOf { it.startTimeNanos }\n    val maxEnd = trace.maxOf { it.endTimeNanos }\n    val durationMs = (maxEnd - minStart) / TracingConstants.NANOS_TO_MILLIS\n\n    return durationMs.milliseconds\n  }\n\n  fun renderTree(): String {\n    val root = tree ?: return \"No spans in trace\"\n    return TraceTreeRenderer.render(root)\n  }\n\n  fun renderSummary(): String {\n    val root = tree ?: return \"No spans in trace\"\n    return TraceTreeRenderer.renderSummary(root)\n  }\n}\n"
  },
  {
    "path": "lib/stove-tracing/src/main/kotlin/com/trendyol/stove/tracing/TracingOptions.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.tracing\n\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * Configuration options for the Stove tracing system.\n *\n * The tracing system works by receiving OTLP spans from the application under test.\n * Configure your application to use the OpenTelemetry Java Agent and export spans\n * to the Stove OTLP receiver endpoint.\n */\nclass TracingOptions {\n  var enabled: Boolean = false\n    private set\n\n  var spanCollectionTimeout: Duration = 5.seconds\n    private set\n\n  var spanFilter: (SpanInfo) -> Boolean = { true }\n    private set\n\n  var maxSpansPerTrace: Int = TracingConstants.DEFAULT_MAX_SPANS_PER_TRACE\n    private set\n\n  var spanReceiverEnabled: Boolean = false\n    private set\n\n  var spanReceiverPort: Int = TracingConstants.DEFAULT_OTLP_GRPC_PORT\n    private set\n\n  fun enabled(): TracingOptions = apply { enabled = true }\n\n  fun disabled(): TracingOptions = apply { enabled = false }\n\n  fun spanCollectionTimeout(timeout: Duration): TracingOptions = apply {\n    spanCollectionTimeout = timeout\n  }\n\n  fun spanFilter(filter: (SpanInfo) -> Boolean): TracingOptions = apply {\n    spanFilter = filter\n  }\n\n  fun maxSpansPerTrace(max: Int): TracingOptions = apply {\n    maxSpansPerTrace = max\n  }\n\n  /**\n   * Enable the OTLP span receiver to collect spans from the application under test.\n   *\n   * The port is determined in the following order:\n   * 1. Explicitly provided port parameter\n   * 2. STOVE_TRACING_PORT environment variable (set by Gradle configuration)\n   * 3. Default port 4317\n   *\n   * The application should be configured to export spans via the OpenTelemetry Java Agent:\n   * ```\n   * -javaagent:path/to/opentelemetry-javaagent.jar\n   * -Dotel.exporter.otlp.endpoint=http://localhost:{port}\n   * -Dotel.exporter.otlp.protocol=grpc\n   * -Dotel.service.name=my-service\n   * ```\n   *\n   * @param port The port for the OTLP gRPC receiver. If not specified, reads from\n   *             STOVE_TRACING_PORT env var or defaults to 4317.\n   */\n  fun enableSpanReceiver(port: Int? = null): TracingOptions = apply {\n    spanReceiverEnabled = true\n    spanReceiverPort = port ?: portFromEnv() ?: TracingConstants.DEFAULT_OTLP_GRPC_PORT\n  }\n\n  private fun portFromEnv(): Int? =\n    System.getenv(TracingConstants.STOVE_TRACING_PORT_ENV)?.toIntOrNull()\n\n  fun copy(): TracingOptions = TracingOptions().also { copy ->\n    copy.enabled = this.enabled\n    copy.spanCollectionTimeout = this.spanCollectionTimeout\n    copy.spanFilter = this.spanFilter\n    copy.maxSpansPerTrace = this.maxSpansPerTrace\n    copy.spanReceiverEnabled = this.spanReceiverEnabled\n    copy.spanReceiverPort = this.spanReceiverPort\n  }\n}\n"
  },
  {
    "path": "lib/stove-tracing/src/main/kotlin/com/trendyol/stove/tracing/TracingSystem.kt",
    "content": "@file:Suppress(\"unused\", \"TooManyFunctions\")\n\npackage com.trendyol.stove.tracing\n\nimport arrow.core.*\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n@StoveDsl\nclass TracingSystem(\n  override val stove: Stove,\n  private val options: TracingSystemOptions\n) : PluggedSystem,\n  RunAware,\n  TraceProvider,\n  SpanListenerRegistry {\n  private val logger = org.slf4j.LoggerFactory.getLogger(TracingSystem::class.java)\n  internal val collector: StoveTraceCollector = StoveTraceCollector()\n  private var spanReceiver: OTLPSpanReceiver? = null\n\n  override suspend fun run() {\n    if (options.tracingOptions.spanReceiverEnabled) {\n      startSpanReceiver()\n    }\n  }\n\n  override suspend fun stop() {\n    stopSpanReceiver()\n    clearAllTraces()\n  }\n\n  override fun getTraceVisualizationForCurrentTest(waitTimeMs: Long): Option<TraceVisualization> {\n    val ctx = TraceContext.current() ?: return None\n    val spans = pollForSpans(ctx.traceId, waitTimeMs)\n    return createVisualizationOrFallback(ctx.traceId, ctx.testId, spans)\n  }\n\n  suspend fun ensureTraceStarted(): TraceContext =\n    TraceContext.current() ?: startNewTrace()\n\n  fun endTrace() {\n    TraceContext.clear()\n  }\n\n  fun currentContext(): Option<TraceContext> =\n    TraceContext.current().toOption()\n\n  fun validation(): Option<TraceValidationDsl> =\n    currentContext().map { createValidation(it.traceId) }\n\n  fun validation(traceId: String): TraceValidationDsl =\n    createValidation(traceId)\n\n  override fun addSpanListener(listener: SpanEventListener) {\n    collector.addSpanListener(listener)\n  }\n\n  override fun then(): Stove = stove\n\n  override fun close() {\n    clearAllTraces()\n  }\n\n  // Private helper methods\n\n  private fun startSpanReceiver() {\n    spanReceiver = OTLPSpanReceiver(collector, options.tracingOptions.spanReceiverPort)\n    spanReceiver?.start()?.fold(\n      ifLeft = { error ->\n        // Port binding failure is non-fatal - tests continue without span collection\n        // This commonly happens when multiple test modules run in parallel on CI\n        logger.warn(\n          \"[StoveTracing] Failed to start OTLP receiver on port {}: {}. \" +\n            \"Tracing spans will not be collected. This is usually caused by another \" +\n            \"test module already using this port.\",\n          options.tracingOptions.spanReceiverPort,\n          error.message\n        )\n        spanReceiver = null\n      },\n      ifRight = { /* Started successfully */ }\n    )\n  }\n\n  private fun stopSpanReceiver() {\n    spanReceiver?.stop()\n  }\n\n  private fun clearAllTraces() {\n    collector.clearAll()\n    TraceContext.clear()\n  }\n\n  /**\n   * Polls for spans with intelligent waiting strategy.\n   * Returns as soon as first spans arrive, then waits briefly for stragglers.\n   */\n  private fun pollForSpans(traceId: String, maxWaitTimeMs: Long): List<SpanInfo> {\n    val deadline = System.currentTimeMillis() + maxWaitTimeMs\n\n    // Poll until we find spans or timeout\n    val initialSpans = pollUntilSpansArrive(traceId, deadline)\n\n    // If we found spans, wait a bit more for stragglers\n    return if (initialSpans.isNotEmpty()) {\n      waitForStragglersAndCollect(traceId, deadline)\n    } else {\n      emptyList()\n    }\n  }\n\n  /**\n   * Polls repeatedly until spans arrive or deadline is reached.\n   */\n  private fun pollUntilSpansArrive(traceId: String, deadline: Long): List<SpanInfo> {\n    while (System.currentTimeMillis() < deadline) {\n      val spans = collector.getTrace(traceId)\n      if (spans.isNotEmpty()) return spans\n\n      if (!sleepQuietly(TracingConstants.DEFAULT_SPAN_POLL_INTERVAL_MS)) {\n        break // Interrupted\n      }\n    }\n    return emptyList()\n  }\n\n  /**\n   * Waits briefly for straggler spans, then collects final result.\n   */\n  private fun waitForStragglersAndCollect(traceId: String, deadline: Long): List<SpanInfo> {\n    val remainingTime = deadline - System.currentTimeMillis()\n    val stragglerWait = minOf(TracingConstants.STRAGGLER_WAIT_TIME_MS, remainingTime).coerceAtLeast(0)\n\n    sleepQuietly(stragglerWait)\n    return collector.getTrace(traceId)\n  }\n\n  /**\n   * Sleeps quietly, handling interruption gracefully.\n   * Returns true if sleep completed, false if interrupted.\n   */\n  private fun sleepQuietly(millis: Long): Boolean {\n    if (millis <= 0) return true\n\n    return try {\n      Thread.sleep(millis)\n      true\n    } catch (_: InterruptedException) {\n      Thread.currentThread().interrupt()\n      false\n    }\n  }\n\n  /**\n   * Creates visualization from spans, or falls back to most relevant trace if empty.\n   */\n  private fun createVisualizationOrFallback(\n    traceId: String,\n    testId: String,\n    spans: List<SpanInfo>\n  ): Option<TraceVisualization> = when {\n    spans.isNotEmpty() -> {\n      TraceVisualization.from(traceId, testId, spans).some()\n    }\n\n    else -> {\n      val traceByTestId = findTraceByTestIdAttribute(testId)\n      when (traceByTestId) {\n        is Some -> traceByTestId\n        None -> findMostRelevantTrace(testId)\n      }\n    }\n  }\n\n  /**\n   * Finds the most recent trace that explicitly carries the current test ID as a span attribute.\n   *\n   * This is a defense-in-depth fallback when the expected trace ID has no spans\n   * (e.g., `traceparent` was rewritten by external instrumentation).\n   */\n  private fun findTraceByTestIdAttribute(testId: String): Option<TraceVisualization> {\n    val matchingTrace = collector\n      .getAllTraces()\n      .asSequence()\n      .filter { (_, spans) -> spans.isNotEmpty() }\n      .filter { (_, spans) -> spans.any { span -> spanContainsTestId(span, testId) } }\n      .maxByOrNull { (_, spans) -> spans.maxOf { it.startTimeNanos } }\n      ?: return None\n\n    val (traceId, traceSpans) = matchingTrace\n    return TraceVisualization.from(traceId, testId, traceSpans).some()\n  }\n\n  private fun spanContainsTestId(span: SpanInfo, testId: String): Boolean =\n    span.attributes.any { (key, value) ->\n      isTestIdAttributeKey(key) && isTestIdAttributeValue(value, testId)\n    }\n\n  private fun isTestIdAttributeKey(key: String): Boolean {\n    val normalized = key.lowercase()\n    return normalized == TraceContext.STOVE_TEST_ID_HEADER.lowercase() ||\n      normalized.contains(\"x-stove-test-id\") ||\n      normalized.contains(\"stove_test_id\") ||\n      normalized.contains(\"stove.test.id\")\n  }\n\n  private fun isTestIdAttributeValue(rawValue: String, testId: String): Boolean {\n    if (rawValue == testId) return true\n\n    return tokenizeAttributeValue(rawValue).any { token -> token == testId }\n  }\n\n  private fun tokenizeAttributeValue(rawValue: String): List<String> {\n    val normalized = rawValue\n      .removePrefix(\"[\")\n      .removeSuffix(\"]\")\n      .removePrefix(\"{\")\n      .removeSuffix(\"}\")\n\n    return normalized\n      .split(\",\", \";\")\n      .flatMap { token ->\n        val separatorIndex = token.indexOfAny(charArrayOf('=', ':'))\n        if (separatorIndex >= 0) {\n          listOf(\n            token.substring(0, separatorIndex),\n            token.substring(separatorIndex + 1)\n          )\n        } else {\n          listOf(token)\n        }\n      }.map { token ->\n        token.trim().trim('\"', '\\'', '{', '}', '[', ']')\n      }.filter { token ->\n        token.isNotEmpty()\n      }\n  }\n\n  /**\n   * Finds the most relevant trace when the expected trace ID has no spans.\n   * Uses the most recent trace (by start time) as a heuristic, as it's likely\n   * to be the trace closest to the test execution.\n   */\n  private fun findMostRelevantTrace(testId: String): Option<TraceVisualization> {\n    val allTraces = collector.getAllTraces()\n    if (allTraces.isEmpty()) return None\n\n    // Find the trace with the most recent span start time\n    val (traceId, traceSpans) = allTraces\n      .filter { it.value.isNotEmpty() }\n      .maxByOrNull { entry -> entry.value.maxOf { it.startTimeNanos } }\n      ?: return None\n\n    return TraceVisualization.from(traceId, testId, traceSpans).some()\n  }\n\n  private suspend fun startNewTrace(): TraceContext {\n    val testId = resolveTestId()\n    val ctx = TraceContext.start(testId)\n    collector.registerTrace(ctx.traceId, testId)\n    return ctx\n  }\n\n  private suspend fun resolveTestId(): String =\n    resolveKotestTestId()\n      ?: resolveJUnitTestId()\n      ?: generateFallbackTestId()\n\n  private suspend fun resolveKotestTestId(): String? =\n    currentStoveTestContext()?.testId\n\n  private fun resolveJUnitTestId(): String? =\n    StoveTestContextHolder.get()?.testId\n\n  private fun generateFallbackTestId(): String =\n    \"stove-trace-${System.currentTimeMillis()}\"\n\n  private fun createValidation(traceId: String): TraceValidationDsl =\n    TraceValidationDsl(collector, traceId)\n}\n\ndata class TracingSystemOptions(\n  val tracingOptions: TracingOptions = TracingOptions().enabled()\n)\n\ninternal fun Stove.withTracing(options: TracingSystemOptions): Stove {\n  this.getOrRegister(TracingSystem(this, options))\n  return this\n}\n\ninternal fun Stove.tracingSystem(): TracingSystem = getOrNone<TracingSystem>().getOrElse {\n  throw SystemNotRegisteredException(TracingSystem::class)\n}\n\nfun WithDsl.tracing(configure: @StoveDsl TracingOptions.() -> Unit = {}): Stove =\n  this.stove.withTracing(createTracingOptions(configure))\n\nprivate fun createTracingOptions(configure: TracingOptions.() -> Unit): TracingSystemOptions {\n  val options = TracingOptions()\n  options.configure()\n  options.enabled()\n  return TracingSystemOptions(options)\n}\n\n/**\n * DSL scope for tracing validation - exposes trace context and all validation methods directly.\n */\n@StoveDsl\nclass TracingValidationScope(\n  val ctx: TraceContext,\n  private val validation: TraceValidationDsl,\n  val collector: StoveTraceCollector\n) {\n  val traceId: String get() = ctx.traceId\n  val rootSpanId: String get() = ctx.rootSpanId\n  val testId: String get() = ctx.testId\n\n  fun toTraceparent(): String = ctx.toTraceparent()\n\n  // Validation method delegates\n  fun shouldContainSpan(operationName: String) = validation.shouldContainSpan(operationName)\n\n  fun shouldContainSpanMatching(predicate: (SpanInfo) -> Boolean) = validation.shouldContainSpanMatching(predicate)\n\n  fun shouldNotContainSpan(operationName: String) = validation.shouldNotContainSpan(operationName)\n\n  fun shouldNotHaveFailedSpans() = validation.shouldNotHaveFailedSpans()\n\n  fun shouldHaveFailedSpan(operationName: String) = validation.shouldHaveFailedSpan(operationName)\n\n  fun executionTimeShouldBeLessThan(duration: kotlin.time.Duration) = validation.executionTimeShouldBeLessThan(duration)\n\n  fun executionTimeShouldBeGreaterThan(duration: kotlin.time.Duration) = validation.executionTimeShouldBeGreaterThan(duration)\n\n  fun spanCountShouldBe(expected: Int) = validation.spanCountShouldBe(expected)\n\n  fun spanCountShouldBeAtLeast(minimum: Int) = validation.spanCountShouldBeAtLeast(minimum)\n\n  fun spanCountShouldBeAtMost(maximum: Int) = validation.spanCountShouldBeAtMost(maximum)\n\n  fun shouldHaveSpanWithAttribute(key: String, value: String) = validation.shouldHaveSpanWithAttribute(key, value)\n\n  fun shouldHaveSpanWithAttributeContaining(key: String, substring: String) =\n    validation.shouldHaveSpanWithAttributeContaining(key, substring)\n\n  // Query methods\n  fun getSpanCount(): Int = validation.getSpanCount()\n\n  fun getFailedSpans(): List<SpanInfo> = validation.getFailedSpans()\n\n  fun getFailedSpanCount(): Int = validation.getFailedSpanCount()\n\n  fun findSpan(predicate: (SpanInfo) -> Boolean): Option<SpanInfo> = validation.findSpan(predicate).toOption()\n\n  fun findSpanByName(operationName: String): Option<SpanInfo> = validation.findSpanByName(operationName).toOption()\n\n  fun spanTree(): Option<SpanNode> = validation.spanTree().toOption()\n\n  fun getTotalDuration(): kotlin.time.Duration = validation.getTotalDuration()\n\n  fun renderTree(): String = validation.renderTree()\n\n  fun renderSummary(): String = validation.renderSummary()\n\n  /**\n   * Waits for at least the expected number of spans to be collected.\n   * Useful for ensuring spans have been exported before making assertions.\n   */\n  fun waitForSpans(expectedCount: Int, timeoutMs: Long = 2000): List<SpanInfo> =\n    collector.waitForSpans(traceId, expectedCount, timeoutMs)\n\n  /**\n   * Gets a visualization of the trace for this test.\n   * Includes all spans for this trace ID formatted as a tree.\n   */\n  fun getTraceVisualization(): TraceVisualization {\n    val spans = collector.getTrace(traceId)\n    return TraceVisualization.from(traceId, testId, spans)\n  }\n\n  /**\n   * Gets visualizations for ALL collected traces (not just this test's trace ID).\n   * Useful for seeing all activity during the test execution.\n   */\n  fun getAllTraceVisualizations(): List<TraceVisualization> {\n    val allTraces = collector.getAllTraces()\n    return allTraces.map { (traceId, spans) ->\n      TraceVisualization.from(traceId, testId, spans)\n    }\n  }\n}\n\nsuspend fun ValidationDsl.tracing(\n  validation: @StoveDsl suspend TracingValidationScope.() -> Unit\n) {\n  val system = this.stove.tracingSystem()\n  val ctx = system.ensureTraceStarted()\n  val scope = createTracingValidationScope(system, ctx)\n  TraceContext.withPropagation(ctx) {\n    validation(scope)\n  }\n}\n\nprivate fun createTracingValidationScope(\n  system: TracingSystem,\n  ctx: TraceContext\n): TracingValidationScope {\n  val traceValidation = system.validation(ctx.traceId)\n  return TracingValidationScope(ctx, traceValidation, system.collector)\n}\n\nfun ValidationDsl.tracingSystem(): TracingSystem = this.stove.tracingSystem()\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/OtlpSpanReceiverTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport arrow.core.getOrElse\nimport io.grpc.ManagedChannelBuilder\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.types.shouldBeInstanceOf\nimport io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\nimport io.opentelemetry.proto.collector.trace.v1.TraceServiceGrpc\nimport io.opentelemetry.proto.common.v1.AnyValue\nimport io.opentelemetry.proto.common.v1.ArrayValue\nimport io.opentelemetry.proto.common.v1.KeyValue\nimport io.opentelemetry.proto.resource.v1.Resource\nimport io.opentelemetry.proto.trace.v1.ResourceSpans\nimport io.opentelemetry.proto.trace.v1.ScopeSpans\nimport io.opentelemetry.proto.trace.v1.Span\nimport io.opentelemetry.proto.trace.v1.Status\nimport java.util.concurrent.TimeUnit\n\nprivate const val TEST_STACKTRACE = \"\"\"java.lang.RuntimeException: Something went wrong\n\tat com.example.Service.process(Service.kt:42)\n\tat com.example.Controller.handle(Controller.kt:15)\n\tat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\"\"\"\n\nclass OtlpSpanReceiverTest :\n  FunSpec({\n    val testPort = 14317 // Use a non-standard port to avoid conflicts\n\n    test(\"start should succeed on available port\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        val result = receiver.start()\n        result.isRight() shouldBe true\n        receiver.endpoint shouldBe \"http://localhost:$testPort\"\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"start should be idempotent\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver.start()\n        val result = receiver.start() // Second start should also succeed\n        result.isRight() shouldBe true\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"stop should be safe to call multiple times\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      receiver.start()\n      receiver.stop()\n      receiver.stop() // Should not throw\n    }\n\n    test(\"export should record spans to collector\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver.start()\n\n        // Create a gRPC client\n        val channel = ManagedChannelBuilder\n          .forAddress(\"localhost\", testPort)\n          .usePlaintext()\n          .build()\n\n        try {\n          val stub = TraceServiceGrpc.newBlockingStub(channel)\n\n          // Create a test span\n          val request = createExportRequest(\n            serviceName = \"test-service\",\n            traceId = \"0123456789abcdef0123456789abcdef\",\n            spanId = \"0123456789abcdef\",\n            spanName = \"test-operation\"\n          )\n\n          stub.export(request)\n\n          // Give some time for the span to be recorded\n          Thread.sleep(100)\n\n          val trace = collector.getTrace(\"0123456789abcdef0123456789abcdef\")\n          trace shouldHaveSize 1\n          trace[0].operationName shouldBe \"test-operation\"\n          trace[0].serviceName shouldBe \"test-service\"\n        } finally {\n          channel.shutdown()\n          channel.awaitTermination(5, TimeUnit.SECONDS)\n        }\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"export should filter out internal gRPC spans\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver.start()\n\n        val channel = ManagedChannelBuilder\n          .forAddress(\"localhost\", testPort)\n          .usePlaintext()\n          .build()\n\n        try {\n          val stub = TraceServiceGrpc.newBlockingStub(channel)\n\n          // Create spans including internal gRPC span that should be filtered\n          val request = createExportRequest(\n            serviceName = \"test-service\",\n            traceId = \"abcdef0123456789abcdef0123456789\",\n            spanId = \"fedcba9876543210\",\n            spanName = \"TraceService/Export\" // This should be filtered out\n          )\n\n          stub.export(request)\n\n          Thread.sleep(100)\n\n          // The internal span should be filtered out\n          val trace = collector.getTrace(\"abcdef0123456789abcdef0123456789\")\n          trace shouldHaveSize 0\n        } finally {\n          channel.shutdown()\n          channel.awaitTermination(5, TimeUnit.SECONDS)\n        }\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"export should extract service name from resource attributes\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver.start()\n\n        val channel = ManagedChannelBuilder\n          .forAddress(\"localhost\", testPort)\n          .usePlaintext()\n          .build()\n\n        try {\n          val stub = TraceServiceGrpc.newBlockingStub(channel)\n\n          val request = createExportRequest(\n            serviceName = \"my-custom-service\",\n            traceId = \"11111111111111111111111111111111\",\n            spanId = \"2222222222222222\",\n            spanName = \"custom-operation\"\n          )\n\n          stub.export(request)\n\n          Thread.sleep(100)\n\n          val trace = collector.getTrace(\"11111111111111111111111111111111\")\n          trace shouldHaveSize 1\n          trace[0].serviceName shouldBe \"my-custom-service\"\n        } finally {\n          channel.shutdown()\n          channel.awaitTermination(5, TimeUnit.SECONDS)\n        }\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"export should handle multiple spans in single request\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver.start()\n\n        val channel = ManagedChannelBuilder\n          .forAddress(\"localhost\", testPort)\n          .usePlaintext()\n          .build()\n\n        try {\n          val stub = TraceServiceGrpc.newBlockingStub(channel)\n\n          val traceId = \"33333333333333333333333333333333\"\n          val request = createExportRequestWithMultipleSpans(\n            serviceName = \"test-service\",\n            traceId = traceId,\n            spanNames = listOf(\"span1\", \"span2\", \"span3\")\n          )\n\n          stub.export(request)\n\n          Thread.sleep(100)\n\n          val trace = collector.getTrace(traceId)\n          trace shouldHaveSize 3\n          trace.map { it.operationName } shouldBe listOf(\"span1\", \"span2\", \"span3\")\n        } finally {\n          channel.shutdown()\n          channel.awaitTermination(5, TimeUnit.SECONDS)\n        }\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"export should handle span with error status\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver.start()\n\n        val channel = ManagedChannelBuilder\n          .forAddress(\"localhost\", testPort)\n          .usePlaintext()\n          .build()\n\n        try {\n          val stub = TraceServiceGrpc.newBlockingStub(channel)\n\n          val traceId = \"44444444444444444444444444444444\"\n          val request = createExportRequestWithErrorSpan(\n            serviceName = \"test-service\",\n            traceId = traceId,\n            spanId = \"5555555555555555\",\n            spanName = \"failed-operation\",\n            errorMessage = \"Something went wrong\"\n          )\n\n          stub.export(request)\n\n          Thread.sleep(100)\n\n          val trace = collector.getTrace(traceId)\n          trace shouldHaveSize 1\n          trace[0].status shouldBe SpanStatus.ERROR\n          trace[0].exception?.message shouldBe \"Something went wrong\"\n        } finally {\n          channel.shutdown()\n          channel.awaitTermination(5, TimeUnit.SECONDS)\n        }\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"export should extract exception from span events\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver.start()\n\n        val channel = ManagedChannelBuilder\n          .forAddress(\"localhost\", testPort)\n          .usePlaintext()\n          .build()\n\n        try {\n          val stub = TraceServiceGrpc.newBlockingStub(channel)\n\n          val traceId = \"88888888888888888888888888888888\"\n          val request = createExportRequestWithExceptionEvent(\n            serviceName = \"test-service\",\n            traceId = traceId,\n            spanId = \"9999999999999999\",\n            spanName = \"failed-operation\",\n            exceptionType = \"java.lang.RuntimeException\",\n            exceptionMessage = \"Something went wrong\",\n            exceptionStacktrace = TEST_STACKTRACE\n          )\n\n          stub.export(request)\n\n          Thread.sleep(100)\n\n          val trace = collector.getTrace(traceId)\n          trace shouldHaveSize 1\n          trace[0].status shouldBe SpanStatus.ERROR\n          trace[0].exception?.type shouldBe \"java.lang.RuntimeException\"\n          trace[0].exception?.message shouldBe \"Something went wrong\"\n          trace[0].exception?.stackTrace?.size shouldBe 4\n          trace[0].exception?.stackTrace?.get(1) shouldBe \"at com.example.Service.process(Service.kt:42)\"\n        } finally {\n          channel.shutdown()\n          channel.awaitTermination(5, TimeUnit.SECONDS)\n        }\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"export should extract span attributes\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver.start()\n\n        val channel = ManagedChannelBuilder\n          .forAddress(\"localhost\", testPort)\n          .usePlaintext()\n          .build()\n\n        try {\n          val stub = TraceServiceGrpc.newBlockingStub(channel)\n\n          val traceId = \"66666666666666666666666666666666\"\n          val request = createExportRequestWithAttributes(\n            serviceName = \"test-service\",\n            traceId = traceId,\n            spanId = \"7777777777777777\",\n            spanName = \"db-operation\",\n            attributes = mapOf(\n              \"db.system\" to \"postgresql\",\n              \"db.name\" to \"test_db\"\n            )\n          )\n\n          stub.export(request)\n\n          Thread.sleep(100)\n\n          val trace = collector.getTrace(traceId)\n          trace shouldHaveSize 1\n          trace[0].attributes[\"db.system\"] shouldBe \"postgresql\"\n          trace[0].attributes[\"db.name\"] shouldBe \"test_db\"\n        } finally {\n          channel.shutdown()\n          channel.awaitTermination(5, TimeUnit.SECONDS)\n        }\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"export should serialize array attributes as JSON-like values\") {\n      val collector = StoveTraceCollector()\n      val receiver = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver.start()\n\n        val channel = ManagedChannelBuilder\n          .forAddress(\"localhost\", testPort)\n          .usePlaintext()\n          .build()\n\n        try {\n          val stub = TraceServiceGrpc.newBlockingStub(channel)\n\n          val traceId = \"99999999999999999999999999999999\"\n          val request = createExportRequestWithArrayAttribute(\n            serviceName = \"test-service\",\n            traceId = traceId,\n            spanId = \"aaaaaaaaaaaaaaaa\",\n            spanName = \"http-call\",\n            attributeKey = \"http.request.header.x_stove_test_id\",\n            values = listOf(\"test-id-1\", \"test-id-2\")\n          )\n\n          stub.export(request)\n\n          Thread.sleep(100)\n\n          val trace = collector.getTrace(traceId)\n          trace shouldHaveSize 1\n          trace[0].attributes[\"http.request.header.x_stove_test_id\"] shouldBe \"[\\\"test-id-1\\\", \\\"test-id-2\\\"]\"\n        } finally {\n          channel.shutdown()\n          channel.awaitTermination(5, TimeUnit.SECONDS)\n        }\n      } finally {\n        receiver.stop()\n      }\n    }\n\n    test(\"start should fail on port already in use\") {\n      val collector = StoveTraceCollector()\n      val receiver1 = OTLPSpanReceiver(collector, testPort)\n      val receiver2 = OTLPSpanReceiver(collector, testPort)\n\n      try {\n        receiver1.start()\n        val result = receiver2.start()\n\n        result.isLeft() shouldBe true\n        result.getOrElse { it }.shouldBeInstanceOf<OTLPReceiverError.StartupFailed>()\n      } finally {\n        receiver1.stop()\n        receiver2.stop()\n      }\n    }\n  })\n\nprivate fun hexStringToByteString(hex: String): com.google.protobuf.ByteString {\n  val bytes = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()\n  return com.google.protobuf.ByteString\n    .copyFrom(bytes)\n}\n\nprivate fun createExportRequest(\n  serviceName: String,\n  traceId: String,\n  spanId: String,\n  spanName: String\n): ExportTraceServiceRequest {\n  val resource = Resource\n    .newBuilder()\n    .addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(\"service.name\")\n        .setValue(AnyValue.newBuilder().setStringValue(serviceName))\n    ).build()\n\n  val span = Span\n    .newBuilder()\n    .setTraceId(hexStringToByteString(traceId))\n    .setSpanId(hexStringToByteString(spanId))\n    .setName(spanName)\n    .setStartTimeUnixNano(System.nanoTime())\n    .setEndTimeUnixNano(System.nanoTime() + 1_000_000)\n    .build()\n\n  val scopeSpans = ScopeSpans\n    .newBuilder()\n    .addSpans(span)\n    .build()\n\n  val resourceSpans = ResourceSpans\n    .newBuilder()\n    .setResource(resource)\n    .addScopeSpans(scopeSpans)\n    .build()\n\n  return ExportTraceServiceRequest\n    .newBuilder()\n    .addResourceSpans(resourceSpans)\n    .build()\n}\n\nprivate fun createExportRequestWithMultipleSpans(\n  serviceName: String,\n  traceId: String,\n  spanNames: List<String>\n): ExportTraceServiceRequest {\n  val resource = Resource\n    .newBuilder()\n    .addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(\"service.name\")\n        .setValue(AnyValue.newBuilder().setStringValue(serviceName))\n    ).build()\n\n  val scopeSpansBuilder = ScopeSpans.newBuilder()\n\n  spanNames.forEachIndexed { index, name ->\n    val span = Span\n      .newBuilder()\n      .setTraceId(hexStringToByteString(traceId))\n      .setSpanId(hexStringToByteString(\"${index + 1}\".padStart(16, '0')))\n      .setName(name)\n      .setStartTimeUnixNano(System.nanoTime())\n      .setEndTimeUnixNano(System.nanoTime() + 1_000_000)\n      .build()\n    scopeSpansBuilder.addSpans(span)\n  }\n\n  val resourceSpans = ResourceSpans\n    .newBuilder()\n    .setResource(resource)\n    .addScopeSpans(scopeSpansBuilder.build())\n    .build()\n\n  return ExportTraceServiceRequest\n    .newBuilder()\n    .addResourceSpans(resourceSpans)\n    .build()\n}\n\nprivate fun createExportRequestWithErrorSpan(\n  serviceName: String,\n  traceId: String,\n  spanId: String,\n  spanName: String,\n  errorMessage: String\n): ExportTraceServiceRequest {\n  val resource = Resource\n    .newBuilder()\n    .addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(\"service.name\")\n        .setValue(AnyValue.newBuilder().setStringValue(serviceName))\n    ).build()\n\n  val span = Span\n    .newBuilder()\n    .setTraceId(hexStringToByteString(traceId))\n    .setSpanId(hexStringToByteString(spanId))\n    .setName(spanName)\n    .setStartTimeUnixNano(System.nanoTime())\n    .setEndTimeUnixNano(System.nanoTime() + 1_000_000)\n    .setStatus(\n      Status\n        .newBuilder()\n        .setCodeValue(TracingConstants.OTEL_STATUS_CODE_ERROR)\n        .setMessage(errorMessage)\n    ).build()\n\n  val scopeSpans = ScopeSpans\n    .newBuilder()\n    .addSpans(span)\n    .build()\n\n  val resourceSpans = ResourceSpans\n    .newBuilder()\n    .setResource(resource)\n    .addScopeSpans(scopeSpans)\n    .build()\n\n  return ExportTraceServiceRequest\n    .newBuilder()\n    .addResourceSpans(resourceSpans)\n    .build()\n}\n\nprivate fun createExportRequestWithAttributes(\n  serviceName: String,\n  traceId: String,\n  spanId: String,\n  spanName: String,\n  attributes: Map<String, String>\n): ExportTraceServiceRequest {\n  val resource = Resource\n    .newBuilder()\n    .addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(\"service.name\")\n        .setValue(AnyValue.newBuilder().setStringValue(serviceName))\n    ).build()\n\n  val spanBuilder = Span\n    .newBuilder()\n    .setTraceId(hexStringToByteString(traceId))\n    .setSpanId(hexStringToByteString(spanId))\n    .setName(spanName)\n    .setStartTimeUnixNano(System.nanoTime())\n    .setEndTimeUnixNano(System.nanoTime() + 1_000_000)\n\n  attributes.forEach { (key, value) ->\n    spanBuilder.addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(key)\n        .setValue(AnyValue.newBuilder().setStringValue(value))\n    )\n  }\n\n  val scopeSpans = ScopeSpans\n    .newBuilder()\n    .addSpans(spanBuilder.build())\n    .build()\n\n  val resourceSpans = ResourceSpans\n    .newBuilder()\n    .setResource(resource)\n    .addScopeSpans(scopeSpans)\n    .build()\n\n  return ExportTraceServiceRequest\n    .newBuilder()\n    .addResourceSpans(resourceSpans)\n    .build()\n}\n\nprivate fun createExportRequestWithExceptionEvent(\n  serviceName: String,\n  traceId: String,\n  spanId: String,\n  spanName: String,\n  exceptionType: String,\n  exceptionMessage: String,\n  exceptionStacktrace: String\n): ExportTraceServiceRequest {\n  val resource = Resource\n    .newBuilder()\n    .addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(\"service.name\")\n        .setValue(AnyValue.newBuilder().setStringValue(serviceName))\n    ).build()\n\n  val exceptionEvent = Span.Event\n    .newBuilder()\n    .setName(TracingConstants.OTEL_EXCEPTION_EVENT_NAME)\n    .setTimeUnixNano(System.nanoTime())\n    .addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(TracingConstants.OTEL_EXCEPTION_TYPE_ATTRIBUTE)\n        .setValue(AnyValue.newBuilder().setStringValue(exceptionType))\n    ).addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(TracingConstants.OTEL_EXCEPTION_MESSAGE_ATTRIBUTE)\n        .setValue(AnyValue.newBuilder().setStringValue(exceptionMessage))\n    ).addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(TracingConstants.OTEL_EXCEPTION_STACKTRACE_ATTRIBUTE)\n        .setValue(AnyValue.newBuilder().setStringValue(exceptionStacktrace))\n    ).build()\n\n  val span = Span\n    .newBuilder()\n    .setTraceId(hexStringToByteString(traceId))\n    .setSpanId(hexStringToByteString(spanId))\n    .setName(spanName)\n    .setStartTimeUnixNano(System.nanoTime())\n    .setEndTimeUnixNano(System.nanoTime() + 1_000_000)\n    .setStatus(\n      Status\n        .newBuilder()\n        .setCodeValue(TracingConstants.OTEL_STATUS_CODE_ERROR)\n        .setMessage(exceptionMessage)\n    ).addEvents(exceptionEvent)\n    .build()\n\n  val scopeSpans = ScopeSpans\n    .newBuilder()\n    .addSpans(span)\n    .build()\n\n  val resourceSpans = ResourceSpans\n    .newBuilder()\n    .setResource(resource)\n    .addScopeSpans(scopeSpans)\n    .build()\n\n  return ExportTraceServiceRequest\n    .newBuilder()\n    .addResourceSpans(resourceSpans)\n    .build()\n}\n\nprivate fun createExportRequestWithArrayAttribute(\n  serviceName: String,\n  traceId: String,\n  spanId: String,\n  spanName: String,\n  attributeKey: String,\n  values: List<String>\n): ExportTraceServiceRequest {\n  val resource = Resource\n    .newBuilder()\n    .addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(\"service.name\")\n        .setValue(AnyValue.newBuilder().setStringValue(serviceName))\n    ).build()\n\n  val arrayValue = ArrayValue\n    .newBuilder()\n    .addAllValues(values.map { AnyValue.newBuilder().setStringValue(it).build() })\n    .build()\n\n  val span = Span\n    .newBuilder()\n    .setTraceId(hexStringToByteString(traceId))\n    .setSpanId(hexStringToByteString(spanId))\n    .setName(spanName)\n    .setStartTimeUnixNano(System.nanoTime())\n    .setEndTimeUnixNano(System.nanoTime() + 1_000_000)\n    .addAttributes(\n      KeyValue\n        .newBuilder()\n        .setKey(attributeKey)\n        .setValue(\n          AnyValue\n            .newBuilder()\n            .setArrayValue(arrayValue)\n            .build()\n        )\n    ).build()\n\n  val scopeSpans = ScopeSpans\n    .newBuilder()\n    .addSpans(span)\n    .build()\n\n  val resourceSpans = ResourceSpans\n    .newBuilder()\n    .setResource(resource)\n    .addScopeSpans(scopeSpans)\n    .build()\n\n  return ExportTraceServiceRequest\n    .newBuilder()\n    .addResourceSpans(resourceSpans)\n    .build()\n}\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/SpanEventListenerTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport com.trendyol.stove.reporting.SpanEventListener\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.shouldBe\n\nclass SpanEventListenerTest :\n  FunSpec({\n\n    test(\"listener receives span events on record\") {\n      val collector = StoveTraceCollector()\n      val received = mutableListOf<SpanInfo>()\n      val listener = object : SpanEventListener {\n        override fun onSpanRecorded(span: SpanInfo) {\n          received.add(span)\n        }\n      }\n\n      collector.addSpanListener(listener)\n      val span = createSpan(traceId = \"trace-1\", spanId = \"span-1\")\n      collector.record(span)\n\n      received shouldHaveSize 1\n      received[0].spanId shouldBe \"span-1\"\n    }\n\n    test(\"listener receives events from recordAll\") {\n      val collector = StoveTraceCollector()\n      val received = mutableListOf<String>()\n      val listener = object : SpanEventListener {\n        override fun onSpanRecorded(span: SpanInfo) {\n          received.add(span.spanId)\n        }\n      }\n\n      collector.addSpanListener(listener)\n      collector.recordAll(\n        listOf(\n          createSpan(traceId = \"trace-1\", spanId = \"span-1\"),\n          createSpan(traceId = \"trace-1\", spanId = \"span-2\")\n        )\n      )\n\n      received shouldBe listOf(\"span-1\", \"span-2\")\n    }\n\n    test(\"throwing listener does not break collector or other listeners\") {\n      val collector = StoveTraceCollector()\n      val received = mutableListOf<String>()\n\n      collector.addSpanListener(object : SpanEventListener {\n        override fun onSpanRecorded(span: SpanInfo) {\n          error(\"boom\")\n        }\n      })\n      collector.addSpanListener(object : SpanEventListener {\n        override fun onSpanRecorded(span: SpanInfo) {\n          received.add(span.spanId)\n        }\n      })\n\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-1\"))\n\n      received shouldHaveSize 1\n      received[0] shouldBe \"span-1\"\n      // Verify the span was still recorded despite listener failure\n      collector.getTrace(\"trace-1\") shouldHaveSize 1\n    }\n\n    test(\"removed listener stops receiving events\") {\n      val collector = StoveTraceCollector()\n      val received = mutableListOf<String>()\n      val listener = object : SpanEventListener {\n        override fun onSpanRecorded(span: SpanInfo) {\n          received.add(span.spanId)\n        }\n      }\n\n      collector.addSpanListener(listener)\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-1\"))\n\n      collector.removeSpanListener(listener)\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-2\"))\n\n      received shouldHaveSize 1\n      received[0] shouldBe \"span-1\"\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span123\",\n  parentSpanId: String? = null,\n  operationName: String = \"test.operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 1_000_000_000L,\n  endTimeNanos: Long = 1_100_000_000L,\n  status: SpanStatus = SpanStatus.OK\n) = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = serviceName,\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status\n)\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/SpanInfoTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass SpanInfoTest :\n  FunSpec({\n\n    test(\"durationMs should calculate correctly\") {\n      val span = createSpan(\n        startTimeNanos = 1_000_000_000L,\n        endTimeNanos = 1_500_000_000L\n      )\n\n      span.durationMs shouldBe 500L\n    }\n\n    test(\"durationNanos should return correct value\") {\n      val span = createSpan(\n        startTimeNanos = 1_000_000_000L,\n        endTimeNanos = 1_500_000_000L\n      )\n\n      span.durationNanos shouldBe 500_000_000L\n    }\n\n    test(\"isFailed should return true for ERROR status\") {\n      val span = createSpan(status = SpanStatus.ERROR)\n\n      span.isFailed shouldBe true\n      span.isSuccess shouldBe false\n    }\n\n    test(\"isSuccess should return true for OK status\") {\n      val span = createSpan(status = SpanStatus.OK)\n\n      span.isSuccess shouldBe true\n      span.isFailed shouldBe false\n    }\n\n    test(\"UNSET status should not be failed or success\") {\n      val span = createSpan(status = SpanStatus.UNSET)\n\n      span.isFailed shouldBe false\n      span.isSuccess shouldBe false\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span123\",\n  parentSpanId: String? = null,\n  operationName: String = \"test.operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 1_000_000_000L,\n  endTimeNanos: Long = 1_100_000_000L,\n  status: SpanStatus = SpanStatus.OK,\n  attributes: Map<String, String> = emptyMap(),\n  exception: ExceptionInfo? = null\n) = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = serviceName,\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status,\n  attributes = attributes,\n  exception = exception\n)\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/SpanTreeTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.nulls.shouldBeNull\nimport io.kotest.matchers.nulls.shouldNotBeNull\nimport io.kotest.matchers.shouldBe\n\nclass SpanTreeTest :\n  FunSpec({\n\n    test(\"build should return null for empty list\") {\n      val tree = SpanTree.build(emptyList())\n\n      tree.shouldBeNull()\n    }\n\n    test(\"build should create single node for single span\") {\n      val span = createSpan(spanId = \"root\", parentSpanId = null)\n\n      val tree = SpanTree.build(listOf(span))\n\n      tree.shouldNotBeNull()\n      tree.span.spanId shouldBe \"root\"\n      tree.children.shouldHaveSize(0)\n    }\n\n    test(\"build should create proper parent-child relationships\") {\n      val spans = listOf(\n        createSpan(spanId = \"root\", parentSpanId = null),\n        createSpan(spanId = \"child1\", parentSpanId = \"root\"),\n        createSpan(spanId = \"child2\", parentSpanId = \"root\"),\n        createSpan(spanId = \"grandchild\", parentSpanId = \"child1\")\n      )\n\n      val tree = SpanTree.build(spans)\n\n      tree.shouldNotBeNull()\n      tree.span.spanId shouldBe \"root\"\n      tree.children shouldHaveSize 2\n\n      val child1 = tree.children.find { it.span.spanId == \"child1\" }\n      child1.shouldNotBeNull()\n      child1.children shouldHaveSize 1\n      child1.children[0].span.spanId shouldBe \"grandchild\"\n    }\n\n    test(\"build should sort children by start time\") {\n      val spans = listOf(\n        createSpan(spanId = \"root\", parentSpanId = null, startTimeNanos = 1000),\n        createSpan(spanId = \"child3\", parentSpanId = \"root\", startTimeNanos = 3000),\n        createSpan(spanId = \"child1\", parentSpanId = \"root\", startTimeNanos = 1000),\n        createSpan(spanId = \"child2\", parentSpanId = \"root\", startTimeNanos = 2000)\n      )\n\n      val tree = SpanTree.build(spans)\n\n      tree.shouldNotBeNull()\n      tree.children[0].span.spanId shouldBe \"child1\"\n      tree.children[1].span.spanId shouldBe \"child2\"\n      tree.children[2].span.spanId shouldBe \"child3\"\n    }\n\n    test(\"SpanNode.hasFailedDescendants should detect failures in subtree\") {\n      val spans = listOf(\n        createSpan(spanId = \"root\", parentSpanId = null, status = SpanStatus.OK),\n        createSpan(spanId = \"child\", parentSpanId = \"root\", status = SpanStatus.OK),\n        createSpan(spanId = \"grandchild\", parentSpanId = \"child\", status = SpanStatus.ERROR)\n      )\n\n      val tree = SpanTree.build(spans)\n\n      tree.shouldNotBeNull()\n      tree.hasFailedDescendants shouldBe true\n      tree.span.isFailed shouldBe false\n    }\n\n    test(\"SpanNode.depth should calculate correct depth\") {\n      val spans = listOf(\n        createSpan(spanId = \"root\", parentSpanId = null),\n        createSpan(spanId = \"child\", parentSpanId = \"root\"),\n        createSpan(spanId = \"grandchild\", parentSpanId = \"child\")\n      )\n\n      val tree = SpanTree.build(spans)\n\n      tree.shouldNotBeNull()\n      tree.depth shouldBe 3\n    }\n\n    test(\"SpanNode.spanCount should count all nodes\") {\n      val spans = listOf(\n        createSpan(spanId = \"root\", parentSpanId = null),\n        createSpan(spanId = \"child1\", parentSpanId = \"root\"),\n        createSpan(spanId = \"child2\", parentSpanId = \"root\"),\n        createSpan(spanId = \"grandchild\", parentSpanId = \"child1\")\n      )\n\n      val tree = SpanTree.build(spans)\n\n      tree.shouldNotBeNull()\n      tree.spanCount shouldBe 4\n    }\n\n    test(\"SpanNode.findFailurePoint should find deepest failure\") {\n      val spans = listOf(\n        createSpan(spanId = \"root\", parentSpanId = null, status = SpanStatus.OK),\n        createSpan(spanId = \"child\", parentSpanId = \"root\", status = SpanStatus.ERROR),\n        createSpan(spanId = \"grandchild\", parentSpanId = \"child\", status = SpanStatus.ERROR)\n      )\n\n      val tree = SpanTree.build(spans)\n\n      tree.shouldNotBeNull()\n      val failurePoint = tree.findFailurePoint()\n      failurePoint.shouldNotBeNull()\n      failurePoint.span.spanId shouldBe \"grandchild\"\n    }\n\n    test(\"SpanNode.flatten should return all spans in order\") {\n      val spans = listOf(\n        createSpan(spanId = \"root\", parentSpanId = null),\n        createSpan(spanId = \"child1\", parentSpanId = \"root\"),\n        createSpan(spanId = \"child2\", parentSpanId = \"root\")\n      )\n\n      val tree = SpanTree.build(spans)\n\n      tree.shouldNotBeNull()\n      val flattened = tree.flatten()\n      flattened shouldHaveSize 3\n      flattened[0].spanId shouldBe \"root\"\n    }\n\n    test(\"findSpan should locate span by predicate\") {\n      val spans = listOf(\n        createSpan(spanId = \"root\", parentSpanId = null, operationName = \"root.op\"),\n        createSpan(spanId = \"child\", parentSpanId = \"root\", operationName = \"child.op\")\n      )\n\n      val tree = SpanTree.build(spans)!!\n\n      val found = SpanTree.findSpan(tree) { it.operationName == \"child.op\" }\n\n      found.shouldNotBeNull()\n      found.span.spanId shouldBe \"child\"\n    }\n\n    test(\"filterSpans should return all matching spans\") {\n      val spans = listOf(\n        createSpan(spanId = \"root\", parentSpanId = null, status = SpanStatus.OK),\n        createSpan(spanId = \"child1\", parentSpanId = \"root\", status = SpanStatus.ERROR),\n        createSpan(spanId = \"child2\", parentSpanId = \"root\", status = SpanStatus.ERROR)\n      )\n\n      val tree = SpanTree.build(spans)!!\n\n      val failed = SpanTree.filterSpans(tree) { it.isFailed }\n\n      failed shouldHaveSize 2\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span123\",\n  parentSpanId: String? = null,\n  operationName: String = \"test.operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 1_000_000_000L,\n  endTimeNanos: Long = 1_100_000_000L,\n  status: SpanStatus = SpanStatus.OK\n) = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = serviceName,\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status\n)\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/StoveTraceCollectorTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldBeEmpty\nimport io.kotest.matchers.collections.shouldContain\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.nulls.shouldBeNull\nimport io.kotest.matchers.nulls.shouldNotBeNull\nimport io.kotest.matchers.shouldBe\n\nclass StoveTraceCollectorTest :\n  FunSpec({\n\n    test(\"registerTrace should create empty span list\") {\n      val collector = StoveTraceCollector()\n\n      collector.registerTrace(\"trace-1\", \"test-1\")\n\n      collector.getTrace(\"trace-1\").shouldBeEmpty()\n      collector.getTestId(\"trace-1\") shouldBe \"test-1\"\n    }\n\n    test(\"record should add span to the correct trace\") {\n      val collector = StoveTraceCollector()\n      collector.registerTrace(\"trace-1\", \"test-1\")\n      val span = createSpan(traceId = \"trace-1\", spanId = \"span-1\")\n\n      collector.record(span)\n\n      collector.getTrace(\"trace-1\") shouldHaveSize 1\n      collector.getTrace(\"trace-1\") shouldContain span\n    }\n\n    test(\"record should auto-create trace if not registered\") {\n      val collector = StoveTraceCollector()\n      val span = createSpan(traceId = \"trace-1\", spanId = \"span-1\")\n\n      collector.record(span)\n\n      collector.getTrace(\"trace-1\") shouldHaveSize 1\n    }\n\n    test(\"recordAll should add multiple spans\") {\n      val collector = StoveTraceCollector()\n      val spans = listOf(\n        createSpan(traceId = \"trace-1\", spanId = \"span-1\"),\n        createSpan(traceId = \"trace-1\", spanId = \"span-2\"),\n        createSpan(traceId = \"trace-1\", spanId = \"span-3\")\n      )\n\n      collector.recordAll(spans)\n\n      collector.getTrace(\"trace-1\") shouldHaveSize 3\n    }\n\n    test(\"getTraceTree should build span tree\") {\n      val collector = StoveTraceCollector()\n      val rootSpan = createSpan(traceId = \"trace-1\", spanId = \"root\", parentSpanId = null)\n      val childSpan = createSpan(traceId = \"trace-1\", spanId = \"child\", parentSpanId = \"root\")\n\n      collector.record(rootSpan)\n      collector.record(childSpan)\n\n      val tree = collector.getTraceTree(\"trace-1\")\n      tree.shouldNotBeNull()\n      tree.span.spanId shouldBe \"root\"\n      tree.children shouldHaveSize 1\n      tree.children[0].span.spanId shouldBe \"child\"\n    }\n\n    test(\"getTracesForTest should return all traces for a test\") {\n      val collector = StoveTraceCollector()\n      collector.registerTrace(\"trace-1\", \"test-1\")\n      collector.registerTrace(\"trace-2\", \"test-1\")\n      collector.registerTrace(\"trace-3\", \"test-2\")\n\n      val traces = collector.getTracesForTest(\"test-1\")\n\n      traces shouldHaveSize 2\n      traces shouldContain \"trace-1\"\n      traces shouldContain \"trace-2\"\n    }\n\n    test(\"getFailedSpans should return only failed spans\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"ok-1\", status = SpanStatus.OK))\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"error-1\", status = SpanStatus.ERROR))\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"ok-2\", status = SpanStatus.OK))\n\n      val failed = collector.getFailedSpans(\"trace-1\")\n\n      failed shouldHaveSize 1\n      failed[0].spanId shouldBe \"error-1\"\n    }\n\n    test(\"hasFailures should detect failures\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"ok-1\", status = SpanStatus.OK))\n\n      collector.hasFailures(\"trace-1\") shouldBe false\n\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"error-1\", status = SpanStatus.ERROR))\n\n      collector.hasFailures(\"trace-1\") shouldBe true\n    }\n\n    test(\"clear should remove trace data\") {\n      val collector = StoveTraceCollector()\n      collector.registerTrace(\"trace-1\", \"test-1\")\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-1\"))\n\n      collector.clear(\"trace-1\")\n\n      collector.getTrace(\"trace-1\").shouldBeEmpty()\n      collector.getTestId(\"trace-1\").shouldBeNull()\n    }\n\n    test(\"clearForTest should remove all traces for a test\") {\n      val collector = StoveTraceCollector()\n      collector.registerTrace(\"trace-1\", \"test-1\")\n      collector.registerTrace(\"trace-2\", \"test-1\")\n      collector.registerTrace(\"trace-3\", \"test-2\")\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-1\"))\n      collector.record(createSpan(traceId = \"trace-2\", spanId = \"span-2\"))\n      collector.record(createSpan(traceId = \"trace-3\", spanId = \"span-3\"))\n\n      collector.clearForTest(\"test-1\")\n\n      collector.getTrace(\"trace-1\").shouldBeEmpty()\n      collector.getTrace(\"trace-2\").shouldBeEmpty()\n      collector.getTrace(\"trace-3\") shouldHaveSize 1\n    }\n\n    test(\"clearAll should remove all data\") {\n      val collector = StoveTraceCollector()\n      collector.registerTrace(\"trace-1\", \"test-1\")\n      collector.registerTrace(\"trace-2\", \"test-2\")\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-1\"))\n      collector.record(createSpan(traceId = \"trace-2\", spanId = \"span-2\"))\n\n      collector.clearAll()\n\n      collector.traceCount() shouldBe 0\n      collector.totalSpanCount() shouldBe 0\n    }\n\n    test(\"spanCount should return correct count for trace\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-1\"))\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-2\"))\n      collector.record(createSpan(traceId = \"trace-2\", spanId = \"span-3\"))\n\n      collector.spanCount(\"trace-1\") shouldBe 2\n      collector.spanCount(\"trace-2\") shouldBe 1\n      collector.spanCount(\"trace-3\") shouldBe 0\n    }\n\n    test(\"totalSpanCount should return total across all traces\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-1\"))\n      collector.record(createSpan(traceId = \"trace-1\", spanId = \"span-2\"))\n      collector.record(createSpan(traceId = \"trace-2\", spanId = \"span-3\"))\n\n      collector.totalSpanCount() shouldBe 3\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span123\",\n  parentSpanId: String? = null,\n  operationName: String = \"test.operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 1_000_000_000L,\n  endTimeNanos: Long = 1_100_000_000L,\n  status: SpanStatus = SpanStatus.OK\n) = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = serviceName,\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status\n)\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/TraceReportBuilderTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport com.trendyol.stove.reporting.ReportEntry\nimport com.trendyol.stove.reporting.StoveTestContext\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.runBlocking\n\nclass TraceReportBuilderTest :\n  FunSpec({\n    test(\"buildFullReport includes stove report and trace tree\") {\n      val stove = Stove()\n      stove.applicationUnderTest(NoOpApplicationUnderTest())\n\n      val tracingSystem = TracingSystem(stove, TracingSystemOptions(TracingOptions().enabled()))\n      stove.getOrRegister(tracingSystem)\n\n      runBlocking { stove.run() }\n\n      val reporter = Stove.reporter()\n      reporter.startTest(StoveTestContext(\"test-id\", \"test-name\", \"spec\"))\n      reporter.record(\n        ReportEntry.failure(\n          system = \"Test\",\n          testId = \"test-id\",\n          action = \"failure\",\n          error = \"boom\"\n        )\n      )\n\n      val ctx = TraceContext.start(\"test-id\")\n      tracingSystem.collector.registerTrace(ctx.traceId, ctx.testId)\n      tracingSystem.collector.record(\n        span(\n          traceId = ctx.traceId,\n          spanId = \"root\",\n          parentSpanId = null,\n          operationName = \"root\",\n          startTimeNanos = 0,\n          endTimeNanos = 1_000_000,\n          status = SpanStatus.OK\n        )\n      )\n\n      val report = TraceReportBuilder.buildFullReport()\n\n      report shouldContain \"STOVE TEST EXECUTION REPORT\"\n      report shouldContain \"EXECUTION TRACE\"\n\n      TraceContext.clear()\n      stove.close()\n    }\n\n    test(\"buildFullReport returns empty when no failures and no trace\") {\n      val stove = Stove()\n      stove.applicationUnderTest(NoOpApplicationUnderTest())\n\n      runBlocking { stove.run() }\n\n      val report = TraceReportBuilder.buildFullReport()\n\n      report shouldBe \"\"\n\n      stove.close()\n    }\n  })\n\nprivate class NoOpApplicationUnderTest : ApplicationUnderTest<String> {\n  override suspend fun start(configurations: List<String>): String = \"context\"\n\n  override suspend fun stop() = Unit\n}\n\nprivate fun span(\n  traceId: String,\n  spanId: String,\n  parentSpanId: String?,\n  operationName: String,\n  startTimeNanos: Long,\n  endTimeNanos: Long,\n  status: SpanStatus\n): SpanInfo = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = \"service\",\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status\n)\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/TraceTreeRendererTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.string.shouldContain\nimport io.kotest.matchers.string.shouldNotContain\n\nclass TraceTreeRendererTest :\n  FunSpec({\n\n    test(\"render should show operation name and duration\") {\n      val span = createSpan(\n        spanId = \"root\",\n        operationName = \"OrderService.createOrder\",\n        startTimeNanos = 0,\n        endTimeNanos = 100_000_000\n      )\n      val tree = SpanNode(span)\n\n      val output = TraceTreeRenderer.render(tree)\n\n      output shouldContain \"OrderService.createOrder\"\n      output shouldContain \"[100ms]\"\n    }\n\n    test(\"render should show checkmark for successful span\") {\n      val span = createSpan(status = SpanStatus.OK)\n      val tree = SpanNode(span)\n\n      val output = TraceTreeRenderer.render(tree)\n\n      output shouldContain \"✓\"\n      output shouldNotContain \"✗\"\n    }\n\n    test(\"render should show X for failed span\") {\n      val span = createSpan(status = SpanStatus.ERROR)\n      val tree = SpanNode(span)\n\n      val output = TraceTreeRenderer.render(tree)\n\n      output shouldContain \"✗\"\n    }\n\n    test(\"render should mark failure point\") {\n      val span = createSpan(\n        status = SpanStatus.ERROR,\n        exception = ExceptionInfo(\"RuntimeException\", \"Something went wrong\", listOf(\"at Test.kt:10\"))\n      )\n      val tree = SpanNode(span)\n\n      val output = TraceTreeRenderer.render(tree)\n\n      output shouldContain \"◄── FAILURE POINT\"\n      output shouldContain \"Error: RuntimeException: Something went wrong\"\n    }\n\n    test(\"render should show relevant attributes\") {\n      val span = createSpan(\n        attributes = mapOf(\n          \"db.system\" to \"postgresql\",\n          \"db.statement\" to \"SELECT * FROM users\",\n          \"internal.flag\" to \"true\"\n        )\n      )\n      val tree = SpanNode(span)\n\n      val output = TraceTreeRenderer.render(tree, includeAttributes = true)\n\n      output shouldContain \"db.system: postgresql\"\n      output shouldContain \"db.statement: SELECT * FROM users\"\n      output shouldNotContain \"internal.flag\"\n    }\n\n    test(\"render should show nested hierarchy\") {\n      val grandchild = SpanNode(createSpan(spanId = \"grandchild\", operationName = \"Repository.save\"))\n      val child = SpanNode(createSpan(spanId = \"child\", operationName = \"Service.process\"), listOf(grandchild))\n      val root = SpanNode(createSpan(spanId = \"root\", operationName = \"Controller.handle\"), listOf(child))\n\n      val output = TraceTreeRenderer.render(root)\n\n      output shouldContain \"Controller.handle\"\n      output shouldContain \"Service.process\"\n      output shouldContain \"Repository.save\"\n    }\n\n    test(\"renderCompact should produce condensed output\") {\n      val child = SpanNode(createSpan(operationName = \"child.op\", startTimeNanos = 0, endTimeNanos = 30_000_000))\n      val root =\n        SpanNode(createSpan(operationName = \"root.op\", startTimeNanos = 0, endTimeNanos = 50_000_000), listOf(child))\n\n      val output = TraceTreeRenderer.renderCompact(root)\n\n      output shouldContain \"✓ root.op (50ms)\"\n      output shouldContain \"✓ child.op (30ms)\"\n    }\n\n    test(\"renderSummary should show statistics\") {\n      val failedChild = SpanNode(\n        createSpan(\n          spanId = \"child\",\n          startTimeNanos = 10_000_000,\n          endTimeNanos = 50_000_000,\n          status = SpanStatus.ERROR,\n          exception = ExceptionInfo(\"TestException\", \"Test error\")\n        )\n      )\n      val root = SpanNode(\n        createSpan(\n          spanId = \"root\",\n          startTimeNanos = 0,\n          endTimeNanos = 100_000_000,\n          status = SpanStatus.OK\n        ),\n        listOf(failedChild)\n      )\n\n      val output = TraceTreeRenderer.renderSummary(root)\n\n      output shouldContain \"Total spans: 2\"\n      output shouldContain \"Failed spans: 1\"\n      output shouldContain \"Total duration: 100ms\"\n      output shouldContain \"Max depth: 2\"\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span123\",\n  parentSpanId: String? = null,\n  operationName: String = \"test.operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 1_000_000_000L,\n  endTimeNanos: Long = 1_100_000_000L,\n  status: SpanStatus = SpanStatus.OK,\n  attributes: Map<String, String> = emptyMap(),\n  exception: ExceptionInfo? = null\n) = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = serviceName,\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status,\n  attributes = attributes,\n  exception = exception\n)\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/TraceValidationTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass TraceValidationTest :\n  FunSpec({\n    test(\"should validate spans and counts\") {\n      val collector = StoveTraceCollector()\n      val traceId = \"trace-1\"\n      collector.registerTrace(traceId, \"test-1\")\n\n      collector.recordAll(\n        listOf(\n          span(\n            traceId = traceId,\n            spanId = \"root\",\n            parentSpanId = null,\n            operationName = \"root-op\",\n            startTimeNanos = 0,\n            endTimeNanos = 10_000_000,\n            status = SpanStatus.OK\n          ),\n          span(\n            traceId = traceId,\n            spanId = \"child\",\n            parentSpanId = \"root\",\n            operationName = \"child-op\",\n            startTimeNanos = 1_000_000,\n            endTimeNanos = 5_000_000,\n            status = SpanStatus.ERROR,\n            attributes = mapOf(\"key\" to \"value\")\n          )\n        )\n      )\n\n      val validation = TraceValidationDsl(collector, traceId)\n\n      validation.shouldContainSpan(\"root\")\n      validation.shouldContainSpanMatching { it.operationName == \"child-op\" }\n      validation.shouldHaveFailedSpan(\"child\")\n      validation.spanCountShouldBe(2)\n      validation.spanCountShouldBeAtLeast(1)\n      validation.spanCountShouldBeAtMost(2)\n      validation.executionTimeShouldBeLessThan(20.milliseconds)\n      validation.executionTimeShouldBeGreaterThan(5.milliseconds)\n      validation.shouldHaveSpanWithAttribute(\"key\", \"value\")\n      validation.shouldHaveSpanWithAttributeContaining(\"key\", \"val\")\n\n      validation.getSpanCount() shouldBe 2\n      validation.getFailedSpanCount() shouldBe 1\n      validation.findSpanByName(\"root\")?.operationName shouldBe \"root-op\"\n      validation.getTotalDuration() shouldBe 10.milliseconds\n      validation.spanTree() shouldNotBe null\n      validation.renderTree() shouldNotBe \"No spans in trace\"\n      validation.renderSummary() shouldNotBe \"No spans in trace\"\n    }\n\n    test(\"should fail when span expectations are not met\") {\n      val collector = StoveTraceCollector()\n      val traceId = \"trace-2\"\n      collector.registerTrace(traceId, \"test-2\")\n      collector.record(\n        span(\n          traceId = traceId,\n          spanId = \"root\",\n          parentSpanId = null,\n          operationName = \"root-op\",\n          startTimeNanos = 0,\n          endTimeNanos = 1_000_000,\n          status = SpanStatus.OK\n        )\n      )\n\n      val validation = TraceValidationDsl(collector, traceId)\n\n      shouldThrow<IllegalArgumentException> {\n        validation.shouldNotContainSpan(\"root\")\n      }\n\n      shouldThrow<IllegalArgumentException> {\n        validation.shouldHaveFailedSpan(\"root\")\n      }\n\n      shouldThrow<IllegalArgumentException> {\n        validation.executionTimeShouldBeGreaterThan(5.milliseconds)\n      }\n    }\n  })\n\nprivate fun span(\n  traceId: String,\n  spanId: String,\n  parentSpanId: String?,\n  operationName: String,\n  startTimeNanos: Long,\n  endTimeNanos: Long,\n  status: SpanStatus,\n  attributes: Map<String, String> = emptyMap()\n): SpanInfo = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = \"service\",\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status,\n  attributes = attributes\n)\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/TracingDslTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.nulls.shouldNotBeNull\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass TracingDslTest :\n  FunSpec({\n\n    test(\"shouldContainSpan should pass when span exists\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"t1\", operationName = \"OrderService.create\"))\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      dsl.shouldContainSpan(\"OrderService\")\n      dsl.shouldContainSpan(\"create\")\n    }\n\n    test(\"shouldContainSpan should fail when span not found\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"t1\", operationName = \"OrderService.create\"))\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      val exception = shouldThrow<IllegalArgumentException> {\n        dsl.shouldContainSpan(\"PaymentService\")\n      }\n      exception.message shouldContain \"Expected span containing 'PaymentService'\"\n    }\n\n    test(\"shouldNotContainSpan should pass when span absent\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"t1\", operationName = \"OrderService.create\"))\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      dsl.shouldNotContainSpan(\"PaymentService\")\n    }\n\n    test(\"shouldNotHaveFailedSpans should pass when no failures\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"t1\", status = SpanStatus.OK))\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      dsl.shouldNotHaveFailedSpans()\n    }\n\n    test(\"shouldNotHaveFailedSpans should fail when failures exist\") {\n      val collector = StoveTraceCollector()\n      collector.record(\n        createSpan(\n          traceId = \"t1\",\n          operationName = \"failed.op\",\n          status = SpanStatus.ERROR,\n          exception = ExceptionInfo(\"TestException\", \"test error\")\n        )\n      )\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      val exception = shouldThrow<IllegalArgumentException> {\n        dsl.shouldNotHaveFailedSpans()\n      }\n      exception.message shouldContain \"Expected no failed spans\"\n    }\n\n    test(\"shouldHaveFailedSpan should pass when matching failure exists\") {\n      val collector = StoveTraceCollector()\n      collector.record(\n        createSpan(\n          traceId = \"t1\",\n          operationName = \"PaymentService.charge\",\n          status = SpanStatus.ERROR\n        )\n      )\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      dsl.shouldHaveFailedSpan(\"PaymentService\")\n    }\n\n    test(\"executionTimeShouldBeLessThan should validate duration\") {\n      val collector = StoveTraceCollector()\n      collector.record(\n        createSpan(\n          traceId = \"t1\",\n          startTimeNanos = 0,\n          endTimeNanos = 50_000_000 // 50ms\n        )\n      )\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      dsl.executionTimeShouldBeLessThan(100.milliseconds)\n\n      shouldThrow<IllegalArgumentException> {\n        dsl.executionTimeShouldBeLessThan(10.milliseconds)\n      }\n    }\n\n    test(\"spanCountShouldBe should validate exact count\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"t1\", spanId = \"s1\"))\n      collector.record(createSpan(traceId = \"t1\", spanId = \"s2\"))\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      dsl.spanCountShouldBe(2)\n    }\n\n    test(\"shouldHaveSpanWithAttribute should find attribute\") {\n      val collector = StoveTraceCollector()\n      collector.record(\n        createSpan(\n          traceId = \"t1\",\n          attributes = mapOf(\"db.system\" to \"postgresql\")\n        )\n      )\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      dsl.shouldHaveSpanWithAttribute(\"db.system\", \"postgresql\")\n    }\n\n    test(\"findSpanByName should locate span\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"t1\", operationName = \"OrderService.create\"))\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      val found = dsl.findSpanByName(\"OrderService\")\n      found.shouldNotBeNull()\n      found.operationName shouldBe \"OrderService.create\"\n    }\n\n    test(\"spanTree should return span tree\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"t1\", spanId = \"root\", parentSpanId = null))\n      collector.record(createSpan(traceId = \"t1\", spanId = \"child\", parentSpanId = \"root\"))\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      val tree = dsl.spanTree()\n      tree.shouldNotBeNull()\n      tree.spanCount shouldBe 2\n    }\n\n    test(\"renderTree should produce output\") {\n      val collector = StoveTraceCollector()\n      collector.record(createSpan(traceId = \"t1\", spanId = \"root\", operationName = \"root.op\"))\n\n      val dsl = TraceValidationDsl(collector, \"t1\")\n\n      val rendered = dsl.renderTree()\n      rendered shouldContain \"root.op\"\n    }\n\n    test(\"tracing DSL should configure options\") {\n      val options = TracingOptions().apply {\n        enabled()\n        maxSpansPerTrace(500)\n        enableSpanReceiver(port = 4318)\n      }\n\n      options.enabled shouldBe true\n      options.maxSpansPerTrace shouldBe 500\n      options.spanReceiverEnabled shouldBe true\n      options.spanReceiverPort shouldBe 4318\n    }\n  })\n\nprivate fun createSpan(\n  traceId: String = \"trace123\",\n  spanId: String = \"span123\",\n  parentSpanId: String? = null,\n  operationName: String = \"test.operation\",\n  serviceName: String = \"test-service\",\n  startTimeNanos: Long = 1_000_000_000L,\n  endTimeNanos: Long = 1_100_000_000L,\n  status: SpanStatus = SpanStatus.OK,\n  attributes: Map<String, String> = emptyMap(),\n  exception: ExceptionInfo? = null\n) = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = serviceName,\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status,\n  attributes = attributes,\n  exception = exception\n)\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/TracingOptionsTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlin.time.Duration.Companion.seconds\n\nclass TracingOptionsTest :\n  FunSpec({\n    test(\"should configure tracing options\") {\n      val options = TracingOptions()\n        .enabled()\n        .spanCollectionTimeout(2.seconds)\n        .spanFilter { it.operationName == \"op\" }\n        .maxSpansPerTrace(99)\n        .enableSpanReceiver(port = 5555)\n\n      options.enabled shouldBe true\n      options.spanCollectionTimeout shouldBe 2.seconds\n      options.maxSpansPerTrace shouldBe 99\n      options.spanReceiverEnabled shouldBe true\n      options.spanReceiverPort shouldBe 5555\n      options.spanFilter(SpanInfo(\"t\", \"s\", null, \"op\", \"svc\", 0, 1, SpanStatus.OK)) shouldBe true\n    }\n\n    test(\"copy should duplicate current values\") {\n      val original = TracingOptions()\n        .enabled()\n        .spanCollectionTimeout(3.seconds)\n        .maxSpansPerTrace(7)\n        .enableSpanReceiver(port = 7777)\n\n      val copy = original.copy()\n\n      copy.enabled shouldBe true\n      copy.spanCollectionTimeout shouldBe 3.seconds\n      copy.maxSpansPerTrace shouldBe 7\n      copy.spanReceiverEnabled shouldBe true\n      copy.spanReceiverPort shouldBe 7777\n    }\n  })\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/TracingSystemTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.system.Stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport kotlinx.coroutines.runBlocking\n\nclass TracingSystemTest :\n  FunSpec({\n    test(\"ensureTraceStarted registers context and validation\") {\n      TraceContext.clear()\n      val stove = Stove()\n      val system = TracingSystem(stove, TracingSystemOptions(TracingOptions().enabled()))\n\n      val ctx = runBlocking { system.ensureTraceStarted() }\n\n      TraceContext.current() shouldNotBe null\n      system.validation(ctx.traceId).getSpanCount() shouldBe 0\n      system.collector.traceCount() shouldBe 1\n\n      system.endTrace()\n      TraceContext.current() shouldBe null\n    }\n\n    test(\"getTraceVisualizationForCurrentTest returns visualization for current trace\") {\n      TraceContext.clear()\n      val stove = Stove()\n      val system = TracingSystem(stove, TracingSystemOptions(TracingOptions().enabled()))\n\n      val ctx = runBlocking { system.ensureTraceStarted() }\n\n      system.collector.record(\n        span(\n          traceId = ctx.traceId,\n          spanId = \"root\",\n          parentSpanId = null,\n          operationName = \"root\",\n          startTimeNanos = 0,\n          endTimeNanos = 1_000_000,\n          status = SpanStatus.OK\n        )\n      )\n\n      val visualization = system.getTraceVisualizationForCurrentTest(waitTimeMs = 10)\n      visualization.getOrElse { null }?.traceId shouldBe ctx.traceId\n    }\n\n    test(\"getTraceVisualizationForCurrentTest falls back to most recent trace\") {\n      TraceContext.clear()\n      val stove = Stove()\n      val system = TracingSystem(stove, TracingSystemOptions(TracingOptions().enabled()))\n\n      runBlocking { system.ensureTraceStarted() }\n\n      system.collector.record(\n        span(\n          traceId = \"other-trace\",\n          spanId = \"root\",\n          parentSpanId = null,\n          operationName = \"root\",\n          startTimeNanos = 10,\n          endTimeNanos = 20,\n          status = SpanStatus.OK\n        )\n      )\n\n      val visualization = system.getTraceVisualizationForCurrentTest(waitTimeMs = 1)\n      visualization.getOrElse { null }?.traceId shouldBe \"other-trace\"\n    }\n\n    test(\"getTraceVisualizationForCurrentTest prefers trace with matching test ID attribute\") {\n      TraceContext.clear()\n      val stove = Stove()\n      val system = TracingSystem(stove, TracingSystemOptions(TracingOptions().enabled()))\n\n      val ctx = system.ensureTraceStarted()\n\n      system.collector.record(\n        span(\n          traceId = \"trace-with-test-id\",\n          spanId = \"root\",\n          parentSpanId = null,\n          operationName = \"root\",\n          startTimeNanos = 10,\n          endTimeNanos = 20,\n          status = SpanStatus.OK,\n          attributes = mapOf(\n            \"http.request.header.x_stove_test_id\" to \"[\\\"${ctx.testId}\\\"]\"\n          )\n        )\n      )\n\n      // More recent span from another test should not win over matching test-id trace.\n      system.collector.record(\n        span(\n          traceId = \"unrelated-recent-trace\",\n          spanId = \"root\",\n          parentSpanId = null,\n          operationName = \"root\",\n          startTimeNanos = 30,\n          endTimeNanos = 40,\n          status = SpanStatus.OK,\n          attributes = mapOf(\n            \"http.request.header.x_stove_test_id\" to \"[\\\"other-test-id\\\"]\"\n          )\n        )\n      )\n\n      val visualization = system.getTraceVisualizationForCurrentTest(waitTimeMs = 1)\n      visualization.getOrElse { null }?.traceId shouldBe \"trace-with-test-id\"\n    }\n\n    test(\"getTraceVisualizationForCurrentTest matches stove.test.id attributes encoded as key-value payload\") {\n      TraceContext.clear()\n      val stove = Stove()\n      val system = TracingSystem(stove, TracingSystemOptions(TracingOptions().enabled()))\n\n      val ctx = system.ensureTraceStarted()\n\n      system.collector.record(\n        span(\n          traceId = \"trace-with-baggage-test-id\",\n          spanId = \"root\",\n          parentSpanId = null,\n          operationName = \"root\",\n          startTimeNanos = 10,\n          endTimeNanos = 20,\n          status = SpanStatus.OK,\n          attributes = mapOf(\n            \"otel.baggage.stove.test.id\" to \"{\\\"stove.test.id\\\":\\\"${ctx.testId}\\\"}\"\n          )\n        )\n      )\n\n      // More recent trace from a different test should not be selected.\n      system.collector.record(\n        span(\n          traceId = \"unrelated-newer-trace\",\n          spanId = \"root\",\n          parentSpanId = null,\n          operationName = \"root\",\n          startTimeNanos = 30,\n          endTimeNanos = 40,\n          status = SpanStatus.OK,\n          attributes = mapOf(\n            \"otel.baggage.stove.test.id\" to \"{\\\"stove.test.id\\\":\\\"other-test-id\\\"}\"\n          )\n        )\n      )\n\n      val visualization = system.getTraceVisualizationForCurrentTest(waitTimeMs = 1)\n      visualization.getOrElse { null }?.traceId shouldBe \"trace-with-baggage-test-id\"\n    }\n\n    test(\"stop clears traces and context\") {\n      TraceContext.clear()\n      val stove = Stove()\n      val system = TracingSystem(stove, TracingSystemOptions(TracingOptions().enabled()))\n\n      runBlocking { system.ensureTraceStarted() }\n      system.collector.record(\n        span(\n          traceId = TraceContext.current()!!.traceId,\n          spanId = \"root\",\n          parentSpanId = null,\n          operationName = \"root\",\n          startTimeNanos = 0,\n          endTimeNanos = 1,\n          status = SpanStatus.OK\n        )\n      )\n\n      runBlocking { system.stop() }\n\n      TraceContext.current() shouldBe null\n      system.collector.traceCount() shouldBe 0\n    }\n  })\n\nprivate fun span(\n  traceId: String,\n  spanId: String,\n  parentSpanId: String?,\n  operationName: String,\n  startTimeNanos: Long,\n  endTimeNanos: Long,\n  status: SpanStatus,\n  attributes: Map<String, String> = emptyMap()\n): SpanInfo = SpanInfo(\n  traceId = traceId,\n  spanId = spanId,\n  parentSpanId = parentSpanId,\n  operationName = operationName,\n  serviceName = \"service\",\n  startTimeNanos = startTimeNanos,\n  endTimeNanos = endTimeNanos,\n  status = status,\n  attributes = attributes\n)\n"
  },
  {
    "path": "lib/stove-tracing/src/test/kotlin/com/trendyol/stove/tracing/TracingValidationScopeTest.kt",
    "content": "package com.trendyol.stove.tracing\n\nimport arrow.core.None\nimport arrow.core.Some\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.shouldContain\nimport io.kotest.matchers.types.shouldBeInstanceOf\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass TracingValidationScopeTest :\n  FunSpec({\n\n    fun createScope(): Triple<StoveTraceCollector, TraceContext, TracingValidationScope> {\n      val collector = StoveTraceCollector()\n      val ctx = TraceContext.start(\"test-scope-1\")\n      collector.registerTrace(ctx.traceId, ctx.testId)\n\n      collector.recordAll(\n        listOf(\n          SpanInfo(\n            traceId = ctx.traceId,\n            spanId = \"root-span\",\n            parentSpanId = null,\n            operationName = \"root-op\",\n            serviceName = \"test-service\",\n            startTimeNanos = 0,\n            endTimeNanos = 10_000_000,\n            status = SpanStatus.OK,\n            attributes = mapOf(\"http.method\" to \"GET\", \"http.url\" to \"/api/users\")\n          ),\n          SpanInfo(\n            traceId = ctx.traceId,\n            spanId = \"child-span\",\n            parentSpanId = \"root-span\",\n            operationName = \"child-op\",\n            serviceName = \"test-service\",\n            startTimeNanos = 1_000_000,\n            endTimeNanos = 5_000_000,\n            status = SpanStatus.ERROR,\n            attributes = mapOf(\"db.system\" to \"postgresql\")\n          )\n        )\n      )\n\n      val validation = TraceValidationDsl(collector, ctx.traceId)\n      val scope = TracingValidationScope(ctx, validation, collector)\n      return Triple(collector, ctx, scope)\n    }\n\n    test(\"should expose trace context properties\") {\n      val (_, ctx, scope) = createScope()\n\n      scope.traceId shouldBe ctx.traceId\n      scope.rootSpanId shouldBe ctx.rootSpanId\n      scope.testId shouldBe ctx.testId\n      TraceContext.clear()\n    }\n\n    test(\"toTraceparent should return W3C format\") {\n      val (_, ctx, scope) = createScope()\n\n      val traceparent = scope.toTraceparent()\n      traceparent shouldBe \"00-${ctx.traceId}-${ctx.rootSpanId}-01\"\n      TraceContext.clear()\n    }\n\n    test(\"should delegate shouldContainSpan\") {\n      val (_, _, scope) = createScope()\n\n      scope.shouldContainSpan(\"root\")\n      scope.shouldContainSpan(\"child\")\n      TraceContext.clear()\n    }\n\n    test(\"should delegate shouldContainSpanMatching\") {\n      val (_, _, scope) = createScope()\n\n      scope.shouldContainSpanMatching { it.operationName == \"child-op\" }\n      TraceContext.clear()\n    }\n\n    test(\"should delegate shouldNotContainSpan\") {\n      val (_, _, scope) = createScope()\n\n      scope.shouldNotContainSpan(\"nonexistent\")\n      TraceContext.clear()\n    }\n\n    test(\"should delegate shouldNotHaveFailedSpans throws when there are failures\") {\n      val (_, _, scope) = createScope()\n\n      try {\n        scope.shouldNotHaveFailedSpans()\n        throw AssertionError(\"Expected exception\")\n      } catch (e: IllegalArgumentException) {\n        e.message shouldContain \"failed spans\"\n      }\n      TraceContext.clear()\n    }\n\n    test(\"should delegate shouldHaveFailedSpan\") {\n      val (_, _, scope) = createScope()\n\n      scope.shouldHaveFailedSpan(\"child\")\n      TraceContext.clear()\n    }\n\n    test(\"should delegate executionTimeShouldBeLessThan\") {\n      val (_, _, scope) = createScope()\n\n      scope.executionTimeShouldBeLessThan(20.milliseconds)\n      TraceContext.clear()\n    }\n\n    test(\"should delegate executionTimeShouldBeGreaterThan\") {\n      val (_, _, scope) = createScope()\n\n      scope.executionTimeShouldBeGreaterThan(5.milliseconds)\n      TraceContext.clear()\n    }\n\n    test(\"should delegate spanCountShouldBe\") {\n      val (_, _, scope) = createScope()\n\n      scope.spanCountShouldBe(2)\n      TraceContext.clear()\n    }\n\n    test(\"should delegate spanCountShouldBeAtLeast\") {\n      val (_, _, scope) = createScope()\n\n      scope.spanCountShouldBeAtLeast(1)\n      TraceContext.clear()\n    }\n\n    test(\"should delegate spanCountShouldBeAtMost\") {\n      val (_, _, scope) = createScope()\n\n      scope.spanCountShouldBeAtMost(5)\n      TraceContext.clear()\n    }\n\n    test(\"should delegate shouldHaveSpanWithAttribute\") {\n      val (_, _, scope) = createScope()\n\n      scope.shouldHaveSpanWithAttribute(\"http.method\", \"GET\")\n      TraceContext.clear()\n    }\n\n    test(\"should delegate shouldHaveSpanWithAttributeContaining\") {\n      val (_, _, scope) = createScope()\n\n      scope.shouldHaveSpanWithAttributeContaining(\"http.url\", \"/api\")\n      TraceContext.clear()\n    }\n\n    test(\"should delegate getSpanCount\") {\n      val (_, _, scope) = createScope()\n\n      scope.getSpanCount() shouldBe 2\n      TraceContext.clear()\n    }\n\n    test(\"should delegate getFailedSpans\") {\n      val (_, _, scope) = createScope()\n\n      scope.getFailedSpans().size shouldBe 1\n      scope.getFailedSpans().first().operationName shouldBe \"child-op\"\n      TraceContext.clear()\n    }\n\n    test(\"should delegate getFailedSpanCount\") {\n      val (_, _, scope) = createScope()\n\n      scope.getFailedSpanCount() shouldBe 1\n      TraceContext.clear()\n    }\n\n    test(\"should delegate findSpan returning Option\") {\n      val (_, _, scope) = createScope()\n\n      scope.findSpan { it.operationName == \"root-op\" }.shouldBeInstanceOf<Some<SpanInfo>>()\n      scope.findSpan { it.operationName == \"nonexistent\" } shouldBe None\n      TraceContext.clear()\n    }\n\n    test(\"should delegate findSpanByName returning Option\") {\n      val (_, _, scope) = createScope()\n\n      scope.findSpanByName(\"root\").shouldBeInstanceOf<Some<SpanInfo>>()\n      scope.findSpanByName(\"nonexistent\") shouldBe None\n      TraceContext.clear()\n    }\n\n    test(\"should delegate spanTree returning Option\") {\n      val (_, _, scope) = createScope()\n\n      scope.spanTree().shouldBeInstanceOf<Some<SpanNode>>()\n      TraceContext.clear()\n    }\n\n    test(\"should delegate getTotalDuration\") {\n      val (_, _, scope) = createScope()\n\n      scope.getTotalDuration() shouldBe 10.milliseconds\n      TraceContext.clear()\n    }\n\n    test(\"should delegate renderTree\") {\n      val (_, _, scope) = createScope()\n\n      scope.renderTree() shouldNotBe \"No spans in trace\"\n      scope.renderTree() shouldContain \"root-op\"\n      TraceContext.clear()\n    }\n\n    test(\"should delegate renderSummary\") {\n      val (_, _, scope) = createScope()\n\n      scope.renderSummary() shouldNotBe \"No spans in trace\"\n      TraceContext.clear()\n    }\n\n    test(\"waitForSpans should return spans immediately when they already exist\") {\n      val (_, _, scope) = createScope()\n\n      val spans = scope.waitForSpans(expectedCount = 2, timeoutMs = 1000)\n      spans.size shouldBe 2\n      TraceContext.clear()\n    }\n\n    test(\"getTraceVisualization should return visualization\") {\n      val (_, _, scope) = createScope()\n\n      val viz = scope.getTraceVisualization()\n      viz.traceId shouldBe scope.traceId\n      viz.testId shouldBe scope.testId\n      viz.totalSpans shouldBe 2\n      viz.failedSpans shouldBe 1\n      TraceContext.clear()\n    }\n\n    test(\"getAllTraceVisualizations should return all traces\") {\n      val (collector, ctx, scope) = createScope()\n\n      // Add another trace\n      val otherTraceId = \"other-trace-id\"\n      collector.registerTrace(otherTraceId, \"other-test\")\n      collector.record(\n        SpanInfo(\n          traceId = otherTraceId,\n          spanId = \"other-span\",\n          parentSpanId = null,\n          operationName = \"other-op\",\n          serviceName = \"other-service\",\n          startTimeNanos = 0,\n          endTimeNanos = 1_000_000,\n          status = SpanStatus.OK\n        )\n      )\n\n      val allViz = scope.getAllTraceVisualizations()\n      allViz.size shouldBe 2\n      TraceContext.clear()\n    }\n  })\n"
  },
  {
    "path": "lib/stove-wiremock/api/stove-wiremock.api",
    "content": "public final class com/trendyol/stove/wiremock/ExtensionsKt {\n\tpublic static final fun containsKey (Lcom/github/benmanes/caffeine/cache/Cache;Ljava/lang/Object;)Z\n}\n\npublic final class com/trendyol/stove/wiremock/OptionsKt {\n\tpublic static final fun wiremock-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun wiremock-QljV9L8 (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun wiremock-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/system/abstractions/SystemKey;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n\tpublic static final fun wiremock-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/wiremock/StubBehaviourBuilder {\n\tpublic fun <init> (Lcom/github/tomakehurst/wiremock/WireMockServer;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/util/Map;)V\n\tpublic synthetic fun <init> (Lcom/github/tomakehurst/wiremock/WireMockServer;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun initially (Lkotlin/jvm/functions/Function0;)V\n\tpublic final fun then (Lkotlin/jvm/functions/Function0;)V\n}\n\npublic final class com/trendyol/stove/wiremock/WireMockContext {\n\tpublic fun <init> (IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/trendyol/stove/serialization/StoveSerde;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;)V\n\tpublic synthetic fun <init> (IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/trendyol/stove/serialization/StoveSerde;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()Z\n\tpublic final fun component3 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun component4 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun component5 ()Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component7 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component8 ()Ljava/lang/String;\n\tpublic final fun copy (IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/trendyol/stove/serialization/StoveSerde;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;)Lcom/trendyol/stove/wiremock/WireMockContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/wiremock/WireMockContext;IZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/trendyol/stove/serialization/StoveSerde;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/wiremock/WireMockContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAfterRequest ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getAfterStubRemoved ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getConfigure ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getKeyName ()Ljava/lang/String;\n\tpublic final fun getPort ()I\n\tpublic final fun getRemoveStubAfterRequestMatched ()Z\n\tpublic final fun getSerde ()Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/wiremock/WireMockExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;I)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()I\n\tpublic final fun copy (Ljava/lang/String;I)Lcom/trendyol/stove/wiremock/WireMockExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/wiremock/WireMockExposedConfiguration;Ljava/lang/String;IILjava/lang/Object;)Lcom/trendyol/stove/wiremock/WireMockExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBaseUrl ()Ljava/lang/String;\n\tpublic final fun getHost ()Ljava/lang/String;\n\tpublic final fun getPort ()I\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/wiremock/WireMockRequestListener : com/github/tomakehurst/wiremock/extension/ServeEventListener {\n\tpublic fun <init> (Lcom/github/benmanes/caffeine/cache/Cache;Lkotlin/jvm/functions/Function2;)V\n\tpublic fun beforeResponseSent (Lcom/github/tomakehurst/wiremock/stubbing/ServeEvent;Lcom/github/tomakehurst/wiremock/extension/Parameters;)V\n\tpublic fun getName ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/wiremock/WireMockSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunAware, com/trendyol/stove/system/abstractions/ValidatedSystem {\n\tpublic static final field Companion Lcom/trendyol/stove/wiremock/WireMockSystem$Companion;\n\tpublic static final field STOVE_TEST_ID_KEY Ljava/lang/String;\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/wiremock/WireMockContext;)V\n\tpublic final fun behaviourFor (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun callsFor (Lcom/github/tomakehurst/wiremock/http/RequestMethod;Ljava/lang/String;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;)Ljava/util/List;\n\tpublic final fun callsFor (Lkotlin/jvm/functions/Function0;)Ljava/util/List;\n\tpublic static synthetic fun callsFor$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Lcom/github/tomakehurst/wiremock/http/RequestMethod;Ljava/lang/String;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Ljava/util/List;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun mockDelete (Ljava/lang/String;ILjava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockDelete$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;ILjava/util/Map;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockDeleteConfigure (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockDeleteConfigure$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockGet (Ljava/lang/String;ILarrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockGet$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;ILarrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockGetConfigure (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockGetConfigure$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockHead (Ljava/lang/String;ILjava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockHead$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;ILjava/util/Map;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockHeadConfigure (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockHeadConfigure$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockPatch (Ljava/lang/String;ILarrow/core/Option;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockPatch$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;ILarrow/core/Option;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockPatchConfigure (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockPatchConfigure$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockPatchContaining (Ljava/lang/String;Ljava/util/Map;ILarrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockPatchContaining$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;Ljava/util/Map;ILarrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockPost (Ljava/lang/String;ILarrow/core/Option;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockPost$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;ILarrow/core/Option;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockPostConfigure (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockPostConfigure$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockPostContaining (Ljava/lang/String;Ljava/util/Map;ILarrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockPostContaining$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;Ljava/util/Map;ILarrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockPut (Ljava/lang/String;ILarrow/core/Option;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockPut$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;ILarrow/core/Option;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockPutConfigure (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockPutConfigure$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun mockPutContaining (Ljava/lang/String;Ljava/util/Map;ILarrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun mockPutContaining$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Ljava/lang/String;Ljava/util/Map;ILarrow/core/Option;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldHaveBeenCalled (Lcom/github/tomakehurst/wiremock/client/CountMatchingStrategy;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldHaveBeenCalled (Lcom/github/tomakehurst/wiremock/http/RequestMethod;Ljava/lang/String;Lcom/github/tomakehurst/wiremock/client/CountMatchingStrategy;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun shouldHaveBeenCalled$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Lcom/github/tomakehurst/wiremock/client/CountMatchingStrategy;Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic static synthetic fun shouldHaveBeenCalled$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Lcom/github/tomakehurst/wiremock/http/RequestMethod;Ljava/lang/String;Lcom/github/tomakehurst/wiremock/client/CountMatchingStrategy;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic final fun shouldNotHaveBeenCalled (Lcom/github/tomakehurst/wiremock/http/RequestMethod;Ljava/lang/String;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldNotHaveBeenCalled (Lkotlin/jvm/functions/Function0;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun shouldNotHaveBeenCalled$default (Lcom/trendyol/stove/wiremock/WireMockSystem;Lcom/github/tomakehurst/wiremock/http/RequestMethod;Ljava/lang/String;Larrow/core/Option;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic fun validate (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/wiremock/WireMockSystem$Companion {\n\tpublic final fun server (Lcom/trendyol/stove/wiremock/WireMockSystem;)Lcom/github/tomakehurst/wiremock/WireMockServer;\n}\n\npublic final class com/trendyol/stove/wiremock/WireMockSystemOptions : com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (ILkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/trendyol/stove/serialization/StoveSerde;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (ILkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/trendyol/stove/serialization/StoveSerde;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component3 ()Z\n\tpublic final fun component4 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun component6 ()Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic final fun component7 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (ILkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/trendyol/stove/serialization/StoveSerde;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/wiremock/WireMockSystemOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/wiremock/WireMockSystemOptions;ILkotlin/jvm/functions/Function1;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lcom/trendyol/stove/serialization/StoveSerde;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/wiremock/WireMockSystemOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAfterRequest ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getAfterStubRemoved ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getConfigure ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun getPort ()I\n\tpublic final fun getRemoveStubAfterRequestMatched ()Z\n\tpublic final fun getSerde ()Lcom/trendyol/stove/serialization/StoveSerde;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/wiremock/WireMockVacuumCleaner : com/github/tomakehurst/wiremock/extension/ServeEventListener {\n\tpublic fun <init> (Lcom/github/benmanes/caffeine/cache/Cache;Lkotlin/jvm/functions/Function2;)V\n\tpublic fun beforeResponseSent (Lcom/github/tomakehurst/wiremock/stubbing/ServeEvent;Lcom/github/tomakehurst/wiremock/extension/Parameters;)V\n\tpublic fun getName ()Ljava/lang/String;\n\tpublic final fun wireMock (Lcom/github/tomakehurst/wiremock/WireMockServer;)V\n}\n\npublic abstract interface annotation class com/trendyol/stove/wiremock/WiremockDsl : java/lang/annotation/Annotation {\n}\n\n"
  },
  {
    "path": "lib/stove-wiremock/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(libs.wiremock.standalone)\n  api(libs.caffeine)\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/Extensions.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.benmanes.caffeine.cache.Cache\n\nfun <K : Any, V : Any> Cache<K, V>.containsKey(key: K): Boolean = this.getIfPresent(key) != null\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/Options.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport arrow.core.getOrElse\nimport com.github.tomakehurst.wiremock.common.ConsoleNotifier\nimport com.github.tomakehurst.wiremock.core.WireMockConfiguration\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\n\n/**\n * Configuration exposed by WireMock after it starts.\n *\n * This allows the application under test to receive the actual WireMock URL,\n * which is especially useful when using dynamic ports (port = 0).\n *\n * @property host The host where WireMock is running.\n * @property port The actual port WireMock is listening on.\n * @property baseUrl The complete base URL (http://host:port).\n */\ndata class WireMockExposedConfiguration(\n  val host: String,\n  val port: Int\n) : ExposedConfiguration {\n  val baseUrl: String get() = WireMockUrls.baseUrl(host, port)\n}\n\ndata class WireMockSystemOptions(\n  /**\n   * Port of wiremock server.\n   * Defaults to 0, which lets WireMock pick an available port automatically.\n   * This avoids port conflicts, especially in CI environments.\n   */\n  val port: Int = 0,\n  /**\n   * Configures wiremock server\n   */\n  val configure: WireMockConfiguration.() -> WireMockConfiguration = { this.notifier(ConsoleNotifier(true)) },\n  /**\n   * Removes the stub when request matches/completes\n   * Default value is false\n   */\n  val removeStubAfterRequestMatched: Boolean = false,\n  /**\n   * Called after stub removed\n   */\n  val afterStubRemoved: AfterStubRemoved = { _, _ -> },\n  /**\n   * Called after request handled\n   */\n  val afterRequest: AfterRequestHandler = { _, _ -> },\n  /**\n   * ObjectMapper for serialization/deserialization\n   */\n  val serde: StoveSerde<Any, ByteArray> = StoveSerde.jackson.anyByteArraySerde(),\n  /**\n   * Configures the exposed configuration for the application under test.\n   * Use this to inject WireMock's URL into your application's configuration.\n   *\n   * Example:\n   * ```kotlin\n   * WireMockSystemOptions(\n   *     port = 0, // dynamic port\n   *     configureExposedConfiguration = { cfg ->\n   *         listOf(\n   *             \"external-apis.inventory.url=${cfg.baseUrl}\",\n   *             \"external-apis.payment.url=${cfg.baseUrl}\"\n   *         )\n   *     }\n   * )\n   * ```\n   */\n  override val configureExposedConfiguration: (WireMockExposedConfiguration) -> List<String> = { _ -> listOf() }\n) : SystemOptions,\n  ConfiguresExposedConfiguration<WireMockExposedConfiguration>\n\ndata class WireMockContext(\n  val port: Int,\n  val removeStubAfterRequestMatched: Boolean,\n  val afterStubRemoved: AfterStubRemoved,\n  val afterRequest: AfterRequestHandler,\n  val serde: StoveSerde<Any, ByteArray>,\n  val configure: WireMockConfiguration.() -> WireMockConfiguration,\n  val configureExposedConfiguration: (WireMockExposedConfiguration) -> List<String>,\n  val keyName: String? = null\n)\n\ninternal fun Stove.withWireMock(options: WireMockSystemOptions = WireMockSystemOptions()): Stove =\n  WireMockSystem(\n    stove = this,\n    WireMockContext(\n      options.port,\n      options.removeStubAfterRequestMatched,\n      options.afterStubRemoved,\n      options.afterRequest,\n      options.serde,\n      options.configure,\n      options.configureExposedConfiguration\n    )\n  ).also { getOrRegister(it) }\n    .let { this }\n\ninternal fun Stove.withWireMock(key: SystemKey, options: WireMockSystemOptions = WireMockSystemOptions()): Stove =\n  WireMockSystem(\n    stove = this,\n    WireMockContext(\n      options.port,\n      options.removeStubAfterRequestMatched,\n      options.afterStubRemoved,\n      options.afterRequest,\n      options.serde,\n      options.configure,\n      options.configureExposedConfiguration,\n      keyName = keyDisplayName(key)\n    )\n  ).also { getOrRegister(key, it) }\n    .let { this }\n\ninternal fun Stove.wiremock(): WireMockSystem =\n  getOrNone<WireMockSystem>().getOrElse {\n    throw SystemNotRegisteredException(WireMockSystem::class)\n  }\n\ninternal fun Stove.wiremock(key: SystemKey): WireMockSystem =\n  getOrNone<WireMockSystem>(key).getOrElse {\n    throw SystemNotRegisteredException(\n      WireMockSystem::class,\n      WireMockSystemMessages.systemNotRegistered(keyDisplayName(key))\n    )\n  }\n\nfun WithDsl.wiremock(\n  configure: @StoveDsl () -> WireMockSystemOptions\n): Stove = this.stove.withWireMock(configure())\n\n/**\n * Registers a keyed WireMock system for testing multiple external service mocks.\n *\n * ```kotlin\n * Stove().with {\n *     wiremock(PaymentGateway) {\n *         WireMockSystemOptions(port = 0, configureExposedConfiguration = { cfg -> listOf(...) })\n *     }\n * }\n * ```\n *\n * @param key The [SystemKey] identifying this WireMock instance.\n * @param configure Configuration block returning [WireMockSystemOptions].\n * @return The test system for fluent chaining.\n */\nfun WithDsl.wiremock(\n  key: SystemKey,\n  configure: @StoveDsl () -> WireMockSystemOptions\n): Stove = this.stove.withWireMock(key, configure())\n\nsuspend fun ValidationDsl.wiremock(validation: @WiremockDsl suspend WireMockSystem.() -> Unit): Unit =\n  validation(this.stove.wiremock())\n\n/**\n * Executes WireMock assertions against a keyed WireMock instance within the validation DSL.\n *\n * ```kotlin\n * stove {\n *     wiremock(PaymentGateway) {\n *         mockGet(url = \"/status\", statusCode = 200)\n *     }\n * }\n * ```\n *\n * @param key The [SystemKey] identifying the WireMock instance.\n * @param validation The WireMock assertion block.\n */\nsuspend fun ValidationDsl.wiremock(\n  key: SystemKey,\n  validation: @WiremockDsl suspend WireMockSystem.() -> Unit\n): Unit = validation(this.stove.wiremock(key))\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/WireMockBodyMatching.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.tomakehurst.wiremock.client.MappingBuilder\nimport com.github.tomakehurst.wiremock.client.WireMock.*\nimport com.github.tomakehurst.wiremock.matching.*\nimport com.trendyol.stove.serialization.StoveSerde\n\ninternal fun MappingBuilder.configureBodyContaining(\n  requestContaining: Map<String, Any>,\n  serde: StoveSerde<Any, ByteArray>\n) {\n  requestContaining.forEach { (key, value) ->\n    val matcher = createValueMatcher(value, serde)\n    val jsonPath = WireMockJsonPath.field(key)\n    withRequestBody(matchingJsonPath(jsonPath, matcher))\n  }\n}\n\ninternal fun RequestPatternBuilder.configureBodyContaining(\n  requestContaining: Map<String, Any>,\n  serde: StoveSerde<Any, ByteArray>\n) {\n  requestContaining.forEach { (key, value) ->\n    val matcher = createValueMatcher(value, serde)\n    val jsonPath = WireMockJsonPath.field(key)\n    withRequestBody(matchingJsonPath(jsonPath, matcher))\n  }\n}\n\nprivate fun createValueMatcher(\n  value: Any,\n  serde: StoveSerde<Any, ByteArray>\n): StringValuePattern = when (value) {\n  is String -> equalTo(value)\n  is Number -> equalTo(value.toString())\n  is Boolean -> equalTo(value.toString())\n  is Map<*, *> -> equalToJson(serde.serialize(value).decodeToString(), true, true)\n  is Collection<*> -> equalToJson(serde.serialize(value).decodeToString(), true, true)\n  else -> equalToJson(serde.serialize(value).decodeToString(), true, true)\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/WireMockCallJournal.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.tomakehurst.wiremock.stubbing.ServeEvent\nimport com.github.tomakehurst.wiremock.stubbing.StubMapping\nimport com.github.tomakehurst.wiremock.verification.LoggedRequest\nimport com.trendyol.stove.tracing.TraceContext\nimport java.util.concurrent.*\n\ninternal class WireMockCallJournal {\n  private val stubsByTestId = ConcurrentHashMap<String, CopyOnWriteArrayList<StubMapping>>()\n  private val serveEventsByTestId = ConcurrentHashMap<String, CopyOnWriteArrayList<ServeEvent>>()\n\n  fun recordStub(stubMapping: StubMapping) {\n    val testId = stubMapping.stoveTestId() ?: return\n    stubsByTestId.computeIfAbsent(testId) { CopyOnWriteArrayList() }.add(stubMapping)\n  }\n\n  fun record(serveEvent: ServeEvent) {\n    val testId = serveEvent.stoveTestId() ?: return\n    serveEventsByTestId.computeIfAbsent(testId) { CopyOnWriteArrayList() }.add(serveEvent)\n  }\n\n  fun requests(testId: String): List<LoggedRequest> =\n    serveEvents(testId).map { it.request }\n\n  fun stubs(testId: String): List<StubMapping> =\n    stubsByTestId[testId]?.toList() ?: emptyList()\n\n  fun serveEvents(testId: String): List<ServeEvent> =\n    serveEventsByTestId[testId]?.toList() ?: emptyList()\n\n  fun clear(testId: String) {\n    stubsByTestId.remove(testId)\n    serveEventsByTestId.remove(testId)\n  }\n\n  fun clearAll() {\n    stubsByTestId.clear()\n    serveEventsByTestId.clear()\n  }\n\n  private fun ServeEvent.stoveTestId(): String? =\n    stubMapping?.metadata?.getString(WireMockSystem.STOVE_TEST_ID_KEY)\n      ?: request.getHeader(TraceContext.STOVE_TEST_ID_HEADER)\n\n  private fun StubMapping.stoveTestId(): String? =\n    metadata?.getString(WireMockSystem.STOVE_TEST_ID_KEY)\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/WireMockReportConstants.kt",
    "content": "package com.trendyol.stove.wiremock\n\ninternal object WireMockHeaders {\n  const val CONTENT_TYPE = \"Content-Type\"\n  const val APPLICATION_JSON = \"application/json\"\n  const val APPLICATION_JSON_UTF8 = \"application/json; charset=UTF-8\"\n}\n\ninternal object WireMockUrls {\n  fun baseUrl(host: String, port: Int): String = \"http://$host:$port\"\n}\n\ninternal object WireMockReportSystem {\n  fun name(keyName: String?): String =\n    \"WireMock\" + (keyName?.let { \" [$it]\" } ?: \"\")\n}\n\ninternal object WireMockSystemMessages {\n  fun systemNotRegistered(keyName: String): String =\n    \"No WireMockSystem registered with key '$keyName'\"\n}\n\ninternal object WireMockReportMetadataKeys {\n  const val STATUS_CODE = \"statusCode\"\n  const val RESPONSE_HEADERS = \"responseHeaders\"\n}\n\ninternal object WireMockReportActions {\n  fun registerStub(method: String, url: String): String = \"Register stub: $method $url\"\n\n  fun registerCustomStub(method: String, url: String): String = \"Register stub: $method $url (custom)\"\n\n  fun registerPartialStub(url: String): String = \"Register stub: $url (partial match)\"\n\n  fun registerBehaviourStub(url: String): String = \"Register behaviour stub: $url\"\n\n  const val VALIDATE_ALL_REQUESTS_SHOULD_MATCH = \"Validate: All requests should match registered stubs\"\n  const val VALIDATE_ALL_REQUESTS_MATCHED = \"Validate: All requests matched registered stubs\"\n  const val VERIFY_REQUEST_WAS_CALLED = \"Verify request was called\"\n  const val VERIFY_REQUEST_WAS_NOT_CALLED = \"Verify request was not called\"\n}\n\ninternal object WireMockValidationMessages {\n  const val REQUEST_CONTAINING_EMPTY = \"requestContaining must not be empty\"\n  const val VALIDATION_FAILED = \"Validation failed\"\n  const val EXPECTED_NO_UNMATCHED_REQUESTS = \"0 unmatched requests\"\n  const val STOP_FAILED_PREFIX = \"got an error while stopping wiremock:\"\n\n  fun unmatchedRequests(problems: String): String =\n    \"There are unmatched requests in the mock pipeline, please satisfy all the wanted requests.\\n$problems\"\n\n  fun unmatchedRequestCount(count: Int): String = \"$count unmatched request(s)\"\n\n  fun requestCount(count: Int): String = \"$count request(s)\"\n\n  fun unmatchedRequestDetails(\n    url: String,\n    bodyAsString: String,\n    queryParams: String\n  ): String =\n    \"\"\"\n        Url: $url\n        Body: $bodyAsString\n        QueryParams: $queryParams\n    \"\"\".trimIndent()\n}\n\ninternal object WireMockSnapshotStateKeys {\n  const val REGISTERED_STUBS = \"registeredStubs\"\n  const val ACTIVE_STUBS = \"activeStubs\"\n  const val RECEIVED_REQUESTS = \"receivedRequests\"\n  const val RECORDED_REQUESTS = \"recordedRequests\"\n  const val SERVED_REQUESTS = \"servedRequests\"\n  const val UNMATCHED_REQUESTS = \"unmatchedRequests\"\n}\n\ninternal object WireMockSnapshotFieldKeys {\n  const val ID = \"id\"\n  const val NAME = \"name\"\n  const val ACTIVE = \"active\"\n  const val PRIORITY = \"priority\"\n  const val SCENARIO_NAME = \"scenarioName\"\n  const val REQUIRED_SCENARIO_STATE = \"requiredScenarioState\"\n  const val NEW_SCENARIO_STATE = \"newScenarioState\"\n  const val REQUEST = \"request\"\n  const val RESPONSE = \"response\"\n  const val RESPONSE_DEFINITION = \"responseDefinition\"\n  const val METADATA = \"metadata\"\n  const val METHOD = \"method\"\n  const val URL = \"url\"\n  const val STATUS = \"status\"\n  const val STATUS_MESSAGE = \"statusMessage\"\n  const val MATCHED = \"matched\"\n  const val STUB_ID = \"stubId\"\n  const val STUB_NAME = \"stubName\"\n  const val TIMING = \"timing\"\n  const val ADDED_DELAY_MS = \"addedDelayMs\"\n  const val PROCESS_TIME_MS = \"processTimeMs\"\n  const val RESPONSE_SEND_TIME_MS = \"responseSendTimeMs\"\n  const val SERVE_TIME_MS = \"serveTimeMs\"\n  const val TOTAL_TIME_MS = \"totalTimeMs\"\n  const val ABSOLUTE_URL = \"absoluteUrl\"\n  const val CLIENT_IP = \"clientIp\"\n  const val LOGGED_DATE = \"loggedDate\"\n  const val HEADERS = \"headers\"\n  const val QUERY_PARAMS = \"queryParams\"\n  const val BODY = \"body\"\n  const val URL_MATCHER = \"urlMatcher\"\n  const val BODY_PATTERNS = \"bodyPatterns\"\n  const val CUSTOM_MATCHER = \"customMatcher\"\n  const val BODY_FILE_NAME = \"bodyFileName\"\n  const val FAULT = \"fault\"\n  const val FIXED_DELAY_MS = \"fixedDelayMs\"\n  const val TRANSFORMERS = \"transformers\"\n  const val MIME_TYPE = \"mimeType\"\n}\n\ninternal object WireMockSnapshotSummary {\n  fun registeredStubs(total: Int, active: Int): String = \"Registered stubs (this test): $total (active: $active)\"\n\n  fun receivedRequests(total: Int): String = \"Received requests (this test): $total\"\n\n  fun servedRequests(total: Int, matched: Int): String = \"Served requests (this test): $total (matched: $matched)\"\n\n  fun unmatchedRequests(total: Int): String = \"Unmatched requests: $total\"\n}\n\ninternal object WireMockSnapshotDisplayValues {\n  const val CUSTOM_MATCHER = \"<custom matcher>\"\n}\n\ninternal object WireMockBehaviourMessages {\n  const val INITIALLY_ONCE = \"You should call initially only once\"\n  const val INITIALLY_BEFORE_THEN = \"You should call initially before calling then\"\n}\n\ninternal object WireMockBehaviourNames {\n  fun scenarioName(url: String): String = \"Scenario for $url\"\n\n  fun state(counter: Int): String = \"State$counter\"\n}\n\ninternal object WireMockJsonPath {\n  fun field(key: String): String = \"\\$.$key\"\n}\n\ninternal object WireMockExtensionNames {\n  const val VACUUM_CLEANER = \"StoveVacuumCleaner\"\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/WireMockRequestListener.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.benmanes.caffeine.cache.Cache\nimport com.github.tomakehurst.wiremock.extension.*\nimport com.github.tomakehurst.wiremock.stubbing.*\nimport java.util.*\n\nclass WireMockRequestListener(\n  private val stubLog: Cache<UUID, StubMapping>,\n  private val afterRequest: AfterRequestHandler\n) : ServeEventListener {\n  private var recordRequest: (ServeEvent) -> Unit = {}\n\n  internal constructor(\n    stubLog: Cache<UUID, StubMapping>,\n    afterRequest: AfterRequestHandler,\n    recordRequest: (ServeEvent) -> Unit\n  ) : this(stubLog, afterRequest) {\n    this.recordRequest = recordRequest\n  }\n\n  override fun getName(): String = WireMockRequestListener::class.java.simpleName\n\n  override fun beforeResponseSent(\n    serveEvent: ServeEvent?,\n    parameters: Parameters?\n  ) {\n    val event = serveEvent!!\n    recordRequest(event)\n    afterRequest(event, stubLog)\n  }\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/WireMockSnapshot.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.tomakehurst.wiremock.http.HttpHeaders\nimport com.github.tomakehurst.wiremock.http.LoggedResponse\nimport com.github.tomakehurst.wiremock.http.ResponseDefinition\nimport com.github.tomakehurst.wiremock.matching.RequestPattern\nimport com.github.tomakehurst.wiremock.stubbing.ServeEvent\nimport com.github.tomakehurst.wiremock.stubbing.StubMapping\nimport com.github.tomakehurst.wiremock.verification.LoggedRequest\nimport com.trendyol.stove.reporting.SystemSnapshot\nimport com.trendyol.stove.wiremock.WireMockSnapshotDisplayValues as Display\nimport com.trendyol.stove.wiremock.WireMockSnapshotFieldKeys as Field\nimport com.trendyol.stove.wiremock.WireMockSnapshotStateKeys as State\n\ninternal class WireMockSnapshotBuilder(\n  private val reportSystemName: String,\n  private val callJournal: WireMockCallJournal,\n  activeStubs: List<StubMapping>\n) {\n  private val activeStubIds = activeStubs.map { it.id }.toSet()\n\n  fun build(testId: String): SystemSnapshot {\n    val registeredStubs = callJournal.stubs(testId)\n    val activeStubs = registeredStubs.filter { it.id in activeStubIds }\n    val serveEvents = callJournal.serveEvents(testId)\n    val receivedRequests = serveEvents.map { it.toReceivedRequestSnapshotMap() }\n    val unmatchedRequests = serveEvents\n      .filterNot { it.wasMatched }\n      .map { it.toReceivedRequestSnapshotMap() }\n\n    return SystemSnapshot(\n      system = reportSystemName,\n      state = mapOf(\n        State.REGISTERED_STUBS to registeredStubs.map { it.toSnapshotMap(active = it.id in activeStubIds) },\n        State.ACTIVE_STUBS to activeStubs.map { it.toSnapshotMap(active = true) },\n        State.RECEIVED_REQUESTS to receivedRequests,\n        State.RECORDED_REQUESTS to receivedRequests,\n        State.SERVED_REQUESTS to serveEvents.map { it.toServedSnapshotMap() },\n        State.UNMATCHED_REQUESTS to unmatchedRequests\n      ),\n      summary = buildString {\n        appendLine(WireMockSnapshotSummary.registeredStubs(registeredStubs.size, activeStubs.size))\n        appendLine(WireMockSnapshotSummary.receivedRequests(receivedRequests.size))\n        appendLine(WireMockSnapshotSummary.servedRequests(serveEvents.size, serveEvents.count { it.wasMatched }))\n        appendLine(WireMockSnapshotSummary.unmatchedRequests(unmatchedRequests.size))\n      }\n    )\n  }\n}\n\ninternal fun StubMapping.toSnapshotMap(active: Boolean): Map<String, Any> =\n  snapshotMap(\n    Field.ID to id.toString(),\n    Field.NAME to name,\n    Field.ACTIVE to active,\n    Field.PRIORITY to priority,\n    Field.SCENARIO_NAME to scenarioName,\n    Field.REQUIRED_SCENARIO_STATE to requiredScenarioState,\n    Field.NEW_SCENARIO_STATE to newScenarioState,\n    Field.REQUEST to request.toSnapshotMap(),\n    Field.RESPONSE to response.toSnapshotMap(),\n    Field.METADATA to metadata\n      ?.filterKeys { it != WireMockSystem.STOVE_TEST_ID_KEY }\n      ?.takeIf { it.isNotEmpty() },\n    Field.METHOD to request.method?.value(),\n    Field.URL to request.displayUrl(),\n    Field.STATUS to response.status\n  )\n\ninternal fun ServeEvent.toServedSnapshotMap(): Map<String, Any> =\n  snapshotMap(\n    Field.ID to id.toString(),\n    Field.MATCHED to wasMatched,\n    Field.STUB_ID to stubMapping?.id?.toString(),\n    Field.STUB_NAME to stubMapping?.name,\n    Field.REQUEST to request.toSnapshotMap(),\n    Field.RESPONSE to response.toSnapshotMap(),\n    Field.RESPONSE_DEFINITION to responseDefinition.toSnapshotMap(),\n    Field.TIMING to timing?.let {\n      snapshotMap(\n        Field.ADDED_DELAY_MS to it.addedDelay,\n        Field.PROCESS_TIME_MS to it.processTime,\n        Field.RESPONSE_SEND_TIME_MS to it.responseSendTime,\n        Field.SERVE_TIME_MS to it.serveTime,\n        Field.TOTAL_TIME_MS to it.totalTime\n      )\n    }\n  )\n\ninternal fun ServeEvent.toReceivedRequestSnapshotMap(): Map<String, Any> =\n  request.toSnapshotMap(\n    Field.MATCHED to wasMatched,\n    Field.STUB_ID to stubMapping?.id?.toString(),\n    Field.STUB_NAME to stubMapping?.name\n  )\n\ninternal fun LoggedRequest.toSnapshotMap(vararg additional: Pair<String, Any?>): Map<String, Any> =\n  snapshotMap(\n    Field.ID to id?.toString(),\n    Field.METHOD to method.value(),\n    Field.URL to url,\n    Field.ABSOLUTE_URL to absoluteUrl,\n    Field.CLIENT_IP to clientIp,\n    Field.LOGGED_DATE to loggedDateString,\n    Field.HEADERS to headers.toSnapshotMap().takeIf { it.isNotEmpty() },\n    Field.QUERY_PARAMS to queryParams.mapValues { it.value.values() }.takeIf { it.isNotEmpty() },\n    Field.BODY to bodyAsString\n  ) + snapshotMap(*additional)\n\nprivate fun RequestPattern.toSnapshotMap(): Map<String, Any> =\n  snapshotMap(\n    Field.METHOD to method?.value(),\n    Field.URL to displayUrl(),\n    Field.URL_MATCHER to urlMatcher?.toString(),\n    Field.HEADERS to headers?.mapValues { it.value.toString() }?.takeIf { it.isNotEmpty() },\n    Field.QUERY_PARAMS to queryParameters?.mapValues { it.value.toString() }?.takeIf { it.isNotEmpty() },\n    Field.BODY_PATTERNS to bodyPatterns?.map { it.toString() }?.takeIf { it.isNotEmpty() },\n    Field.CUSTOM_MATCHER to customMatcher?.toString()\n  )\n\nprivate fun ResponseDefinition.toSnapshotMap(): Map<String, Any> =\n  snapshotMap(\n    Field.STATUS to status,\n    Field.STATUS_MESSAGE to statusMessage,\n    Field.HEADERS to headers.toSnapshotMap().takeIf { it.isNotEmpty() },\n    Field.BODY to body,\n    Field.BODY_FILE_NAME to bodyFileName,\n    Field.FAULT to fault?.name,\n    Field.FIXED_DELAY_MS to fixedDelayMilliseconds,\n    Field.TRANSFORMERS to transformers?.takeIf { it.isNotEmpty() }\n  )\n\nprivate fun LoggedResponse?.toSnapshotMap(): Map<String, Any> =\n  this?.let { response ->\n    snapshotMap(\n      Field.STATUS to response.status,\n      Field.HEADERS to response.headers.toSnapshotMap().takeIf { it.isNotEmpty() },\n      Field.BODY to response.bodyAsString,\n      Field.MIME_TYPE to response.mimeType,\n      Field.FAULT to response.fault?.name\n    )\n  } ?: emptyMap()\n\nprivate fun HttpHeaders?.toSnapshotMap(): Map<String, List<String>> =\n  this?.all()\n    ?.associate { header -> header.key() to header.values() }\n    ?: emptyMap()\n\nprivate fun RequestPattern.displayUrl(): String =\n  url\n    ?: urlPath\n    ?: urlPattern\n    ?: urlPathPattern\n    ?: urlPathTemplate\n    ?: urlMatcher?.toString()\n    ?: Display.CUSTOM_MATCHER\n\nprivate fun snapshotMap(vararg entries: Pair<String, Any?>): Map<String, Any> =\n  entries.mapNotNull { (key, value) -> value?.let { key to it } }.toMap()\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/WireMockSystem.kt",
    "content": "@file:Suppress(\"unused\")\n\npackage com.trendyol.stove.wiremock\n\nimport arrow.core.*\nimport com.github.benmanes.caffeine.cache.*\nimport com.github.tomakehurst.wiremock.WireMockServer\nimport com.github.tomakehurst.wiremock.client.*\nimport com.github.tomakehurst.wiremock.client.WireMock.*\nimport com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig\nimport com.github.tomakehurst.wiremock.extension.Extension\nimport com.github.tomakehurst.wiremock.http.RequestMethod\nimport com.github.tomakehurst.wiremock.matching.*\nimport com.github.tomakehurst.wiremock.stubbing.*\nimport com.github.tomakehurst.wiremock.verification.LoggedRequest\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.tracing.TraceContext\nimport com.trendyol.stove.wiremock.WireMockHeaders.APPLICATION_JSON\nimport com.trendyol.stove.wiremock.WireMockHeaders.APPLICATION_JSON_UTF8\nimport com.trendyol.stove.wiremock.WireMockHeaders.CONTENT_TYPE\nimport com.trendyol.stove.wiremock.WireMockReportActions.VALIDATE_ALL_REQUESTS_MATCHED\nimport com.trendyol.stove.wiremock.WireMockReportActions.VALIDATE_ALL_REQUESTS_SHOULD_MATCH\nimport com.trendyol.stove.wiremock.WireMockReportMetadataKeys.RESPONSE_HEADERS\nimport com.trendyol.stove.wiremock.WireMockReportMetadataKeys.STATUS_CODE\nimport kotlinx.coroutines.runBlocking\nimport wiremock.org.slf4j.*\nimport java.util.*\nimport java.util.concurrent.ConcurrentLinkedQueue\n\n/**\n * Callback invoked after a stub is removed (when `removeStubAfterRequestMatched` is enabled).\n */\ntypealias AfterStubRemoved = (ServeEvent, Cache<UUID, StubMapping>) -> Unit\n\n/**\n * Callback invoked after a request is handled by WireMock.\n */\ntypealias AfterRequestHandler = (ServeEvent, Cache<UUID, StubMapping>) -> Unit\n\n/**\n * WireMock HTTP mocking system for testing external service integrations.\n *\n * WireMock allows you to mock external HTTP services that your application depends on,\n * enabling isolated testing without actual network calls.\n *\n * ## Mocking GET Requests\n *\n * ```kotlin\n * wiremock {\n *     // Simple GET mock\n *     mockGet(\n *         url = \"/api/users/123\",\n *         statusCode = 200,\n *         responseBody = User(id = \"123\", name = \"John\").some()\n *     )\n *\n *     // GET with custom headers\n *     mockGet(\n *         url = \"/api/users/123\",\n *         statusCode = 200,\n *         responseBody = user.some(),\n *         responseHeaders = mapOf(\n *             \"Content-Type\" to \"application/json\",\n *             \"X-Custom-Header\" to \"value\"\n *         )\n *     )\n * }\n * ```\n *\n * ## Mocking POST Requests\n *\n * ```kotlin\n * wiremock {\n *     // POST with request and response bodies\n *     mockPost(\n *         url = \"/api/payments\",\n *         statusCode = 200,\n *         requestBody = PaymentRequest(amount = 99.99).some(),\n *         responseBody = PaymentResponse(transactionId = \"txn-123\").some()\n *     )\n *\n *     // POST returning error\n *     mockPost(\n *         url = \"/api/payments\",\n *         statusCode = 400,\n *         responseBody = ErrorResponse(code = \"INVALID_AMOUNT\").some()\n *     )\n * }\n * ```\n *\n * ## Mocking PUT, DELETE, PATCH\n *\n * ```kotlin\n * wiremock {\n *     mockPut(\n *         url = \"/api/users/123\",\n *         statusCode = 200,\n *         requestBody = UpdateUserRequest(name = \"Jane\").some(),\n *         responseBody = User(id = \"123\", name = \"Jane\").some()\n *     )\n *\n *     mockDelete(\n *         url = \"/api/users/123\",\n *         statusCode = 204\n *     )\n *\n *     mockPatch(\n *         url = \"/api/users/123\",\n *         statusCode = 200,\n *         requestBody = mapOf(\"status\" to \"active\").some(),\n *         responseBody = User(id = \"123\", status = \"active\").some()\n *     )\n * }\n * ```\n *\n * ## Verifying Requests\n *\n * ```kotlin\n * wiremock {\n *     // Verify a request was made exactly once\n *     shouldHaveBeenCalled(RequestMethod.GET, \"/api/users/123\")\n *\n *     // Verify request count\n *     shouldHaveBeenCalled(exactly(2)) {\n *         postRequestedFor(urlEqualTo(\"/api/payments\"))\n *     }\n *\n *     // Verify with request body\n *     shouldHaveBeenCalled {\n *         postRequestedFor(urlEqualTo(\"/api/users\"))\n *             .withRequestBody(matchingJsonPath(\"$.name\", equalTo(\"John\")))\n *     }\n * }\n * ```\n *\n * ## Test Workflow Example\n *\n * ```kotlin\n * test(\"should process payment via external gateway\") {\n *     stove {\n *         // Mock external payment gateway\n *         wiremock {\n *             mockPost(\n *                 url = \"/gateway/charge\",\n *                 statusCode = 200,\n *                 responseBody = GatewayResponse(success = true, txnId = \"123\").some()\n *             )\n *         }\n *\n *         // Make request to our application (which calls the gateway)\n *         http {\n *             postAndExpectJson<OrderResponse>(\n *                 uri = \"/orders\",\n *                 body = CreateOrderRequest(amount = 99.99).some()\n *             ) { order ->\n *                 order.status shouldBe \"PAID\"\n *                 order.transactionId shouldBe \"123\"\n *             }\n *         }\n *\n *         // Verify the gateway was called\n *         wiremock {\n *             shouldHaveBeenCalled(RequestMethod.POST, \"/gateway/charge\")\n *         }\n *     }\n * }\n * ```\n *\n * ## Configuration\n *\n * ```kotlin\n * Stove()\n *     .with {\n *         wiremock {\n *             WireMockSystemOptions(\n *                 port = 9090,\n *                 removeStubAfterRequestMatched = true,  // Clean stubs after use\n *                 afterRequest = { event, _ ->\n *                     println(\"Request: ${event.request}\")\n *                 }\n *             )\n *         }\n *     }\n * ```\n *\n * @property stove The parent test system.\n * @see WireMockSystemOptions\n */\n@WiremockDsl\n@Suppress(\"LargeClass\", \"TooManyFunctions\")\nclass WireMockSystem(\n  override val stove: Stove,\n  private val ctx: WireMockContext\n) : PluggedSystem,\n  ValidatedSystem,\n  RunAware,\n  ExposesConfiguration,\n  Reports {\n  override val reportSystemName: String = WireMockReportSystem.name(ctx.keyName)\n  private val stubLog: Cache<UUID, StubMapping> = Caffeine.newBuilder().build()\n  private val callJournal = WireMockCallJournal()\n  private val serde: StoveSerde<Any, ByteArray> = ctx.serde\n  private val verification = WireMockVerification(this, callJournal, serde)\n  private val completedTestIds = ConcurrentLinkedQueue<String>()\n  private val reportListener = object : ReportEventListener {\n    override fun onTestStarted(ctx: StoveTestContext) {\n      clearCompletedTestJournals()\n      callJournal.clear(ctx.testId)\n    }\n\n    override fun onTestEnded(testId: String) {\n      completedTestIds.add(testId)\n    }\n  }\n  private var reportListenerRegistered = false\n  private lateinit var exposedConfiguration: WireMockExposedConfiguration\n\n  override fun configuration(): List<String> = ctx.configureExposedConfiguration(exposedConfiguration)\n\n  override fun snapshot(): SystemSnapshot =\n    WireMockSnapshotBuilder(reportSystemName, callJournal, wireMock.stubMappings)\n      .build(reporter.currentTestId())\n\n  private var wireMock: WireMockServer\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  init {\n    val cfg = wireMockConfig()\n      .port(ctx.port)\n      .extensions(WireMockRequestListener(stubLog, ctx.afterRequest, callJournal::record))\n    val stoveExtensions = mutableListOf<Extension>()\n    if (ctx.removeStubAfterRequestMatched) {\n      stoveExtensions.add(WireMockVacuumCleaner(stubLog, ctx.afterStubRemoved))\n    }\n    stoveExtensions.map { cfg.extensions(it) }\n    wireMock = WireMockServer(cfg.let(ctx.configure))\n    stoveExtensions.filterIsInstance<WireMockVacuumCleaner>().forEach { it.wireMock(wireMock) }\n  }\n\n  /**\n   * Starts the WireMock server.\n   */\n  override suspend fun run() {\n    if (!reportListenerRegistered) {\n      stove.addReportListener(reportListener)\n      reportListenerRegistered = true\n    }\n    wireMock.start()\n    exposedConfiguration = WireMockExposedConfiguration(\n      host = LOCALHOST,\n      port = wireMock.port()\n    )\n  }\n\n  /**\n   * Stops the WireMock server.\n   */\n  override suspend fun stop(): Unit = wireMock.shutdownServer()\n\n  /**\n   * Mocks a GET request with exact URL matching.\n   *\n   * @param url The exact URL to match.\n   * @param statusCode The HTTP status code to return.\n   * @param responseBody Optional response body to return.\n   * @param metadata Optional metadata to attach to the stub.\n   * @param responseHeaders Optional response headers.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockGet(\n    url: String,\n    statusCode: Int,\n    responseBody: Option<Any> = None,\n    metadata: Map<String, String> = mapOf(),\n    responseHeaders: Map<String, String> = mapOf()\n  ): WireMockSystem =\n    mockRequest(\n      methodName = RequestMethod.GET.value(),\n      url = url,\n      method = ::get,\n      statusCode = statusCode,\n      responseBody = responseBody,\n      metadata = metadata,\n      responseHeaders = responseHeaders,\n      reportMetadata = mapOf(STATUS_CODE to statusCode, RESPONSE_HEADERS to responseHeaders)\n    )\n\n  /**\n   * Mocks a POST request with exact URL and request body matching.\n   *\n   * The request body must match exactly (ignoring field order but not extra fields).\n   * For partial body matching, use [mockPostContaining] instead.\n   *\n   * @param url The exact URL to match.\n   * @param statusCode The HTTP status code to return.\n   * @param requestBody Optional request body to match exactly.\n   * @param responseBody Optional response body to return.\n   * @param metadata Optional metadata to attach to the stub.\n   * @param responseHeaders Optional response headers.\n   * @return This [WireMockSystem] for chaining.\n   * @see mockPostContaining\n   */\n  suspend fun mockPost(\n    url: String,\n    statusCode: Int,\n    requestBody: Option<Any> = None,\n    responseBody: Option<Any> = None,\n    metadata: Map<String, Any> = mapOf(),\n    responseHeaders: Map<String, String> = mapOf()\n  ): WireMockSystem =\n    mockRequest(\n      methodName = RequestMethod.POST.value(),\n      url = url,\n      method = ::post,\n      statusCode = statusCode,\n      requestBody = requestBody,\n      responseBody = responseBody,\n      metadata = metadata,\n      responseHeaders = responseHeaders\n    )\n\n  /**\n   * Mocks a PUT request with exact URL and request body matching.\n   *\n   * The request body must match exactly (ignoring field order but not extra fields).\n   * For partial body matching, use [mockPutContaining] instead.\n   *\n   * @param url The exact URL to match.\n   * @param statusCode The HTTP status code to return.\n   * @param requestBody Optional request body to match exactly.\n   * @param responseBody Optional response body to return.\n   * @param metadata Optional metadata to attach to the stub.\n   * @param responseHeaders Optional response headers.\n   * @return This [WireMockSystem] for chaining.\n   * @see mockPutContaining\n   */\n  suspend fun mockPut(\n    url: String,\n    statusCode: Int,\n    requestBody: Option<Any> = None,\n    responseBody: Option<Any> = None,\n    metadata: Map<String, Any> = mapOf(),\n    responseHeaders: Map<String, String> = mapOf()\n  ): WireMockSystem =\n    mockRequest(\n      methodName = RequestMethod.PUT.value(),\n      url = url,\n      method = ::put,\n      statusCode = statusCode,\n      requestBody = requestBody,\n      responseBody = responseBody,\n      metadata = metadata,\n      responseHeaders = responseHeaders\n    )\n\n  /**\n   * Mocks a PATCH request with exact URL and request body matching.\n   *\n   * The request body must match exactly (ignoring field order but not extra fields).\n   * For partial body matching, use [mockPatchContaining] instead.\n   *\n   * @param url The exact URL to match.\n   * @param statusCode The HTTP status code to return.\n   * @param requestBody Optional request body to match exactly.\n   * @param responseBody Optional response body to return.\n   * @param metadata Optional metadata to attach to the stub.\n   * @param responseHeaders Optional response headers.\n   * @return This [WireMockSystem] for chaining.\n   * @see mockPatchContaining\n   */\n  suspend fun mockPatch(\n    url: String,\n    statusCode: Int,\n    requestBody: Option<Any> = None,\n    responseBody: Option<Any> = None,\n    metadata: Map<String, Any> = mapOf(),\n    responseHeaders: Map<String, String> = mapOf()\n  ): WireMockSystem =\n    mockRequest(\n      methodName = RequestMethod.PATCH.value(),\n      url = url,\n      method = ::patch,\n      statusCode = statusCode,\n      requestBody = requestBody,\n      responseBody = responseBody,\n      metadata = metadata,\n      responseHeaders = responseHeaders\n    )\n\n  /**\n   * Mocks a DELETE request with exact URL matching.\n   *\n   * @param url The exact URL to match.\n   * @param statusCode The HTTP status code to return.\n   * @param metadata Optional metadata to attach to the stub.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockDelete(\n    url: String,\n    statusCode: Int,\n    metadata: Map<String, Any> = mapOf()\n  ): WireMockSystem =\n    mockRequest(\n      methodName = RequestMethod.DELETE.value(),\n      url = url,\n      method = ::delete,\n      statusCode = statusCode,\n      metadata = metadata\n    )\n\n  /**\n   * Mocks a HEAD request with exact URL matching.\n   *\n   * @param url The exact URL to match.\n   * @param statusCode The HTTP status code to return.\n   * @param metadata Optional metadata to attach to the stub.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockHead(\n    url: String,\n    statusCode: Int,\n    metadata: Map<String, Any> = mapOf()\n  ): WireMockSystem =\n    mockRequest(\n      methodName = RequestMethod.HEAD.value(),\n      url = url,\n      method = ::head,\n      statusCode = statusCode,\n      metadata = metadata\n    )\n\n  /**\n   * Mocks a PUT request with full configuration control.\n   *\n   * This method provides access to the underlying WireMock [MappingBuilder] for advanced\n   * configuration scenarios like custom matchers, headers, or response transformers.\n   *\n   * @param url The URL or URL pattern to match.\n   * @param urlPatternFn Function to create URL pattern. Defaults to exact URL matching.\n   *                     Use `{ urlPathMatching(it) }` for regex patterns.\n   * @param configure Lambda to configure the request and response using WireMock's API.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockPutConfigure(\n    url: String,\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) },\n    configure: (MappingBuilder, StoveSerde<Any, ByteArray>) -> MappingBuilder\n  ): WireMockSystem = mockRequestConfigure(RequestMethod.PUT.value(), url, urlPatternFn, ::put, configure)\n\n  /**\n   * Mocks a PATCH request with full configuration control.\n   *\n   * This method provides access to the underlying WireMock [MappingBuilder] for advanced\n   * configuration scenarios like custom matchers, headers, or response transformers.\n   *\n   * @param url The URL or URL pattern to match.\n   * @param urlPatternFn Function to create URL pattern. Defaults to exact URL matching.\n   *                     Use `{ urlPathMatching(it) }` for regex patterns.\n   * @param configure Lambda to configure the request and response using WireMock's API.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockPatchConfigure(\n    url: String,\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) },\n    configure: (MappingBuilder, StoveSerde<Any, ByteArray>) -> MappingBuilder\n  ): WireMockSystem = mockRequestConfigure(RequestMethod.PATCH.value(), url, urlPatternFn, ::patch, configure)\n\n  /**\n   * Mocks a GET request with full configuration control.\n   *\n   * This method provides access to the underlying WireMock [MappingBuilder] for advanced\n   * configuration scenarios like custom matchers, headers, or response transformers.\n   *\n   * @param url The URL or URL pattern to match.\n   * @param urlPatternFn Function to create URL pattern. Defaults to exact URL matching.\n   *                     Use `{ urlPathMatching(it) }` for regex patterns.\n   * @param configure Lambda to configure the request and response using WireMock's API.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockGetConfigure(\n    url: String,\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) },\n    configure: (MappingBuilder, StoveSerde<Any, ByteArray>) -> MappingBuilder\n  ): WireMockSystem = mockRequestConfigure(RequestMethod.GET.value(), url, urlPatternFn, ::get, configure)\n\n  /**\n   * Mocks a HEAD request with full configuration control.\n   *\n   * This method provides access to the underlying WireMock [MappingBuilder] for advanced\n   * configuration scenarios like custom matchers, headers, or response transformers.\n   *\n   * @param url The URL or URL pattern to match.\n   * @param urlPatternFn Function to create URL pattern. Defaults to exact URL matching.\n   *                     Use `{ urlPathMatching(it) }` for regex patterns.\n   * @param configure Lambda to configure the request and response using WireMock's API.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockHeadConfigure(\n    url: String,\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) },\n    configure: (MappingBuilder, StoveSerde<Any, ByteArray>) -> MappingBuilder\n  ): WireMockSystem = mockRequestConfigure(RequestMethod.HEAD.value(), url, urlPatternFn, ::head, configure)\n\n  /**\n   * Mocks a DELETE request with full configuration control.\n   *\n   * This method provides access to the underlying WireMock [MappingBuilder] for advanced\n   * configuration scenarios like custom matchers, headers, or response transformers.\n   *\n   * @param url The URL or URL pattern to match.\n   * @param urlPatternFn Function to create URL pattern. Defaults to exact URL matching.\n   *                     Use `{ urlPathMatching(it) }` for regex patterns.\n   * @param configure Lambda to configure the request and response using WireMock's API.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockDeleteConfigure(\n    url: String,\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) },\n    configure: (MappingBuilder, StoveSerde<Any, ByteArray>) -> MappingBuilder\n  ): WireMockSystem = mockRequestConfigure(RequestMethod.DELETE.value(), url, urlPatternFn, ::delete, configure)\n\n  /**\n   * Mocks a POST request with full configuration control.\n   *\n   * This method provides access to the underlying WireMock [MappingBuilder] for advanced\n   * configuration scenarios like custom matchers, headers, or response transformers.\n   *\n   * @param url The URL or URL pattern to match.\n   * @param urlPatternFn Function to create URL pattern. Defaults to exact URL matching.\n   *                     Use `{ urlPathMatching(it) }` for regex patterns.\n   * @param configure Lambda to configure the request and response using WireMock's API.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockPostConfigure(\n    url: String,\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) },\n    configure: (MappingBuilder, StoveSerde<Any, ByteArray>) -> MappingBuilder\n  ): WireMockSystem = mockRequestConfigure(RequestMethod.POST.value(), url, urlPatternFn, ::post, configure)\n\n  /**\n   * Configures stateful stub behavior for scenario-based testing.\n   *\n   * Use this method when you need different responses for the same URL based on\n   * the order of requests (e.g., first call returns success, second returns error).\n   *\n   * ## Example\n   *\n   * ```kotlin\n   * wiremock {\n   *     behaviourFor(\"/api/resource\", ::post) { serde ->\n   *         initially {\n   *             aResponse().withStatus(200).withBody(\"first response\")\n   *         }\n   *         then {\n   *             aResponse().withStatus(500).withBody(\"server error\")\n   *         }\n   *     }\n   * }\n   * ```\n   *\n   * @param url The URL to match.\n   * @param method Function to create the HTTP method matcher (e.g., `::post`, `::get`).\n   * @param block Lambda to define the sequence of responses.\n   */\n  suspend fun behaviourFor(\n    url: String,\n    method: (String) -> MappingBuilder,\n    block: StubBehaviourBuilder.(StoveSerde<Any, ByteArray>) -> Unit\n  ) {\n    report(action = WireMockReportActions.registerBehaviourStub(url)) {\n      stubBehaviour(\n        wireMockServer = wireMock,\n        serde = serde,\n        url = url,\n        method = method,\n        metadata = enrichMetadataWithTestId(emptyMap()),\n        recordStub = ::recordStub,\n        block = block\n      )\n    }\n  }\n\n  /**\n   * Mocks a POST request with partial body matching.\n   *\n   * Unlike [mockPost], this method allows matching requests where the body\n   * **contains** the specified fields, without requiring an exact match of\n   * the entire request body. This is useful when you only care about specific\n   * fields in the request for test matching purposes.\n   *\n   * ## Features\n   * - **AND logic**: When multiple fields are specified, ALL must match\n   * - **Dot notation**: Use `\"order.customer.id\"` to match deep nested keys\n   * - **Partial object matching**: Nested objects match if they contain at least the specified fields\n   * - **Multiple fields**: Specify multiple keys to match several fields in one mock\n   *\n   * ## Examples\n   *\n   * ```kotlin\n   * // Match a top-level field\n   * wiremock {\n   *     mockPostContaining(\n   *         url = \"/orders\",\n   *         requestContaining = mapOf(\"productId\" to 123),\n   *         responseBody = OrderResponse(id = \"order-1\").some()\n   *     )\n   * }\n   *\n   * // Match a deeply nested field using dot notation\n   * wiremock {\n   *     mockPostContaining(\n   *         url = \"/orders\",\n   *         requestContaining = mapOf(\"order.customer.id\" to \"cust-123\"),\n   *         responseBody = OrderResponse(id = \"order-1\").some()\n   *     )\n   * }\n   *\n   * // Match multiple fields at different depths\n   * wiremock {\n   *     mockPostContaining(\n   *         url = \"/orders\",\n   *         requestContaining = mapOf(\n   *             \"order.customer.id\" to \"cust-123\",\n   *             \"order.payment.method\" to \"credit_card\"\n   *         ),\n   *         responseBody = OrderResponse(id = \"order-1\").some()\n   *     )\n   * }\n   * ```\n   *\n   * @param url The URL to match.\n   * @param requestContaining Map of field paths to values. Supports dot notation for nested paths (e.g., \"order.customer.id\").\n   * @param statusCode The HTTP status code to return. Defaults to 200.\n   * @param responseBody Optional response body to return.\n   * @param metadata Optional metadata to attach to the stub.\n   * @param responseHeaders Optional response headers.\n   * @param urlPatternFn Function to create URL pattern. Defaults to exact URL matching.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockPostContaining(\n    url: String,\n    requestContaining: Map<String, Any>,\n    statusCode: Int = 200,\n    responseBody: Option<Any> = None,\n    metadata: Map<String, Any> = mapOf(),\n    responseHeaders: Map<String, String> = mapOf(),\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) }\n  ): WireMockSystem = mockRequestContaining(\n    url = url,\n    method = ::post,\n    requestContaining = requestContaining,\n    statusCode = statusCode,\n    responseBody = responseBody,\n    metadata = metadata,\n    responseHeaders = responseHeaders,\n    urlPatternFn = urlPatternFn\n  )\n\n  /**\n   * Mocks a PUT request with partial body matching.\n   *\n   * Unlike [mockPut], this method allows matching requests where the body\n   * **contains** the specified fields, without requiring an exact match of\n   * the entire request body. This is useful when you only care about specific\n   * fields in the request for test matching purposes.\n   *\n   * ## Features\n   * - **AND logic**: When multiple fields are specified, ALL must match\n   * - **Dot notation**: Use `\"user.profile.settings.theme\"` to match deep nested keys\n   * - **Partial object matching**: Nested objects match if they contain at least the specified fields\n   * - **Multiple fields**: Specify multiple keys to match several fields in one mock\n   *\n   * ## Examples\n   *\n   * ```kotlin\n   * // Match a top-level field\n   * wiremock {\n   *     mockPutContaining(\n   *         url = \"/users/123\",\n   *         requestContaining = mapOf(\"userId\" to \"user-123\"),\n   *         responseBody = User(id = \"123\", name = \"Updated\").some()\n   *     )\n   * }\n   *\n   * // Match a deeply nested field using dot notation\n   * wiremock {\n   *     mockPutContaining(\n   *         url = \"/users/123\",\n   *         requestContaining = mapOf(\"user.profile.settings.theme\" to \"dark\"),\n   *         responseBody = User(id = \"123\").some()\n   *     )\n   * }\n   * ```\n   *\n   * @param url The URL to match.\n   * @param requestContaining Map of field paths to values. Supports dot notation for nested paths (e.g., \"user.profile.id\").\n   * @param statusCode The HTTP status code to return. Defaults to 200.\n   * @param responseBody Optional response body to return.\n   * @param metadata Optional metadata to attach to the stub.\n   * @param responseHeaders Optional response headers.\n   * @param urlPatternFn Function to create URL pattern. Defaults to exact URL matching.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockPutContaining(\n    url: String,\n    requestContaining: Map<String, Any>,\n    statusCode: Int = 200,\n    responseBody: Option<Any> = None,\n    metadata: Map<String, Any> = mapOf(),\n    responseHeaders: Map<String, String> = mapOf(),\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) }\n  ): WireMockSystem = mockRequestContaining(\n    url = url,\n    method = ::put,\n    requestContaining = requestContaining,\n    statusCode = statusCode,\n    responseBody = responseBody,\n    metadata = metadata,\n    responseHeaders = responseHeaders,\n    urlPatternFn = urlPatternFn\n  )\n\n  /**\n   * Mocks a PATCH request with partial body matching.\n   *\n   * Unlike [mockPatch], this method allows matching requests where the body\n   * **contains** the specified fields, without requiring an exact match of\n   * the entire request body. This is useful when you only care about specific\n   * fields in the request for test matching purposes.\n   *\n   * ## Features\n   * - **AND logic**: When multiple fields are specified, ALL must match\n   * - **Dot notation**: Use `\"document.section.text\"` to match deep nested keys\n   * - **Partial object matching**: Nested objects match if they contain at least the specified fields\n   * - **Multiple fields**: Specify multiple keys to match several fields in one mock\n   *\n   * ## Examples\n   *\n   * ```kotlin\n   * // Match a top-level field\n   * wiremock {\n   *     mockPatchContaining(\n   *         url = \"/users/123\",\n   *         requestContaining = mapOf(\"status\" to \"active\"),\n   *         responseBody = User(id = \"123\", status = \"active\").some()\n   *     )\n   * }\n   *\n   * // Match a deeply nested field using dot notation\n   * wiremock {\n   *     mockPatchContaining(\n   *         url = \"/documents/123\",\n   *         requestContaining = mapOf(\"document.section.paragraph.text\" to \"updated\"),\n   *         responseBody = Document(id = \"123\").some()\n   *     )\n   * }\n   * ```\n   *\n   * @param url The URL to match.\n   * @param requestContaining Map of field paths to values. Supports dot notation for nested paths (e.g., \"config.settings.enabled\").\n   * @param statusCode The HTTP status code to return. Defaults to 200.\n   * @param responseBody Optional response body to return.\n   * @param metadata Optional metadata to attach to the stub.\n   * @param responseHeaders Optional response headers.\n   * @param urlPatternFn Function to create URL pattern. Defaults to exact URL matching.\n   * @return This [WireMockSystem] for chaining.\n   */\n  suspend fun mockPatchContaining(\n    url: String,\n    requestContaining: Map<String, Any>,\n    statusCode: Int = 200,\n    responseBody: Option<Any> = None,\n    metadata: Map<String, Any> = mapOf(),\n    responseHeaders: Map<String, String> = mapOf(),\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) }\n  ): WireMockSystem = mockRequestContaining(\n    url = url,\n    method = ::patch,\n    requestContaining = requestContaining,\n    statusCode = statusCode,\n    responseBody = responseBody,\n    metadata = metadata,\n    responseHeaders = responseHeaders,\n    urlPatternFn = urlPatternFn\n  )\n\n  /**\n   * Verifies that a request matching the provided criteria has been called.\n   *\n   * By default, the request must have been called exactly once. Use WireMock's count helpers\n   * such as [moreThanOrExactly], [lessThan], or [exactly] to customize the expected count.\n   */\n  suspend fun shouldHaveBeenCalled(\n    method: RequestMethod,\n    url: String,\n    count: CountMatchingStrategy = exactly(1),\n    requestBody: Option<Any> = None,\n    requestContaining: Map<String, Any> = emptyMap(),\n    headers: Map<String, String> = emptyMap(),\n    queryParams: Map<String, String> = emptyMap(),\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) }\n  ): WireMockSystem =\n    verification.shouldHaveBeenCalled(\n      method = method,\n      url = url,\n      count = count,\n      requestBody = requestBody,\n      requestContaining = requestContaining,\n      headers = headers,\n      queryParams = queryParams,\n      urlPatternFn = urlPatternFn\n    )\n\n  /**\n   * Verifies that a request matching the provided WireMock pattern has been called.\n   *\n   * By default, the request must have been called exactly once.\n   */\n  suspend fun shouldHaveBeenCalled(\n    count: CountMatchingStrategy = exactly(1),\n    request: @WiremockDsl () -> RequestPatternBuilder\n  ): WireMockSystem =\n    verification.shouldHaveBeenCalled(count, request)\n\n  /**\n   * Verifies that no request matching the provided criteria has been called.\n   */\n  suspend fun shouldNotHaveBeenCalled(\n    method: RequestMethod,\n    url: String,\n    requestBody: Option<Any> = None,\n    requestContaining: Map<String, Any> = emptyMap(),\n    headers: Map<String, String> = emptyMap(),\n    queryParams: Map<String, String> = emptyMap(),\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) }\n  ): WireMockSystem =\n    verification.shouldNotHaveBeenCalled(\n      method = method,\n      url = url,\n      requestBody = requestBody,\n      requestContaining = requestContaining,\n      headers = headers,\n      queryParams = queryParams,\n      urlPatternFn = urlPatternFn\n    )\n\n  /**\n   * Verifies that no request matching the provided WireMock pattern has been called.\n   */\n  suspend fun shouldNotHaveBeenCalled(\n    request: @WiremockDsl () -> RequestPatternBuilder\n  ): WireMockSystem =\n    verification.shouldNotHaveBeenCalled(request)\n\n  /**\n   * Returns requests from the current test matching the provided criteria.\n   */\n  fun callsFor(\n    method: RequestMethod,\n    url: String,\n    requestBody: Option<Any> = None,\n    requestContaining: Map<String, Any> = emptyMap(),\n    headers: Map<String, String> = emptyMap(),\n    queryParams: Map<String, String> = emptyMap(),\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) }\n  ): List<LoggedRequest> =\n    verification.callsFor(\n      method = method,\n      url = url,\n      requestBody = requestBody,\n      requestContaining = requestContaining,\n      headers = headers,\n      queryParams = queryParams,\n      urlPatternFn = urlPatternFn\n    )\n\n  /**\n   * Returns requests from the current test matching the provided WireMock pattern.\n   */\n  fun callsFor(\n    request: @WiremockDsl () -> RequestPatternBuilder\n  ): List<LoggedRequest> = verification.callsFor(request)\n\n  private suspend fun mockRequest(\n    methodName: String,\n    url: String,\n    method: (UrlPattern) -> MappingBuilder,\n    statusCode: Int,\n    requestBody: Option<Any> = None,\n    responseBody: Option<Any> = None,\n    metadata: Map<String, Any> = emptyMap(),\n    responseHeaders: Map<String, String> = emptyMap(),\n    reportMetadata: Map<String, Any> = mapOf(STATUS_CODE to statusCode)\n  ): WireMockSystem =\n    mockRequest(\n      action = WireMockReportActions.registerStub(methodName, url),\n      request = method(urlEqualTo(url)),\n      statusCode = statusCode,\n      requestBody = requestBody,\n      responseBody = responseBody,\n      metadata = metadata,\n      responseHeaders = responseHeaders,\n      reportMetadata = reportMetadata\n    )\n\n  private suspend fun mockRequest(\n    action: String,\n    request: MappingBuilder,\n    statusCode: Int,\n    requestBody: Option<Any> = None,\n    responseBody: Option<Any> = None,\n    metadata: Map<String, Any> = emptyMap(),\n    responseHeaders: Map<String, String> = emptyMap(),\n    reportMetadata: Map<String, Any> = mapOf(STATUS_CODE to statusCode)\n  ): WireMockSystem =\n    registerStub(\n      action = action,\n      input = requestBody,\n      metadata = reportMetadata\n    ) {\n      configureBodyAndMetadata(request, metadata, requestBody)\n      request.willReturn(configureResponse(statusCode, responseBody, responseHeaders))\n    }\n\n  private suspend fun mockRequestConfigure(\n    methodName: String,\n    url: String,\n    urlPatternFn: (url: String) -> UrlPattern,\n    method: (UrlPattern) -> MappingBuilder,\n    configure: (MappingBuilder, StoveSerde<Any, ByteArray>) -> MappingBuilder\n  ): WireMockSystem =\n    registerStub(action = WireMockReportActions.registerCustomStub(methodName, url)) {\n      val configuredRequest = configure(method(urlPatternFn(url)), serde)\n      configuredRequest.withMetadata(enrichMetadataWithTestId(emptyMap()))\n      configuredRequest\n    }\n\n  private suspend fun mockRequestContaining(\n    url: String,\n    method: (UrlPattern) -> MappingBuilder,\n    requestContaining: Map<String, Any>,\n    statusCode: Int,\n    responseBody: Option<Any>,\n    metadata: Map<String, Any>,\n    responseHeaders: Map<String, String>,\n    urlPatternFn: (url: String) -> UrlPattern\n  ): WireMockSystem {\n    require(requestContaining.isNotEmpty()) { WireMockValidationMessages.REQUEST_CONTAINING_EMPTY }\n\n    return registerStub(\n      action = WireMockReportActions.registerPartialStub(url),\n      input = requestContaining.some(),\n      metadata = mapOf(STATUS_CODE to statusCode)\n    ) {\n      val mockRequest = method(urlPatternFn(url))\n      mockRequest.withMetadata(enrichMetadataWithTestId(metadata))\n      mockRequest.withHeader(CONTENT_TYPE, ContainsPattern(APPLICATION_JSON))\n      mockRequest.configureBodyContaining(requestContaining, serde)\n      val mockResponse = configureResponse(statusCode, responseBody, responseHeaders)\n      mockRequest.willReturn(mockResponse)\n    }\n  }\n\n  /**\n   * Validates that all registered stubs were matched by incoming requests.\n   *\n   * If any requests were received that didn't match a stub, this method throws\n   * an [AssertionError] with details about the unmatched requests.\n   *\n   * This is typically called at the end of a test to ensure all expected\n   * external service calls were properly mocked.\n   *\n   * @throws AssertionError if there are unmatched requests.\n   */\n  override suspend fun validate() {\n    val currentTestId = reporter.currentTestId()\n\n    // Filter unmatched requests to only include those from the current test\n    // by checking the X-Stove-Test-Id header\n    val unmatched = wireMock.findAllUnmatchedRequests().filter { req ->\n      req.getHeader(TraceContext.STOVE_TEST_ID_HEADER) == currentTestId\n    }\n    val passed = unmatched.isEmpty()\n\n    if (!passed) {\n      val problems = unmatched.joinToString(\"\\n\") {\n        WireMockValidationMessages.unmatchedRequestDetails(\n          url = \"${it.method.value()} ${it.url}\",\n          bodyAsString = it.bodyAsString,\n          queryParams = serde.serialize(it.queryParams).decodeToString()\n        )\n      }\n      val error = AssertionError(\n        WireMockValidationMessages.unmatchedRequests(problems)\n      )\n\n      reporter.record(\n        ReportEntry.failure(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = VALIDATE_ALL_REQUESTS_SHOULD_MATCH,\n          error = error.message ?: WireMockValidationMessages.VALIDATION_FAILED,\n          expected = WireMockValidationMessages.EXPECTED_NO_UNMATCHED_REQUESTS.some(),\n          actual = WireMockValidationMessages.unmatchedRequestCount(unmatched.size).some()\n        )\n      )\n\n      throw error\n    } else {\n      reporter.record(\n        ReportEntry.success(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = VALIDATE_ALL_REQUESTS_MATCHED\n        )\n      )\n    }\n  }\n\n  /**\n   * Closes the WireMock system and stops the server.\n   */\n  override fun close(): Unit = runBlocking {\n    Try {\n      if (reportListenerRegistered) {\n        stove.removeReportListener(reportListener)\n        reportListenerRegistered = false\n      }\n      stop()\n      callJournal.clearAll()\n    }.recover { logger.warn(\"${WireMockValidationMessages.STOP_FAILED_PREFIX} ${it.message}\") }\n  }\n\n  private fun clearCompletedTestJournals() {\n    while (true) {\n      val testId = completedTestIds.poll() ?: return\n      callJournal.clear(testId)\n    }\n  }\n\n  private suspend fun registerStub(\n    action: String,\n    input: Option<Any> = None,\n    metadata: Map<String, Any> = emptyMap(),\n    request: () -> MappingBuilder\n  ): WireMockSystem {\n    report(action = action, input = input, metadata = metadata) {\n      registerStub(request())\n    }\n    return this\n  }\n\n  private fun registerStub(request: MappingBuilder) {\n    val stub = wireMock.stubFor(request.withId(UUID.randomUUID()))\n    recordStub(stub)\n  }\n\n  private fun recordStub(stub: StubMapping) {\n    stubLog.put(stub.id, stub)\n    callJournal.recordStub(stub)\n  }\n\n  private fun enrichMetadataWithTestId(metadata: Map<String, Any>): Map<String, Any> =\n    metadata + (STOVE_TEST_ID_KEY to reporter.currentTestId())\n\n  private fun configureBodyAndMetadata(\n    request: MappingBuilder,\n    metadata: Map<String, Any>,\n    body: Option<Any>\n  ) {\n    request.withMetadata(enrichMetadataWithTestId(metadata))\n    body.map {\n      request\n        .withRequestBody(\n          equalToJson(\n            serde.serialize(it).decodeToString(),\n            true,\n            false\n          )\n        ).withHeader(CONTENT_TYPE, ContainsPattern(APPLICATION_JSON))\n    }\n  }\n\n  private fun configureResponse(\n    statusCode: Int,\n    responseBody: Option<Any>,\n    responseHeaders: Map<String, String>\n  ): ResponseDefinitionBuilder? {\n    val mockResponse = aResponse()\n      .withStatus(statusCode)\n      .withHeader(CONTENT_TYPE, APPLICATION_JSON_UTF8)\n    responseHeaders.forEach {\n      mockResponse.withHeader(it.key, it.value)\n    }\n    responseBody.map { mockResponse.withBody(serde.serialize(it)) }\n    return mockResponse\n  }\n\n  companion object {\n    /**\n     * Metadata key used to associate stubs with test IDs for filtering in snapshots.\n     */\n    const val STOVE_TEST_ID_KEY = \"stoveTestId\"\n    private const val LOCALHOST = \"localhost\"\n\n    /**\n     * Exposes the [WireMockServer] instance for the given [WireMockSystem].\n     * Use this for advanced WireMock operations not covered by the DSL.\n     */\n    @Suppress(\"unused\")\n    fun WireMockSystem.server(): WireMockServer = wireMock\n  }\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/WireMockVacuumCleaner.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.benmanes.caffeine.cache.Cache\nimport com.github.tomakehurst.wiremock.WireMockServer\nimport com.github.tomakehurst.wiremock.extension.*\nimport com.github.tomakehurst.wiremock.stubbing.*\nimport com.trendyol.stove.functional.*\nimport wiremock.org.slf4j.*\nimport java.util.*\n\nclass WireMockVacuumCleaner(\n  private val stubLog: Cache<UUID, StubMapping>,\n  private val afterStubRemoved: AfterStubRemoved\n) : ServeEventListener {\n  private lateinit var wireMock: WireMockServer\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  override fun getName(): String = WireMockExtensionNames.VACUUM_CLEANER\n\n  fun wireMock(wireMockServer: WireMockServer) {\n    this.wireMock = wireMockServer\n  }\n\n  override fun beforeResponseSent(\n    serveEvent: ServeEvent,\n    parameters: Parameters?\n  ) {\n    if (!serveEvent.wasMatched) {\n      return\n    }\n\n    if (!stubLog.containsKey(serveEvent.stubMapping.id)) {\n      return\n    }\n\n    Try {\n      synchronized(wireMock) {\n        val stubToBeRemoved = stubLog.getIfPresent(serveEvent.stubMapping.id)\n        wireMock.removeStub(stubToBeRemoved)\n        wireMock.removeServeEvent(serveEvent.id)\n        stubLog.invalidate(serveEvent.stubMapping.id)\n        afterStubRemoved(serveEvent, stubLog)\n      }\n    }.recover { throwable -> logger.warn(throwable.message) }\n  }\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/WireMockVerification.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport arrow.core.*\nimport com.github.tomakehurst.wiremock.client.*\nimport com.github.tomakehurst.wiremock.client.WireMock.*\nimport com.github.tomakehurst.wiremock.http.RequestMethod\nimport com.github.tomakehurst.wiremock.matching.*\nimport com.github.tomakehurst.wiremock.verification.LoggedRequest\nimport com.trendyol.stove.serialization.StoveSerde\n\ninternal class WireMockVerification(\n  private val system: WireMockSystem,\n  private val callJournal: WireMockCallJournal,\n  private val serde: StoveSerde<Any, ByteArray>\n) {\n  suspend fun shouldHaveBeenCalled(\n    method: RequestMethod,\n    url: String,\n    count: CountMatchingStrategy = exactly(1),\n    requestBody: Option<Any> = None,\n    requestContaining: Map<String, Any> = emptyMap(),\n    headers: Map<String, String> = emptyMap(),\n    queryParams: Map<String, String> = emptyMap(),\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) }\n  ): WireMockSystem = shouldHaveBeenCalled(count) {\n    requestPattern(\n      method = method,\n      url = url,\n      requestBody = requestBody,\n      requestContaining = requestContaining,\n      headers = headers,\n      queryParams = queryParams,\n      urlPatternFn = urlPatternFn\n    )\n  }\n\n  suspend fun shouldHaveBeenCalled(\n    count: CountMatchingStrategy = exactly(1),\n    request: @WiremockDsl () -> RequestPatternBuilder\n  ): WireMockSystem =\n    verifyCalls(\n      action = WireMockReportActions.VERIFY_REQUEST_WAS_CALLED,\n      count = count,\n      requestPattern = request().build()\n    )\n\n  suspend fun shouldNotHaveBeenCalled(\n    method: RequestMethod,\n    url: String,\n    requestBody: Option<Any> = None,\n    requestContaining: Map<String, Any> = emptyMap(),\n    headers: Map<String, String> = emptyMap(),\n    queryParams: Map<String, String> = emptyMap(),\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) }\n  ): WireMockSystem = shouldHaveBeenCalled(\n    method = method,\n    url = url,\n    count = exactly(0),\n    requestBody = requestBody,\n    requestContaining = requestContaining,\n    headers = headers,\n    queryParams = queryParams,\n    urlPatternFn = urlPatternFn\n  )\n\n  suspend fun shouldNotHaveBeenCalled(\n    request: @WiremockDsl () -> RequestPatternBuilder\n  ): WireMockSystem =\n    verifyCalls(\n      action = WireMockReportActions.VERIFY_REQUEST_WAS_NOT_CALLED,\n      count = exactly(0),\n      requestPattern = request().build()\n    )\n\n  fun callsFor(\n    method: RequestMethod,\n    url: String,\n    requestBody: Option<Any> = None,\n    requestContaining: Map<String, Any> = emptyMap(),\n    headers: Map<String, String> = emptyMap(),\n    queryParams: Map<String, String> = emptyMap(),\n    urlPatternFn: (url: String) -> UrlPattern = { urlEqualTo(it) }\n  ): List<LoggedRequest> = callsFor(\n    requestPattern(\n      method = method,\n      url = url,\n      requestBody = requestBody,\n      requestContaining = requestContaining,\n      headers = headers,\n      queryParams = queryParams,\n      urlPatternFn = urlPatternFn\n    ).build()\n  )\n\n  fun callsFor(\n    request: @WiremockDsl () -> RequestPatternBuilder\n  ): List<LoggedRequest> = callsFor(request().build())\n\n  private suspend fun verifyCalls(\n    action: String,\n    count: CountMatchingStrategy,\n    requestPattern: RequestPattern\n  ): WireMockSystem {\n    val actualCount = callsFor(requestPattern).size\n\n    system.report(\n      action = action,\n      input = requestPattern.toString().some(),\n      expected = count.toString().some(),\n      actual = WireMockValidationMessages.requestCount(actualCount).some()\n    ) {\n      if (!count.match(actualCount)) {\n        throw VerificationException(requestPattern, count, actualCount)\n      }\n    }\n\n    return system\n  }\n\n  private fun callsFor(requestPattern: RequestPattern): List<LoggedRequest> =\n    callJournal.requests(system.reporter.currentTestId())\n      .filter { request -> requestPattern.match(request).isExactMatch }\n\n  private fun requestPattern(\n    method: RequestMethod,\n    url: String,\n    requestBody: Option<Any>,\n    requestContaining: Map<String, Any>,\n    headers: Map<String, String>,\n    queryParams: Map<String, String>,\n    urlPatternFn: (url: String) -> UrlPattern\n  ): RequestPatternBuilder {\n    val request = RequestPatternBuilder.newRequestPattern(method, urlPatternFn(url))\n    requestBody.map {\n      request.withRequestBody(\n        equalToJson(\n          serde.serialize(it).decodeToString(),\n          true,\n          false\n        )\n      )\n    }\n    request.configureBodyContaining(requestContaining, serde)\n    headers.forEach { (key, value) -> request.withHeader(key, equalTo(value)) }\n    queryParams.forEach { (key, value) -> request.withQueryParam(key, equalTo(value)) }\n    return request\n  }\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/WiremockDsl.kt",
    "content": "package com.trendyol.stove.wiremock\n\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class WiremockDsl\n"
  },
  {
    "path": "lib/stove-wiremock/src/main/kotlin/com/trendyol/stove/wiremock/stubbing.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.tomakehurst.wiremock.WireMockServer\nimport com.github.tomakehurst.wiremock.client.*\nimport com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED\nimport com.github.tomakehurst.wiremock.stubbing.StubMapping\nimport com.trendyol.stove.serialization.StoveSerde\n\ninternal fun stubBehaviour(\n  wireMockServer: WireMockServer,\n  serde: StoveSerde<Any, ByteArray>,\n  url: String,\n  method: (String) -> MappingBuilder,\n  metadata: Map<String, Any> = emptyMap(),\n  recordStub: (StubMapping) -> Unit = {},\n  block: StubBehaviourBuilder.(StoveSerde<Any, ByteArray>) -> Unit\n) {\n  val builder = StubBehaviourBuilder(wireMockServer, url, method, metadata, recordStub)\n  builder.block(serde)\n}\n\nclass StubBehaviourBuilder(\n  private val wireMockServer: WireMockServer,\n  private val url: String,\n  private val method: (String) -> MappingBuilder,\n  private val metadata: Map<String, Any> = emptyMap()\n) {\n  private val scenarioName = WireMockBehaviourNames.scenarioName(url)\n  private var previousState: String = STARTED\n  private var stateCounter = 0\n  private var initializedCounter = 0\n  private var recordStub: (StubMapping) -> Unit = {}\n\n  internal constructor(\n    wireMockServer: WireMockServer,\n    url: String,\n    method: (String) -> MappingBuilder,\n    metadata: Map<String, Any> = emptyMap(),\n    recordStub: (StubMapping) -> Unit\n  ) : this(wireMockServer, url, method, metadata) {\n    this.recordStub = recordStub\n  }\n\n  fun initially(step: () -> ResponseDefinitionBuilder) {\n    check(initializedCounter == 0) { WireMockBehaviourMessages.INITIALLY_ONCE }\n    stateCounter++\n    val nextState = WireMockBehaviourNames.state(stateCounter)\n    createStub(step(), previousState, nextState)\n    previousState = nextState\n    initializedCounter++\n  }\n\n  fun then(step: () -> ResponseDefinitionBuilder) {\n    check(previousState != STARTED) { WireMockBehaviourMessages.INITIALLY_BEFORE_THEN }\n    stateCounter++\n    val nextState = WireMockBehaviourNames.state(stateCounter)\n    createStub(step(), previousState, nextState)\n    previousState = nextState\n  }\n\n  private fun createStub(\n    response: ResponseDefinitionBuilder,\n    whenState: String,\n    setState: String\n  ) {\n    val stub = wireMockServer.stubFor(\n      method(url)\n        .inScenario(scenarioName)\n        .whenScenarioStateIs(whenState)\n        .willReturn(response)\n        .willSetStateTo(setState)\n        .withMetadata(metadata)\n    )\n    recordStub(stub)\n  }\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/test/kotlin/com/trendyol/stove/wiremock/ExtensionsTest.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.benmanes.caffeine.cache.Caffeine\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass ExtensionsTest :\n  FunSpec({\n    test(\"containsKey should reflect cache contents\") {\n      val cache = Caffeine.newBuilder().build<String, String>()\n\n      cache.containsKey(\"missing\") shouldBe false\n\n      cache.put(\"key\", \"value\")\n      cache.containsKey(\"key\") shouldBe true\n    }\n  })\n"
  },
  {
    "path": "lib/stove-wiremock/src/test/kotlin/com/trendyol/stove/wiremock/StoveConfig.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.PortFinder\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\n\nval WIREMOCK_PORT = PortFinder.findAvailablePort()\nval WIREMOCK_BASE_URL = \"http://localhost:$WIREMOCK_PORT\"\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        wiremock {\n          WireMockSystemOptions(\n            port = WIREMOCK_PORT,\n            removeStubAfterRequestMatched = true\n          )\n        }\n        applicationUnderTest(\n          object : ApplicationUnderTest<Unit> {\n            override suspend fun start(configurations: List<String>) = Unit\n\n            override suspend fun stop() = Unit\n          }\n        )\n      }.run()\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n"
  },
  {
    "path": "lib/stove-wiremock/src/test/kotlin/com/trendyol/stove/wiremock/WireMockDeletionTest.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.tomakehurst.wiremock.client.WireMock.*\nimport com.github.tomakehurst.wiremock.matching.ContainsPattern\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport java.net.URI\nimport java.net.http.*\nimport java.net.http.HttpRequest.BodyPublishers\nimport java.net.http.HttpResponse.BodyHandlers\n\nclass WireMockDeletionTest :\n  FunSpec({\n    /*\n     * Check [WireMockContext.removeStubAfterRequestMatched]\n     */\n    test(\"Remove stub from wiremock when request is matched\") {\n      val reqBody = \"{\\\"req\\\": 1}\"\n      val responseBody = \"{\\\"res\\\": 1}\"\n      stove {\n        wiremock {\n          mockPostConfigure(\"/post-url\") { req, _ ->\n            req\n              .withRequestBody(equalToJson(reqBody))\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(responseBody)\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val client = HttpClient.newBuilder().build()\n      val reqBuilder = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL/post-url\"))\n        .header(\"Content-Type\", \"application/json\")\n\n      val request = reqBuilder.POST(BodyPublishers.ofString(reqBody)).build()\n      val response = client.send(request, BodyHandlers.ofString())\n      response.body() shouldBe responseBody\n\n      val request2 = reqBuilder.POST(BodyPublishers.ofString(reqBody)).build()\n      val response2 = client.send(request2, BodyHandlers.ofString())\n      response2.statusCode() shouldBe 404\n    }\n\n    /*\n     * Check [WireMockContext.removeStubAfterRequestMatched]\n     */\n    test(\"Removes the stub after request completes, and can be added again\") {\n      val reqBody = \"{\\\"req\\\": 1}\"\n      val responseBody = \"{\\\"res\\\": 1}\"\n      val url = \"/post-url-2\"\n      stove {\n        wiremock {\n          mockPostConfigure(url) { req, _ ->\n            req\n              .withRequestBody(equalToJson(reqBody))\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(responseBody)\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val client = HttpClient.newBuilder().build()\n      val reqBuilder = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n\n      val request = reqBuilder.POST(BodyPublishers.ofString(reqBody)).build()\n      val response = client.send(request, BodyHandlers.ofString())\n      response.body() shouldBe responseBody\n\n      stove {\n        wiremock {\n          mockPostConfigure(url) { req, _ ->\n            req\n              .withRequestBody(equalToJson(reqBody))\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(responseBody)\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val request2 = reqBuilder.POST(BodyPublishers.ofString(reqBody)).build()\n      val response2 = client.send(request2, BodyHandlers.ofString())\n      response2.body() shouldBe responseBody\n    }\n  })\n"
  },
  {
    "path": "lib/stove-wiremock/src/test/kotlin/com/trendyol/stove/wiremock/WireMockExposedConfigurationTest.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport io.kotest.core.spec.IsolationMode\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\n/**\n * Tests for [WireMockExposedConfiguration] data class.\n * These tests are isolated and don't require a running Stove instance.\n */\nclass WireMockExposedConfigurationTest :\n  FunSpec({\n    isolationMode = IsolationMode.InstancePerTest\n\n    test(\"WireMockExposedConfiguration should have correct baseUrl format\") {\n      val config = WireMockExposedConfiguration(host = \"localhost\", port = 9090)\n      config.baseUrl shouldBe \"http://localhost:9090\"\n    }\n\n    test(\"WireMockExposedConfiguration should handle different hosts\") {\n      val config = WireMockExposedConfiguration(host = \"127.0.0.1\", port = 8080)\n      config.baseUrl shouldBe \"http://127.0.0.1:8080\"\n    }\n\n    test(\"WireMockExposedConfiguration should handle different ports\") {\n      val config = WireMockExposedConfiguration(host = \"localhost\", port = 0)\n      config.baseUrl shouldBe \"http://localhost:0\"\n\n      val config2 = WireMockExposedConfiguration(host = \"localhost\", port = 65535)\n      config2.baseUrl shouldBe \"http://localhost:65535\"\n    }\n\n    test(\"WireMockSystemOptions default configureExposedConfiguration returns empty list\") {\n      val options = WireMockSystemOptions()\n      val config = WireMockExposedConfiguration(host = \"localhost\", port = 9090)\n      options.configureExposedConfiguration(config) shouldBe emptyList()\n    }\n\n    test(\"WireMockSystemOptions custom configureExposedConfiguration is called correctly\") {\n      val options = WireMockSystemOptions(\n        port = 0,\n        configureExposedConfiguration = { cfg ->\n          listOf(\n            \"api.url=${cfg.baseUrl}\",\n            \"api.port=${cfg.port}\"\n          )\n        }\n      )\n      val config = WireMockExposedConfiguration(host = \"localhost\", port = 12345)\n      val result = options.configureExposedConfiguration(config)\n\n      result shouldBe listOf(\n        \"api.url=http://localhost:12345\",\n        \"api.port=12345\"\n      )\n    }\n  })\n"
  },
  {
    "path": "lib/stove-wiremock/src/test/kotlin/com/trendyol/stove/wiremock/WireMockOperationsTest.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport com.github.tomakehurst.wiremock.client.WireMock.*\nimport com.github.tomakehurst.wiremock.matching.ContainsPattern\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport java.net.URI\nimport java.net.http.*\nimport java.net.http.HttpRequest.BodyPublishers\nimport java.net.http.HttpResponse.BodyHandlers\n\nclass WireMockOperationsTest :\n  FunSpec({\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockPostConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to use the default URL pattern for [WireMockSystem.mockPostConfigure].\n     */\n    test(\"Wiremock mockPostConfigure should mock urls with urlEqualTo(url) pattern in default\") {\n      val url = \"/post-url\"\n      val client = HttpClient.newBuilder().build()\n      val reqBuilder = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL/$url\"))\n        .header(\"Content-Type\", \"application/json\")\n\n      stove {\n        wiremock {\n          mockPostConfigure(\"/$url\") { req, _ ->\n            req\n              .withRequestBody(equalTo(\"request2\"))\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"response2\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n          mockPostConfigure(\"/$url\") { req, _ ->\n            req\n              .withRequestBody(equalTo(\"request1\"))\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"response1\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val request2 = reqBuilder.POST(BodyPublishers.ofString(\"request2\")).build()\n      val response2 = client.send(request2, BodyHandlers.ofString())\n      response2.body() shouldBe \"response2\"\n\n      val request1 = reqBuilder.POST(BodyPublishers.ofString(\"request1\")).build()\n      val response1 = client.send(request1, BodyHandlers.ofString())\n      response1.body() shouldBe \"response1\"\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockPostConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to configure and override the default URL matcher for [WireMockSystem.mockPostConfigure].\n     */\n    test(\"Wiremock mockPostConfigure should accept overridden urlMatcher\") {\n      val url = \"categories/createCategory\"\n      val client = HttpClient.newBuilder().build()\n      val reqBuilder = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL/$url\"))\n        .header(\"Content-Type\", \"application/json\")\n\n      stove {\n        wiremock {\n          mockPostConfigure(\"/categories/.*\", { urlPathMatching(it) }) { req, _ ->\n            req\n              .withRequestBody(equalTo(\"request2\"))\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"response2\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n          mockPostConfigure(\"/categories/.*\", { urlPathMatching(it) }) { req, _ ->\n            req\n              .withRequestBody(equalTo(\"request1\"))\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"response1\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val request2 = reqBuilder.POST(BodyPublishers.ofString(\"request2\")).build()\n      val response2 = client.send(request2, BodyHandlers.ofString())\n      response2.body() shouldBe \"response2\"\n\n      val request1 = reqBuilder.POST(BodyPublishers.ofString(\"request1\")).build()\n      val response1 = client.send(request1, BodyHandlers.ofString())\n      response1.body() shouldBe \"response1\"\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockGetConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to use the default URL pattern for [WireMockSystem.mockGetConfigure].\n     */\n    test(\"Wiremock mockGetConfigure should mock urls with urlEqualTo(url) pattern in default\") {\n      val client = HttpClient.newBuilder().build()\n      var id = 1\n      var active = true\n      stove {\n        wiremock {\n          mockGetConfigure(\"/suppliers/1?active=true\") { req, _ ->\n            req\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"Supplier1Response\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n          mockGetConfigure(\"/suppliers/2?active=false\") { req, _ ->\n            req\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"Supplier2Response\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val uri = URI.create(\"$WIREMOCK_BASE_URL/suppliers/$id?active=$active\")\n      val reqBuilder = HttpRequest\n        .newBuilder(uri)\n        .header(\"Content-Type\", \"application/json\")\n\n      val request2 = reqBuilder.GET().build()\n      val response2 = client.send(request2, BodyHandlers.ofString())\n      response2.body() shouldBe \"Supplier1Response\"\n\n      id = 2\n      active = false\n      val uri2 = URI.create(\"$WIREMOCK_BASE_URL/suppliers/$id?active=$active\")\n      val reqBuilder2 = HttpRequest\n        .newBuilder(uri2)\n        .header(\"Content-Type\", \"application/json\")\n      val request1 = reqBuilder2.GET().build()\n      val response1 = client.send(request1, BodyHandlers.ofString())\n      response1.body() shouldBe \"Supplier2Response\"\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockGetConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to configure and override the default URL matcher for [WireMockSystem.mockGetConfigure].\n     */\n    test(\"Wiremock mockGetConfigure should accept overridden urlMatcher\") {\n      val client = HttpClient.newBuilder().build()\n      stove {\n        wiremock {\n          mockGetConfigure(\"/suppliers/1.*\", { urlPathMatching(it) }) { req, _ ->\n            req\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .withQueryParam(\"active\", matching(\"true|false\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"Supplier1Response\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n          mockGetConfigure(\"/suppliers/2.*\", { urlPathMatching(it) }) { req, _ ->\n            req\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .withQueryParam(\"active\", matching(\"true|false\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"Supplier2Response\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      var id = 1\n      var active = true\n      val uri1 = URI.create(\"$WIREMOCK_BASE_URL/suppliers/$id?active=$active\")\n      val request1 = HttpRequest\n        .newBuilder(uri1)\n        .header(\"Content-Type\", \"application/json\")\n        .GET()\n        .build()\n      val response1 = client.send(request1, BodyHandlers.ofString())\n      response1.body() shouldBe \"Supplier1Response\"\n\n      id = 2\n      active = false\n      val uri2 = URI.create(\"$WIREMOCK_BASE_URL/suppliers/$id?active=$active\")\n      val request2 = HttpRequest\n        .newBuilder(uri2)\n        .header(\"Content-Type\", \"application/json\")\n        .GET()\n        .build()\n      val response2 = client.send(request2, BodyHandlers.ofString())\n      response2.body() shouldBe \"Supplier2Response\"\n      stove {\n        wiremock {\n          mockGetConfigure(\"/suppliers/2.*\", { urlPathMatching(it) }) { req, _ ->\n            req\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .withQueryParam(\"active\", matching(\"true|false\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"Supplier2Response\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n      active = true\n      val uri3 = URI.create(\"$WIREMOCK_BASE_URL/suppliers/$id?active=$active\")\n      val request3 = HttpRequest\n        .newBuilder(uri3)\n        .header(\"Content-Type\", \"application/json\")\n        .GET()\n        .build()\n      val response3 = client.send(request3, BodyHandlers.ofString())\n      response3.body() shouldBe \"Supplier2Response\"\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockPutConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to use the default URL pattern for [WireMockSystem.mockPutConfigure].\n     */\n    test(\"Wiremock mockPutConfigure should accept default urlMatcher\") {\n      val client = HttpClient.newBuilder().build()\n      stove {\n        wiremock {\n          mockPutConfigure(\"/resources/1\") { req, _ ->\n            req\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"PutResource1\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val uri = URI.create(\"$WIREMOCK_BASE_URL/resources/1\")\n      val reqBuilder = HttpRequest\n        .newBuilder(uri)\n        .header(\"Content-Type\", \"application/json\")\n        .PUT(BodyPublishers.ofString(\"{\\\"name\\\":\\\"test\\\"}\"))\n\n      val request = reqBuilder.build()\n      val response = client.send(request, BodyHandlers.ofString())\n      response.body() shouldBe \"PutResource1\"\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockPutConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to configure and override the default URL matcher for [WireMockSystem.mockPutConfigure].\n     */\n    test(\"Wiremock mockPutConfigure should accept overridden urlMatcher\") {\n      val client = HttpClient.newBuilder().build()\n      stove {\n        wiremock {\n          mockPutConfigure(\"/resources/.*\", { urlPathMatching(it) }) { req, _ ->\n            req\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"PutResourceMatched\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val uri = URI.create(\"$WIREMOCK_BASE_URL/resources/123\")\n      val reqBuilder = HttpRequest\n        .newBuilder(uri)\n        .header(\"Content-Type\", \"application/json\")\n        .PUT(BodyPublishers.ofString(\"{\\\"name\\\":\\\"test\\\"}\"))\n\n      val request = reqBuilder.build()\n      val response = client.send(request, BodyHandlers.ofString())\n      response.body() shouldBe \"PutResourceMatched\"\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockDeleteConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to use the default URL pattern for [WireMockSystem.mockDeleteConfigure].\n     */\n    test(\"Wiremock mockDeleteConfigure should accept default urlMatcher\") {\n      val client = HttpClient.newBuilder().build()\n      stove {\n        wiremock {\n          mockDeleteConfigure(\"/resources/1\") { req, _ ->\n            req\n              .withHeader(\"Authorization\", equalTo(\"Bearer token\"))\n              .willReturn(\n                aResponse().withStatus(204)\n              )\n          }\n        }\n\n        val uri = URI.create(\"$WIREMOCK_BASE_URL/resources/1\")\n        val reqBuilder = HttpRequest\n          .newBuilder(uri)\n          .header(\"Authorization\", \"Bearer token\")\n          .DELETE()\n\n        val request = reqBuilder.build()\n        val response = client.send(request, BodyHandlers.ofString())\n\n        response.statusCode() shouldBe 204\n      }\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockDeleteConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to configure and override the default URL matcher for [WireMockSystem.mockDeleteConfigure].\n     */\n    test(\"Wiremock mockDeleteConfigure should accept overridden urlMatcher\") {\n      val client = HttpClient.newBuilder().build()\n      stove {\n        wiremock {\n          mockDeleteConfigure(\"/resources/.*\", { urlPathMatching(it) }) { req, _ ->\n            req\n              .withHeader(\"Authorization\", equalTo(\"Bearer token\"))\n              .willReturn(\n                aResponse().withStatus(204)\n              )\n          }\n        }\n\n        val uri = URI.create(\"$WIREMOCK_BASE_URL/resources/123\")\n        val reqBuilder = HttpRequest\n          .newBuilder(uri)\n          .header(\"Authorization\", \"Bearer token\")\n          .DELETE()\n\n        val request = reqBuilder.build()\n        val response = client.send(request, BodyHandlers.ofString())\n\n        response.statusCode() shouldBe 204\n      }\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockPatchConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to use the default URL pattern for [WireMockSystem.mockPatchConfigure].\n     */\n    test(\"Wiremock mockPatchConfigure should accept default urlMatcher\") {\n      val client = HttpClient.newBuilder().build()\n      stove {\n        wiremock {\n          mockPatchConfigure(\"/resources/1\") { req, _ ->\n            req\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"PatchResource1\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val uri = URI.create(\"$WIREMOCK_BASE_URL/resources/1\")\n      val reqBuilder = HttpRequest\n        .newBuilder(uri)\n        .header(\"Content-Type\", \"application/json\")\n        .method(\"PATCH\", BodyPublishers.ofString(\"{\\\"name\\\":\\\"updated\\\"}\"))\n\n      val request = reqBuilder.build()\n      val response = client.send(request, BodyHandlers.ofString())\n      response.body() shouldBe \"PatchResource1\"\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockPatchConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to configure and override the default URL matcher for [WireMockSystem.mockPatchConfigure].\n     */\n    test(\"Wiremock mockPatchConfigure should accept overridden urlMatcher\") {\n      val client = HttpClient.newBuilder().build()\n      stove {\n        wiremock {\n          mockPatchConfigure(\"/resources/.*\", { (urlPathMatching(it)) }) { req, _ ->\n            req\n              .withHeader(\"Content-Type\", ContainsPattern(\"application/json\"))\n              .willReturn(\n                aResponse()\n                  .withBody(\"PatchResourceMatched\")\n                  .withStatus(200)\n                  .withHeader(\"Content-Type\", \"application/json; charset=UTF-8\")\n              )\n          }\n        }\n      }\n\n      val uri = URI.create(\"$WIREMOCK_BASE_URL/resources/123\")\n      val reqBuilder = HttpRequest\n        .newBuilder(uri)\n        .header(\"Content-Type\", \"application/json\")\n        .method(\"PATCH\", BodyPublishers.ofString(\"{\\\"name\\\":\\\"updated\\\"}\"))\n\n      val request = reqBuilder.build()\n      val response = client.send(request, BodyHandlers.ofString())\n      response.body() shouldBe \"PatchResourceMatched\"\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockHeadConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to use the default URL pattern for [WireMockSystem.mockHeadConfigure].\n     */\n    test(\"Wiremock mockHeadConfigure should accept default urlMatcher\") {\n      val client = HttpClient.newBuilder().build()\n      stove {\n        wiremock {\n          mockHeadConfigure(\"/resources/1\") { req, _ ->\n            req\n              .withHeader(\"Authorization\", equalTo(\"Bearer token\"))\n              .willReturn(\n                aResponse()\n                  .withStatus(200)\n                  .withHeader(\"X-Custom-Header\", \"CustomValue\")\n              )\n          }\n        }\n      }\n\n      val uri = URI.create(\"$WIREMOCK_BASE_URL/resources/1\")\n      val reqBuilder = HttpRequest\n        .newBuilder(uri)\n        .header(\"Authorization\", \"Bearer token\")\n        .method(\"HEAD\", BodyPublishers.noBody())\n\n      val request = reqBuilder.build()\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.headers().firstValue(\"X-Custom-Header\").orElse(\"\") shouldBe \"CustomValue\"\n    }\n\n    /*\n     * Configures a POST request mock using [WireMockSystem.mockHeadConfigure].\n     *\n     * @param urlMatcher A [UrlPattern] used to match the request URL. Defaults to [urlEqualTo] with the provided [url].\n     *\n     * This test demonstrates how to configure and override the default URL matcher for [WireMockSystem.mockHeadConfigure].\n     */\n    test(\"Wiremock mockHeadConfigure should accept overridden urlMatcher\") {\n      val client = HttpClient.newBuilder().build()\n      stove {\n        wiremock {\n          mockHeadConfigure(\"/resources/.*\", { urlPathMatching(it) }) { req, _ ->\n            req\n              .withHeader(\"Authorization\", equalTo(\"Bearer token\"))\n              .willReturn(\n                aResponse()\n                  .withStatus(200)\n                  .withHeader(\"X-Overridden-Header\", \"OverriddenValue\")\n              )\n          }\n        }\n      }\n\n      val uri = URI.create(\"$WIREMOCK_BASE_URL/resources/123\")\n      val reqBuilder = HttpRequest\n        .newBuilder(uri)\n        .header(\"Authorization\", \"Bearer token\")\n        .method(\"HEAD\", BodyPublishers.noBody())\n\n      val request = reqBuilder.build()\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.headers().firstValue(\"X-Overridden-Header\").orElse(\"\") shouldBe \"OverriddenValue\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove-wiremock/src/test/kotlin/com/trendyol/stove/wiremock/WireMockPartialMockingTest.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport arrow.core.some\nimport com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport org.intellij.lang.annotations.Language\nimport java.net.URI\nimport java.net.http.*\nimport java.net.http.HttpRequest.BodyPublishers\nimport java.net.http.HttpResponse.BodyHandlers\n\nclass WireMockPartialMockingTest :\n  FunSpec({\n\n    val client = HttpClient.newBuilder().build()\n\n    test(\"mockPostContaining should match requests containing specified fields\") {\n      val uniqueProductId = 12345\n      val url = \"/orders\"\n\n      stove {\n        wiremock {\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\"productId\" to uniqueProductId),\n            statusCode = 201,\n            responseBody = mapOf(\"orderId\" to \"order-123\", \"status\" to \"created\").some()\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\"productId\": $uniqueProductId, \"quantity\": 5, \"customerName\": \"John Doe\"}\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 201\n      response.body() shouldBe \"\"\"{\"orderId\":\"order-123\",\"status\":\"created\"}\"\"\"\n    }\n\n    test(\"mockPostContaining should match requests with multiple containing fields (AND logic)\") {\n      val productId = 999\n      val customerId = \"cust-abc\"\n      val url = \"/orders/multi\"\n\n      stove {\n        wiremock {\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\n              \"productId\" to productId,\n              \"customerId\" to customerId\n            ),\n            statusCode = 200,\n            responseBody = mapOf(\"matched\" to true).some()\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\"productId\": $productId, \"customerId\": \"$customerId\", \"extra\": \"ignored\"}\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"matched\":true}\"\"\"\n    }\n\n    test(\"mockPostContaining should NOT match when one of multiple required fields is missing (AND logic)\") {\n      val url = \"/orders/and-logic-test\"\n\n      stove {\n        wiremock {\n          // Stub expects BOTH productId AND customerId to match\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\n              \"productId\" to 123,\n              \"customerId\" to \"cust-required\"\n            ),\n            statusCode = 200,\n            responseBody = mapOf(\"matched\" to true).some()\n          )\n        }\n      }\n\n      // Request only has productId, missing customerId - should NOT match\n      val requestBody = \"\"\"{\"productId\": 123, \"extra\": \"data\"}\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 404 // Not matched because customerId is missing\n    }\n\n    test(\"mockPostContaining should NOT match when field value is different (AND logic)\") {\n      val url = \"/orders/and-logic-value-test\"\n\n      stove {\n        wiremock {\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\n              \"productId\" to 123,\n              \"status\" to \"active\"\n            ),\n            statusCode = 200,\n            responseBody = mapOf(\"matched\" to true).some()\n          )\n        }\n      }\n\n      // Request has both fields but status has wrong value - should NOT match\n      val requestBody = \"\"\"{\"productId\": 123, \"status\": \"inactive\"}\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 404 // Not matched because status value is different\n    }\n\n    test(\"mockPutContaining should match PUT requests containing specified fields\") {\n      val userId = \"user-456\"\n      val url = \"/users/456\"\n\n      stove {\n        wiremock {\n          mockPutContaining(\n            url = url,\n            requestContaining = mapOf(\"userId\" to userId),\n            statusCode = 200,\n            responseBody = mapOf(\"updated\" to true).some()\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\"userId\": \"$userId\", \"name\": \"Updated Name\", \"email\": \"test@example.com\"}\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .PUT(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"updated\":true}\"\"\"\n    }\n\n    test(\"mockPatchContaining should match PATCH requests containing specified fields\") {\n      val status = \"active\"\n      val url = \"/users/789/status\"\n\n      stove {\n        wiremock {\n          mockPatchContaining(\n            url = url,\n            requestContaining = mapOf(\"status\" to status),\n            statusCode = 200,\n            responseBody = mapOf(\"status\" to status).some()\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\"status\": \"$status\", \"updatedBy\": \"admin\", \"timestamp\": 1234567890}\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .method(\"PATCH\", BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"status\":\"active\"}\"\"\"\n    }\n\n    test(\"mockPostContaining should work with URL pattern matching\") {\n      val transactionId = \"txn-unique-123\"\n\n      stove {\n        wiremock {\n          mockPostContaining(\n            url = \"/payments/.*\",\n            requestContaining = mapOf(\"transactionId\" to transactionId),\n            statusCode = 200,\n            responseBody = mapOf(\"processed\" to true).some(),\n            urlPatternFn = { urlPathMatching(it) }\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\"transactionId\": \"$transactionId\", \"amount\": 99.99}\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL/payments/credit-card\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"processed\":true}\"\"\"\n    }\n\n    test(\"mockPostContaining should support custom response headers\") {\n      val url = \"/with-headers\"\n\n      stove {\n        wiremock {\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\"id\" to 1),\n            statusCode = 200,\n            responseHeaders = mapOf(\"X-Custom-Header\" to \"CustomValue\")\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\"id\": 1, \"extra\": \"data\"}\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.headers().firstValue(\"X-Custom-Header\").orElse(\"\") shouldBe \"CustomValue\"\n    }\n\n    test(\"mockPostContaining should match nested objects\") {\n      val url = \"/nested-objects\"\n\n      stove {\n        wiremock {\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\"user\" to mapOf(\"id\" to 123)),\n            statusCode = 200,\n            responseBody = mapOf(\"success\" to true).some()\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\"user\": {\"id\": 123, \"name\": \"John\"}, \"action\": \"update\"}\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"success\":true}\"\"\"\n    }\n\n    test(\"mockPostContaining should match deeply nested objects with partial matching\") {\n      val url = \"/deep-nested\"\n\n      stove {\n        wiremock {\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\n              \"order\" to mapOf(\n                \"customer\" to mapOf(\n                  \"id\" to \"cust-deep-123\"\n                )\n              )\n            ),\n            statusCode = 200,\n            responseBody = mapOf(\"deepMatched\" to true).some()\n          )\n        }\n      }\n\n      // Request has extra fields at every level\n      val requestBody = \"\"\"{\n        \"order\": {\n          \"id\": \"order-1\",\n          \"customer\": {\n            \"id\": \"cust-deep-123\",\n            \"name\": \"Deep Customer\",\n            \"email\": \"deep@example.com\"\n          },\n          \"items\": [{\"sku\": \"ABC\"}]\n        },\n        \"timestamp\": 1234567890\n      }\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"deepMatched\":true}\"\"\"\n    }\n\n    test(\"mockPostContaining should match arrays in nested objects\") {\n      val url = \"/nested-arrays\"\n\n      stove {\n        wiremock {\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\n              \"data\" to mapOf(\n                \"tags\" to listOf(\"important\", \"urgent\")\n              )\n            ),\n            statusCode = 200,\n            responseBody = mapOf(\"arrayMatched\" to true).some()\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\n        \"data\": {\n          \"tags\": [\"important\", \"urgent\"],\n          \"other\": \"ignored\"\n        },\n        \"metadata\": {}\n      }\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"arrayMatched\":true}\"\"\"\n    }\n\n    test(\"mockPutContaining should match deeply nested structures\") {\n      val url = \"/deep-put\"\n\n      stove {\n        wiremock {\n          mockPutContaining(\n            url = url,\n            requestContaining = mapOf(\n              \"config\" to mapOf(\n                \"settings\" to mapOf(\n                  \"enabled\" to true,\n                  \"level\" to 5\n                )\n              )\n            ),\n            statusCode = 200,\n            responseBody = mapOf(\"configured\" to true).some()\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\n        \"config\": {\n          \"name\": \"test-config\",\n          \"settings\": {\n            \"enabled\": true,\n            \"level\": 5,\n            \"extra\": \"data\"\n          }\n        }\n      }\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .PUT(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"configured\":true}\"\"\"\n    }\n\n    test(\"mockPatchContaining should match complex nested structures\") {\n      val url = \"/complex-patch\"\n\n      stove {\n        wiremock {\n          mockPatchContaining(\n            url = url,\n            requestContaining = mapOf(\n              \"update\" to mapOf(\n                \"type\" to \"partial\",\n                \"fields\" to mapOf(\"status\" to \"active\")\n              )\n            ),\n            statusCode = 200,\n            responseBody = mapOf(\"patched\" to true).some()\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\n        \"update\": {\n          \"type\": \"partial\",\n          \"fields\": {\n            \"status\": \"active\",\n            \"timestamp\": 9999\n          },\n          \"meta\": {\"source\": \"api\"}\n        }\n      }\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .method(\"PATCH\", BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"patched\":true}\"\"\"\n    }\n\n    test(\"mockPostContaining should match single key in deep nested JSON using dot notation\") {\n      val url = \"/deep-single-key\"\n      val deepCustomerId = \"deep-cust-xyz\"\n\n      stove {\n        wiremock {\n          // Using dot notation to match a single key deep in the JSON\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\"order.customer.id\" to deepCustomerId),\n            statusCode = 200,\n            responseBody = mapOf(\"deepKeyMatched\" to true).some()\n          )\n        }\n      }\n\n      val requestBody = \"\"\"{\n        \"order\": {\n          \"id\": \"order-999\",\n          \"customer\": {\n            \"id\": \"$deepCustomerId\",\n            \"name\": \"Deep User\",\n            \"address\": {\n              \"city\": \"Istanbul\"\n            }\n          },\n          \"items\": [{\"sku\": \"ITEM-1\"}]\n        },\n        \"metadata\": {\"source\": \"test\"}\n      }\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"deepKeyMatched\":true}\"\"\"\n    }\n\n    test(\"mockPostContaining should match multiple single keys at different depths using dot notation\") {\n      val url = \"/multi-deep-keys\"\n\n      stove {\n        wiremock {\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\n              \"order.customer.id\" to \"cust-multi-123\",\n              \"order.payment.method\" to \"credit_card\",\n              \"metadata.version\" to 2\n            ),\n            statusCode = 200,\n            responseBody = mapOf(\"multiDeepMatched\" to true).some()\n          )\n        }\n      }\n\n      @Language(\"JSON\")\n      val requestBody = \"\"\"{\n        \"order\": {\n          \"id\": \"order-multi\",\n          \"customer\": {\n            \"id\": \"cust-multi-123\",\n            \"name\": \"Multi Test\"\n          },\n          \"payment\": {\n            \"method\": \"credit_card\",\n            \"amount\": 99.99\n          }\n        },\n        \"metadata\": {\n          \"version\": 2,\n          \"timestamp\": 1234567890\n        }\n      }\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"multiDeepMatched\":true}\"\"\"\n    }\n\n    test(\"mockPostContaining should match nested object at deep path using dot notation\") {\n      val url = \"/deep-nested-object\"\n\n      stove {\n        wiremock {\n          // Match a nested object at a deep path\n          mockPostContaining(\n            url = url,\n            requestContaining = mapOf(\n              \"data.config.settings\" to mapOf(\"enabled\" to true)\n            ),\n            statusCode = 200,\n            responseBody = mapOf(\"deepObjectMatched\" to true).some()\n          )\n        }\n      }\n\n      @Language(\"JSON\")\n      val requestBody = \"\"\"{\n        \"data\": {\n          \"config\": {\n            \"name\": \"test\",\n            \"settings\": {\n              \"enabled\": true,\n              \"level\": 5,\n              \"extra\": \"ignored\"\n            }\n          }\n        }\n      }\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"deepObjectMatched\":true}\"\"\"\n    }\n\n    test(\"mockPutContaining should match deep key with dot notation\") {\n      val url = \"/deep-put-key\"\n\n      stove {\n        wiremock {\n          mockPutContaining(\n            url = url,\n            requestContaining = mapOf(\"user.profile.settings.theme\" to \"dark\"),\n            statusCode = 200,\n            responseBody = mapOf(\"themeUpdated\" to true).some()\n          )\n        }\n      }\n\n      @Language(\"JSON\")\n      val requestBody = \"\"\"{\n        \"user\": {\n          \"id\": \"user-1\",\n          \"profile\": {\n            \"name\": \"Test User\",\n            \"settings\": {\n              \"theme\": \"dark\",\n              \"notifications\": true\n            }\n          }\n        }\n      }\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .PUT(BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"themeUpdated\":true}\"\"\"\n    }\n\n    test(\"mockPatchContaining should match deep key with dot notation\") {\n      val url = \"/deep-patch-key\"\n\n      stove {\n        wiremock {\n          mockPatchContaining(\n            url = url,\n            requestContaining = mapOf(\"document.section.paragraph.text\" to \"updated content\"),\n            statusCode = 200,\n            responseBody = mapOf(\"textUpdated\" to true).some()\n          )\n        }\n      }\n\n      @Language(\"JSON\")\n      val requestBody = \"\"\"{\n        \"document\": {\n          \"title\": \"My Doc\",\n          \"section\": {\n            \"id\": 1,\n            \"paragraph\": {\n              \"text\": \"updated content\",\n              \"style\": \"normal\"\n            }\n          }\n        }\n      }\"\"\"\n      val request = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .method(\"PATCH\", BodyPublishers.ofString(requestBody))\n        .build()\n\n      val response = client.send(request, BodyHandlers.ofString())\n      response.statusCode() shouldBe 200\n      response.body() shouldBe \"\"\"{\"textUpdated\":true}\"\"\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove-wiremock/src/test/kotlin/com/trendyol/stove/wiremock/WireMockSystemTests.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport arrow.core.some\nimport com.github.tomakehurst.wiremock.WireMockServer\nimport com.github.tomakehurst.wiremock.client.WireMock.*\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.*\nimport java.net.URI\nimport java.net.http.*\nimport java.net.http.HttpRequest.BodyPublishers\nimport java.net.http.HttpResponse.BodyHandlers\n\nclass WireMockSystemTests :\n  FunSpec({\n    lateinit var wireMock: WireMockServer\n    lateinit var client: HttpClient\n    lateinit var reqBuilder: HttpRequest.Builder\n    val url = \"post-url\"\n    beforeSpec {\n      wireMock = WireMockServer(0)\n      wireMock.start()\n      client = HttpClient.newBuilder().build()\n      reqBuilder = HttpRequest.newBuilder(URI(\"http://localhost:${wireMock.port()}/$url\"))\n    }\n\n    test(\"Single thread stubbing\") {\n      wireMock.stubFor(\n        post(\"/$url\")\n          .withRequestBody(equalTo(\"request1\"))\n          .willReturn(\n            aResponse()\n              .withBody(\"response1\")\n          )\n      )\n\n      wireMock.stubFor(\n        post(\"/$url\")\n          .withRequestBody(equalTo(\"request2\"))\n          .willReturn(\n            aResponse()\n              .withBody(\"response2\")\n          )\n      )\n\n      val request2 = reqBuilder.POST(BodyPublishers.ofString(\"request2\")).build()\n      val response2 = client.send(request2, BodyHandlers.ofString())\n      response2.body() shouldBe \"response2\"\n\n      val request1 = reqBuilder.POST(BodyPublishers.ofString(\"request1\")).build()\n      val response1 = client.send(request1, BodyHandlers.ofString())\n      response1.body() shouldBe \"response1\"\n    }\n\n    test(\"Multi thread stubbing\") {\n\n      (1..20)\n        .map { i ->\n          async {\n            wireMock.stubFor(\n              post(\"/$url\")\n                .withRequestBody(equalTo(\"request$i\"))\n                .willReturn(\n                  aResponse()\n                    .withBody(\"response$i\")\n                )\n            )\n          }\n        }.awaitAll()\n\n      (1..20)\n        .map { i ->\n          async {\n            val request = reqBuilder.POST(BodyPublishers.ofString(\"request$i\")).build()\n            val response = client.send(request, BodyHandlers.ofString())\n            response.body() shouldBe \"response$i\"\n          }\n        }.awaitAll()\n    }\n\n    context(\"Response Headers\") {\n      val reqBuilder = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL/headers\"))\n        .header(\"Content-Type\", \"application/json\")\n\n      val headers = mapOf(\"CustomHeaderKey\" to \"CustomHeaderValue\")\n\n      test(\"Stub get response with header\") {\n        val response = TestDto(\"get\")\n        stove {\n          wiremock {\n            mockGet(\"/headers\", statusCode = 200, responseBody = response.some(), responseHeaders = headers)\n          }\n        }\n\n        val request = reqBuilder.GET().build()\n        val httpResponse = client.send(request, BodyHandlers.ofString())\n        httpResponse.body() shouldBe \"{\\\"name\\\":\\\"get\\\"}\"\n        httpResponse.headers().firstValue(\"CustomHeaderKey\").get() shouldBe \"CustomHeaderValue\"\n      }\n\n      test(\"Stub post response with header\") {\n        val response = TestDto(\"post\")\n        stove {\n          wiremock {\n            mockPost(\"/headers\", statusCode = 200, responseBody = response.some(), responseHeaders = headers)\n          }\n        }\n\n        val request = reqBuilder.POST(BodyPublishers.ofString(\"post-response-with-header\")).build()\n        val httpResponse = client.send(request, BodyHandlers.ofString())\n        httpResponse.body() shouldBe \"{\\\"name\\\":\\\"post\\\"}\"\n        httpResponse.headers().firstValue(\"CustomHeaderKey\").get() shouldBe \"CustomHeaderValue\"\n      }\n\n      test(\"Stub put response with header\") {\n        val response = TestDto(\"put\")\n        stove {\n          wiremock {\n            mockPut(\"/headers\", statusCode = 200, responseBody = response.some(), responseHeaders = headers)\n          }\n        }\n\n        val request = reqBuilder.PUT(BodyPublishers.ofString(\"put-response-with-header\")).build()\n        val httpResponse = client.send(request, BodyHandlers.ofString())\n        httpResponse.body() shouldBe \"{\\\"name\\\":\\\"put\\\"}\"\n        httpResponse.headers().firstValue(\"CustomHeaderKey\").get() shouldBe \"CustomHeaderValue\"\n      }\n\n      test(\"Stub patch response with header\") {\n        val response = TestDto(\"patch\")\n        stove {\n          wiremock {\n            mockPatch(\"/headers\", statusCode = 200, responseBody = response.some(), responseHeaders = headers)\n          }\n        }\n\n        val request = reqBuilder.method(\"PATCH\", BodyPublishers.ofString(\"patch-response-with-header\")).build()\n        val httpResponse = client.send(request, BodyHandlers.ofString())\n        httpResponse.body() shouldBe \"{\\\"name\\\":\\\"patch\\\"}\"\n        httpResponse.headers().firstValue(\"CustomHeaderKey\").get() shouldBe \"CustomHeaderValue\"\n      }\n    }\n  })\n\ndata class TestDto(\n  val name: String\n)\n"
  },
  {
    "path": "lib/stove-wiremock/src/test/kotlin/com/trendyol/stove/wiremock/WireMockVerificationTest.kt",
    "content": "package com.trendyol.stove.wiremock\n\nimport arrow.core.some\nimport com.github.tomakehurst.wiremock.client.WireMock.*\nimport com.github.tomakehurst.wiremock.http.RequestMethod\nimport com.trendyol.stove.reporting.SystemSnapshot\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.tracing.TraceContext\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport java.net.URI\nimport java.net.http.HttpClient\nimport java.net.http.HttpRequest\nimport java.net.http.HttpRequest.BodyPublishers\nimport java.net.http.HttpResponse.BodyHandlers\n\nclass WireMockVerificationTest :\n  FunSpec({\n    val client = HttpClient.newBuilder().build()\n\n    fun request(\n      url: String,\n      body: String = \"{}\",\n      headers: Map<String, String> = emptyMap()\n    ): HttpRequest {\n      val builder = HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .header(\"Content-Type\", \"application/json\")\n        .POST(BodyPublishers.ofString(body))\n\n      headers.forEach { (key, value) -> builder.header(key, value) }\n      return builder.build()\n    }\n\n    fun get(url: String): HttpRequest =\n      HttpRequest\n        .newBuilder(URI(\"$WIREMOCK_BASE_URL$url\"))\n        .GET()\n        .build()\n\n    @Suppress(\"UNCHECKED_CAST\")\n    fun SystemSnapshot.listState(name: String): List<Map<String, Any>> =\n      state[name] as List<Map<String, Any>>\n\n    test(\"shouldHaveBeenCalled should pass for exact request body after stub removal\") {\n      val url = \"/verification/exact-body\"\n      val body = mapOf(\"orderId\" to \"order-1\")\n\n      stove {\n        wiremock {\n          mockPost(url = url, statusCode = 200, requestBody = body.some())\n        }\n      }\n\n      client.send(request(url, \"\"\"{\"orderId\":\"order-1\"}\"\"\"), BodyHandlers.ofString()).statusCode() shouldBe 200\n\n      stove {\n        wiremock {\n          shouldHaveBeenCalled(\n            method = RequestMethod.POST,\n            url = url,\n            requestBody = body.some()\n          )\n        }\n      }\n    }\n\n    test(\"shouldHaveBeenCalled should pass for partial nested request body\") {\n      val url = \"/verification/partial-body\"\n\n      stove {\n        wiremock {\n          mockPost(url = url, statusCode = 200)\n        }\n      }\n\n      val body = \"\"\"{\"order\":{\"customer\":{\"id\":\"customer-1\",\"name\":\"Ada\"}},\"ignored\":true}\"\"\"\n      client.send(request(url, body), BodyHandlers.ofString()).statusCode() shouldBe 200\n\n      stove {\n        wiremock {\n          shouldHaveBeenCalled(\n            method = RequestMethod.POST,\n            url = url,\n            requestContaining = mapOf(\"order.customer.id\" to \"customer-1\")\n          )\n        }\n      }\n    }\n\n    test(\"shouldHaveBeenCalled should pass for headers query params and url pattern\") {\n      val url = \"/verification/query\"\n\n      stove {\n        wiremock {\n          mockPostConfigure(url = url, urlPatternFn = { urlPathEqualTo(it) }) { req, _ ->\n            req\n              .withQueryParam(\"page\", equalTo(\"1\"))\n              .willReturn(aResponse().withStatus(200))\n          }\n        }\n      }\n\n      client\n        .send(\n          request(\"$url?page=1\", headers = mapOf(\"X-Request-Id\" to \"req-1\")),\n          BodyHandlers.ofString()\n        ).statusCode() shouldBe 200\n\n      stove {\n        wiremock {\n          shouldHaveBeenCalled(\n            method = RequestMethod.POST,\n            url = url,\n            headers = mapOf(\"X-Request-Id\" to \"req-1\"),\n            queryParams = mapOf(\"page\" to \"1\"),\n            urlPatternFn = { urlPathEqualTo(it) }\n          )\n        }\n      }\n    }\n\n    test(\"shouldHaveBeenCalled should pass with advanced WireMock request pattern\") {\n      val url = \"/verification/advanced\"\n\n      stove {\n        wiremock {\n          mockPost(url = url, statusCode = 200)\n        }\n      }\n\n      client.send(request(url, headers = mapOf(\"X-Mode\" to \"advanced\")), BodyHandlers.ofString()).statusCode() shouldBe 200\n\n      stove {\n        wiremock {\n          shouldHaveBeenCalled {\n            postRequestedFor(urlEqualTo(url))\n              .withHeader(\"X-Mode\", equalTo(\"advanced\"))\n          }\n        }\n      }\n    }\n\n    test(\"shouldHaveBeenCalled should fail when no matching call exists\") {\n      val error = shouldThrow<AssertionError> {\n        stove {\n          wiremock {\n            shouldHaveBeenCalled(method = RequestMethod.GET, url = \"/verification/not-called\")\n          }\n        }\n      }\n\n      error.message shouldContain \"Expected exactly 1 requests\"\n    }\n\n    test(\"shouldHaveBeenCalled should fail on duplicate calls by default\") {\n      val url = \"/verification/duplicate\"\n\n      repeat(2) {\n        stove {\n          wiremock {\n            mockGet(url = url, statusCode = 200)\n          }\n        }\n        client.send(get(url), BodyHandlers.ofString()).statusCode() shouldBe 200\n      }\n\n      val error = shouldThrow<AssertionError> {\n        stove {\n          wiremock {\n            shouldHaveBeenCalled(method = RequestMethod.GET, url = url)\n          }\n        }\n      }\n\n      error.message shouldContain \"received 2\"\n    }\n\n    test(\"shouldNotHaveBeenCalled should pass for zero calls and fail for matching calls\") {\n      val url = \"/verification/not-called-negative\"\n      val calledUrl = \"/verification/called-negative\"\n\n      stove {\n        wiremock {\n          mockGet(url = calledUrl, statusCode = 200)\n          shouldNotHaveBeenCalled(method = RequestMethod.GET, url = url)\n        }\n      }\n\n      client.send(get(calledUrl), BodyHandlers.ofString()).statusCode() shouldBe 200\n\n      val error = shouldThrow<AssertionError> {\n        stove {\n          wiremock {\n            shouldNotHaveBeenCalled(method = RequestMethod.GET, url = calledUrl)\n          }\n        }\n      }\n\n      error.message shouldContain \"Expected exactly 0 requests\"\n    }\n\n    test(\"callsFor should return only matching current test requests\") {\n      val targetUrl = \"/verification/calls-for-target\"\n      val otherUrl = \"/verification/calls-for-other\"\n\n      stove {\n        wiremock {\n          mockPost(url = targetUrl, statusCode = 200)\n          mockPost(url = otherUrl, statusCode = 200)\n        }\n      }\n\n      client.send(request(targetUrl, \"\"\"{\"type\":\"target\"}\"\"\"), BodyHandlers.ofString()).statusCode() shouldBe 200\n      client.send(request(otherUrl, \"\"\"{\"type\":\"other\"}\"\"\"), BodyHandlers.ofString()).statusCode() shouldBe 200\n\n      stove {\n        wiremock {\n          callsFor(method = RequestMethod.POST, url = targetUrl) shouldHaveSize 1\n        }\n      }\n    }\n\n    test(\"shouldHaveBeenCalled should pass with a custom count strategy\") {\n      val url = \"/verification/custom-count\"\n\n      repeat(2) {\n        stove {\n          wiremock {\n            mockGet(url = url, statusCode = 200)\n          }\n        }\n        client.send(get(url), BodyHandlers.ofString()).statusCode() shouldBe 200\n      }\n\n      stove {\n        wiremock {\n          shouldHaveBeenCalled(\n            method = RequestMethod.GET,\n            url = url,\n            count = moreThanOrExactly(2)\n          )\n        }\n      }\n    }\n\n    test(\"snapshot should include registered received and served state after stub removal\") {\n      val url = \"/verification/snapshot-served\"\n\n      stove {\n        wiremock {\n          mockPost(\n            url = url,\n            statusCode = 201,\n            responseBody = mapOf(\"created\" to true).some(),\n            responseHeaders = mapOf(\"X-Served-By\" to \"stove\")\n          )\n        }\n      }\n\n      client.send(request(url, \"\"\"{\"orderId\":\"snapshot-1\"}\"\"\"), BodyHandlers.ofString()).statusCode() shouldBe 201\n\n      lateinit var snapshot: SystemSnapshot\n      stove {\n        wiremock {\n          snapshot = snapshot()\n        }\n      }\n\n      val registeredStubs = snapshot.listState(\"registeredStubs\")\n      registeredStubs shouldHaveSize 1\n      registeredStubs.first()[\"active\"] shouldBe false\n      registeredStubs.first()[\"method\"] shouldBe \"POST\"\n      registeredStubs.first()[\"url\"] shouldBe url\n      registeredStubs.first()[\"status\"] shouldBe 201\n\n      val activeStubs = snapshot.listState(\"activeStubs\")\n      activeStubs shouldHaveSize 0\n\n      val receivedRequests = snapshot.listState(\"receivedRequests\")\n      receivedRequests shouldHaveSize 1\n      receivedRequests.first()[\"method\"] shouldBe \"POST\"\n      receivedRequests.first()[\"url\"] shouldBe url\n      receivedRequests.first()[\"body\"].toString() shouldContain \"snapshot-1\"\n\n      val servedRequests = snapshot.listState(\"servedRequests\")\n      servedRequests shouldHaveSize 1\n      val servedResponse = servedRequests.first()[\"response\"] as Map<*, *>\n      servedResponse[\"status\"] shouldBe 201\n      servedResponse[\"body\"].toString() shouldContain \"created\"\n\n      snapshot.summary shouldContain \"Registered stubs (this test): 1 (active: 0)\"\n      snapshot.summary shouldContain \"Received requests (this test): 1\"\n      snapshot.summary shouldContain \"Served requests (this test): 1 (matched: 1)\"\n    }\n\n    test(\"snapshot should include unmatched requests scoped by Stove test header\") {\n      val testId = Stove.reporter().currentTestId()\n      val url = \"/verification/snapshot-unmatched\"\n\n      client\n        .send(\n          request(url, headers = mapOf(TraceContext.STOVE_TEST_ID_HEADER to testId)),\n          BodyHandlers.ofString()\n        ).statusCode() shouldBe 404\n\n      lateinit var snapshot: SystemSnapshot\n      stove {\n        wiremock {\n          snapshot = snapshot()\n        }\n      }\n\n      val unmatchedRequests = snapshot.listState(\"unmatchedRequests\")\n      unmatchedRequests shouldHaveSize 1\n      unmatchedRequests.first()[\"matched\"] shouldBe false\n      unmatchedRequests.first()[\"method\"] shouldBe \"POST\"\n      unmatchedRequests.first()[\"url\"] shouldBe url\n\n      val servedRequests = snapshot.listState(\"servedRequests\")\n      servedRequests shouldHaveSize 1\n      val servedResponse = servedRequests.first()[\"response\"] as Map<*, *>\n      servedResponse[\"status\"] shouldBe 404\n\n      snapshot.summary shouldContain \"Unmatched requests: 1\"\n    }\n  })\n"
  },
  {
    "path": "lib/stove-wiremock/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.wiremock.StoveConfig\n"
  },
  {
    "path": "lint.sh",
    "content": "#!/bin/sh\n#\n# Lint & format all projects in the Stove monorepo.\n#\n#   ./lint.sh --check    Check only (git hooks, CI)\n#   ./lint.sh --format   Auto-fix everything\n#   ./lint.sh            Same as --check\n#\n# Pass project names to scope the run (default: all changed projects, or all if --all):\n#\n#   ./lint.sh --format jvm spa\n#   ./lint.sh --check rust recipes\n#   ./lint.sh --format --all\n#\n# Projects: jvm, rust, spa, recipes, go\n\nset -e\n\n# Ensure cargo is in PATH (not always inherited by subshells)\nif [ -d \"$HOME/.cargo/bin\" ]; then\n  export PATH=\"$HOME/.cargo/bin:$PATH\"\nfi\n\nREPO_ROOT=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nCLI_DIR=\"$REPO_ROOT/tools/stove-cli\"\nSPA_DIR=\"$CLI_DIR/spa\"\nRECIPES_DIR=\"$REPO_ROOT/recipes/jvm\"\n\n# ── Parse args ────────────────────────────────────────────────────────\n\nMODE=\"check\"\nRUN_ALL=false\nPROJECTS=\"\"\n\nfor arg in \"$@\"; do\n  case \"$arg\" in\n    --check)  MODE=\"check\" ;;\n    --format) MODE=\"format\" ;;\n    --all)    RUN_ALL=true ;;\n    jvm|rust|spa|recipes|go) PROJECTS=\"$PROJECTS $arg\" ;;\n    *)\n      echo \"Usage: $0 [--check|--format] [--all] [jvm] [rust] [spa] [recipes]\"\n      exit 1\n      ;;\n  esac\ndone\n\n# ── Detect changed projects when no explicit selection ────────────────\n\ndetect_changed() {\n  # In a git hook context, use cached diff; otherwise use working tree diff\n  if git diff --cached --name-only 2>/dev/null | grep -q .; then\n    DIFF_CMD=\"git diff --cached --name-only\"\n  else\n    DIFF_CMD=\"git diff --name-only HEAD\"\n  fi\n\n  CHANGED=$($DIFF_CMD 2>/dev/null || true)\n\n  if echo \"$CHANGED\" | grep -qE '\\.(kt|kts|java)$'; then\n    PROJECTS=\"$PROJECTS jvm\"\n  fi\n  if echo \"$CHANGED\" | grep -qE \"^tools/stove-cli/.*\\.(rs|toml)$\"; then\n    PROJECTS=\"$PROJECTS rust\"\n  fi\n  if echo \"$CHANGED\" | grep -qE \"^tools/stove-cli/spa/src/.*\\.(ts|tsx|js|jsx|css)$\"; then\n    PROJECTS=\"$PROJECTS spa\"\n  fi\n  if echo \"$CHANGED\" | grep -qE \"^recipes/\"; then\n    PROJECTS=\"$PROJECTS recipes\"\n  fi\n  if echo \"$CHANGED\" | grep -qE '\\.go$'; then\n    PROJECTS=\"$PROJECTS go\"\n  fi\n}\n\nif [ -z \"$PROJECTS\" ]; then\n  if [ \"$RUN_ALL\" = true ]; then\n    PROJECTS=\"jvm rust spa recipes go\"\n  else\n    detect_changed\n    if [ -z \"$PROJECTS\" ]; then\n      echo \"No changes detected. Use --all to lint everything.\"\n      exit 0\n    fi\n  fi\nfi\n\n# ── Helpers ───────────────────────────────────────────────────────────\n\nEXIT_CODE=0\n\nrun() {\n  echo \"  \\$ $*\"\n  if ! \"$@\"; then\n    EXIT_CODE=1\n  fi\n}\n\nsection() {\n  echo \"\"\n  echo \"── $1 ──\"\n}\n\n# ── JVM (Kotlin / Java) ──────────────────────────────────────────────\n\nlint_jvm() {\n  section \"JVM (Kotlin / Java)\"\n  if [ \"$MODE\" = \"format\" ]; then\n    run \"$REPO_ROOT/gradlew\" -p \"$REPO_ROOT\" --no-daemon spotlessApply detekt apiDump\n  else\n    run \"$REPO_ROOT/gradlew\" -p \"$REPO_ROOT\" --no-daemon spotlessCheck detekt apiCheck\n  fi\n}\n\n# ── Rust ──────────────────────────────────────────────────────────────\n\nlint_rust() {\n  section \"Rust\"\n  if [ \"$MODE\" = \"format\" ]; then\n    (cd \"$CLI_DIR\" && run cargo fmt)\n  else\n    (cd \"$CLI_DIR\" && run cargo fmt -- --check)\n  fi\n  (cd \"$CLI_DIR\" && SKIP_SPA_BUILD=1 run cargo clippy -- -D warnings)\n}\n\n# ── SPA (TypeScript / React) ─────────────────────────────────────────\n\nlint_spa() {\n  section \"SPA (TypeScript / React)\"\n  if [ ! -d \"$SPA_DIR/node_modules\" ]; then\n    (cd \"$SPA_DIR\" && run npm install)\n  fi\n  if [ \"$MODE\" = \"format\" ]; then\n    (cd \"$SPA_DIR\" && run npx biome check --write src)\n  else\n    (cd \"$SPA_DIR\" && run npx tsc -b)\n    (cd \"$SPA_DIR\" && run npx biome check src)\n  fi\n}\n\n# ── Go ───────────────────────────────────────────────────────────────\n\nlint_go() {\n  section \"Go\"\n  GO_DIRS=\"$REPO_ROOT/go/stove-kafka $REPO_ROOT/recipes/process/golang/go-showcase\"\n  for dir in $GO_DIRS; do\n    if [ -d \"$dir\" ]; then\n      if [ \"$MODE\" = \"format\" ]; then\n        run gofmt -w \"$dir\"\n      else\n        if [ -n \"$(gofmt -l \"$dir\")\" ]; then\n          echo \"gofmt: files need formatting in $dir:\"\n          gofmt -l \"$dir\"\n          EXIT_CODE=1\n        fi\n      fi\n      (cd \"$dir\" && run go vet ./...)\n    fi\n  done\n}\n\n# ── Recipes (Kotlin / Java / Scala) ──────────────────────────────────\n\nlint_recipes() {\n  section \"Recipes (Kotlin / Java / Scala)\"\n  if [ \"$MODE\" = \"format\" ]; then\n    run \"$REPO_ROOT/gradlew\" -p \"$RECIPES_DIR\" --no-daemon spotlessApply\n  else\n    run \"$REPO_ROOT/gradlew\" -p \"$RECIPES_DIR\" --no-daemon spotlessCheck\n  fi\n}\n\n# ── Run selected projects concurrently ────────────────────────────────\n\necho \"Mode: $MODE\"\n\nPIDS=\"\"\nfor proj in $PROJECTS; do\n  (\n    case \"$proj\" in\n      jvm)     lint_jvm ;;\n      rust)    lint_rust ;;\n      spa)     lint_spa ;;\n      recipes) lint_recipes ;;\n      go)      lint_go ;;\n    esac\n    exit $EXIT_CODE\n  ) &\n  PIDS=\"$PIDS $!\"\ndone\n\nEXIT_CODE=0\nfor pid in $PIDS; do\n  if ! wait \"$pid\"; then\n    EXIT_CODE=1\n  fi\ndone\n\necho \"\"\nif [ $EXIT_CODE -ne 0 ]; then\n  echo \"Some checks failed. Run './lint.sh --format' to auto-fix.\"\n  exit 1\nelse\n  echo \"All checks passed.\"\nfi\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: \"Stove\"\nsite_description: \"End-to-end testing framework for the JVM\"\nrepo_url: https://github.com/trendyol/stove\nrepo_name: trendyol/stove\nedit_uri: edit/main/docs/\nextra_css:\n  - css/custom.css\nextra_javascript:\n  - assets/rough-notation.iife.js\n  - js/rough-notation-mkdocs.js\nplugins:\n  - search\n  - awesome-pages:\n     collapse_single_pages: true\nnav:\n  - Home: index.md\n  - Getting Started: getting-started.md\n  - Supported Frameworks:\n    - Overview: frameworks/index.md\n    - Spring Boot: frameworks/spring-boot.md\n    - Ktor: frameworks/ktor.md\n    - Micronaut: frameworks/micronaut.md\n    - Quarkus: frameworks/quarkus.md\n  - Other Languages & Stacks:\n    - Overview: other-languages/index.md\n    - Go:\n      - Overview: other-languages/go.md\n      - Process Mode: other-languages/go-process.md\n      - Container Mode: other-languages/go-container.md\n  - Components:\n    - Overview: Components/index.md\n    - Couchbase: Components/01-couchbase.md\n    - Kafka: Components/02-kafka.md\n    - Elasticsearch: Components/03-elasticsearch.md\n    - WireMock: Components/04-wiremock.md\n    - HTTP Client: Components/05-http.md\n    - PostgreSQL: Components/06-postgresql.md\n    - MongoDB: Components/07-mongodb.md\n    - MSSQL: Components/08-mssql.md\n    - Redis: Components/09-redis.md\n    - Bridge: Components/10-bridge.md\n    - Provided Instances: Components/11-provided-instances.md\n    - gRPC: Components/12-grpc.md\n    - Reporting: Components/13-reporting.md\n    - gRPC Mocking: Components/14-grpc-mock.md\n    - Tracing: Components/15-tracing.md\n    - MySQL: Components/16-mysql.md\n    - Cassandra: Components/17-cassandra.md\n    - Dashboard: Components/18-dashboard.md\n    - MCP: Components/21-mcp.md\n    - Container AUT: Components/22-container.md\n    - Provided Application: Components/19-provided-application.md\n    - Multiple Systems: Components/20-multiple-systems.md\n  - Writing Custom Systems: writing-custom-systems.md\n  - Best Practices: best-practices.md\n  - Troubleshooting: troubleshooting.md\n  - Blog:\n      - \"Polyglot Stove in 0.24.0\": blog/polyglot-0.24.0.md\n      - \"Stove Dashboard in 0.23.0\": blog/dashboard-0.23.0.md\n      - \"Execution Tracing in 0.21.0\": blog/tracing-0.21.0.md\n  - Release Notes:\n      - \"0.24.0\": release-notes/0.24.0.md\n      - \"0.23.0\": release-notes/0.23.0.md\n      - 0.22.2: release-notes/0.22.2.md\n      - 0.21.2: release-notes/0.21.2.md\n      - 0.21.0: release-notes/0.21.0.md\n      - 0.20.0: release-notes/0.20.0.md\n      - 0.19.0: release-notes/0.19.0.md\n      - 0.15.0: release-notes/0.15.0.md\ntheme:\n  name: material\n  logo: assets/logo.png\n  favicon: assets/logo.png\n  features:\n    # Navigation\n    - navigation.instant\n    - navigation.instant.progress\n    - navigation.tracking\n    - navigation.tabs\n    - navigation.sections\n    - navigation.indexes\n    - navigation.top\n    - navigation.footer\n    - navigation.path\n    # Search\n    - search.highlight\n    - search.share\n    - search.suggest\n    # Content\n    - content.code.copy\n    - content.code.annotate\n    - content.tabs.link\n    - content.tooltips\n    # Header\n    - header.autohide\n    # Table of contents\n    - toc.follow\n\n  palette:\n    - scheme: default\n      primary: teal\n      accent: amber\n      toggle:\n        icon: material/weather-sunny\n        name: Switch to dark mode\n    - scheme: slate\n      primary: teal\n      accent: amber\n      toggle:\n        icon: material/weather-night\n        name: Switch to light mode\n  font:\n    text: Inter\n    code: JetBrains Mono\n  icon:\n    repo: fontawesome/brands/github\n\nmarkdown_extensions:\n  - admonition\n  - pymdownx.details\n  - pymdownx.highlight:\n      anchor_linenums: true\n      line_spans: __span\n      pygments_lang_class: true\n  - pymdownx.inlinehilite\n  - pymdownx.snippets\n  - pymdownx.superfences:\n      custom_fences:\n        - name: mermaid\n          class: mermaid\n          format: !!python/name:pymdownx.superfences.fence_code_format\n  - pymdownx.tabbed:\n      alternate_style: true\n      slugify: !!python/object/apply:pymdownx.slugs.slugify\n        kwds:\n          case: lower\n  - pymdownx.emoji:\n      emoji_index: !!python/name:material.extensions.emoji.twemoji\n      emoji_generator: !!python/name:material.extensions.emoji.to_svg\n  - tables\n  - attr_list\n  - md_in_html\n  - def_list\n  - toc:\n      permalink: true\n\nextra:\n  social:\n    - icon: fontawesome/brands/github\n      link: https://github.com/trendyol/stove\n    - icon: fontawesome/regular/building\n      link: https://trendyol.github.io/\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/build.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nplugins {\n  `kotlin-dsl`\n  `java-gradle-plugin`\n  alias(libs.plugins.maven.publish)\n}\n\ngradlePlugin {\n  plugins {\n    create(\"stoveTracing\") {\n      id = \"com.trendyol.stove.tracing\"\n      implementationClass = \"com.trendyol.stove.gradle.StoveTracingPlugin\"\n    }\n  }\n}\n\ntasks.test {\n  useJUnitPlatform()\n}\n\ndependencies {\n  testImplementation(libs.kotest.runner.junit5)\n  testImplementation(libs.kotest.framework.engine)\n  testImplementation(libs.kotest.assertions.core)\n}\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/gradle/libs.versions.toml",
    "content": "[versions]\nkotest = \"6.1.11\"\n\n[libraries]\nkotest-runner-junit5 = { module = \"io.kotest:kotest-runner-junit5\", version.ref = \"kotest\" }\nkotest-framework-engine = { module = \"io.kotest:kotest-framework-engine\", version.ref = \"kotest\" }\nkotest-assertions-core = { module = \"io.kotest:kotest-assertions-core\", version.ref = \"kotest\" }\n\n[plugins]\nmaven-publish = { id = \"com.vanniktech.maven.publish\", version = \"0.36.0\" }\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/gradle.properties",
    "content": "projectDescription=Gradle plugin that configures OpenTelemetry Java Agent for Stove test tracing\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/settings.gradle.kts",
    "content": "rootProject.name = \"stove-tracing-gradle-plugin\"\n\ndependencyResolutionManagement {\n  repositories {\n    mavenCentral()\n    gradlePluginPortal()\n  }\n}\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/src/main/kotlin/com/trendyol/stove/gradle/StoveTracingExtension.kt",
    "content": "package com.trendyol.stove.gradle\n\nimport com.trendyol.stove.gradle.internal.TracingDefaults\nimport org.gradle.api.model.ObjectFactory\nimport org.gradle.api.provider.ListProperty\nimport org.gradle.api.provider.Property\nimport javax.inject.Inject\n\n/**\n * Configuration DSL for the Stove Tracing Gradle plugin.\n *\n * Example usage in build.gradle.kts:\n * ```kotlin\n * stoveTracing {\n *     serviceName.set(\"my-service\")\n *     testTaskNames.set(listOf(\"integrationTest\"))\n * }\n * ```\n */\nabstract class StoveTracingExtension @Inject constructor(objects: ObjectFactory) {\n\n  /** The service name to use in traces. This should match your application's service name. */\n  val serviceName: Property<String> = objects.property(String::class.java)\n    .convention(TracingDefaults.DEFAULT_SERVICE_NAME)\n\n  /** Whether tracing is enabled. Set false to disable tracing without removing configuration. */\n  val enabled: Property<Boolean> = objects.property(Boolean::class.java)\n    .convention(true)\n\n  /**\n   * The OTLP protocol to use.\n   * Currently only \"grpc\" is supported.\n   */\n  val protocol: Property<String> = objects.property(String::class.java)\n    .convention(TracingDefaults.DEFAULT_PROTOCOL)\n\n  /** The batch span processor schedule delay in milliseconds. Lower = faster export. */\n  val bspScheduleDelay: Property<Int> = objects.property(Int::class.java)\n    .convention(TracingDefaults.DEFAULT_BSP_SCHEDULE_DELAY)\n\n  /** The maximum batch size for span export. 1 = immediate export per span. */\n  val bspMaxBatchSize: Property<Int> = objects.property(Int::class.java)\n    .convention(TracingDefaults.DEFAULT_BSP_MAX_BATCH_SIZE)\n\n  /** Whether to capture HTTP headers in spans. */\n  val captureHttpHeaders: Property<Boolean> = objects.property(Boolean::class.java)\n    .convention(true)\n\n  /** Whether to enable experimental HTTP telemetry features. */\n  val captureExperimentalTelemetry: Property<Boolean> = objects.property(Boolean::class.java)\n    .convention(true)\n\n  /** List of instrumentation modules to disable. Example: listOf(\"jdbc\", \"hibernate\") */\n  val disabledInstrumentations: ListProperty<String> = objects.listProperty(String::class.java)\n    .convention(emptyList())\n\n  /** List of additional instrumentation modules to enable. */\n  val additionalInstrumentations: ListProperty<String> = objects.listProperty(String::class.java)\n    .convention(emptyList())\n\n  /** List of custom annotation class names to instrument. */\n  val customAnnotations: ListProperty<String> = objects.listProperty(String::class.java)\n    .convention(emptyList())\n\n  /** The OpenTelemetry Java Agent version to use. */\n  val otelAgentVersion: Property<String> = objects.property(String::class.java)\n    .convention(TracingDefaults.DEFAULT_OTEL_AGENT_VERSION)\n\n  /**\n   * List of test task names to configure. If empty, applies to all test tasks.\n   * Example: listOf(\"integrationTest\") to only apply to the integrationTest task.\n   */\n  val testTaskNames: ListProperty<String> = objects.listProperty(String::class.java)\n    .convention(emptyList())\n}\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/src/main/kotlin/com/trendyol/stove/gradle/StoveTracingPlugin.kt",
    "content": "package com.trendyol.stove.gradle\n\nimport com.trendyol.stove.gradle.internal.TestTaskConfigurator\nimport org.gradle.api.Plugin\nimport org.gradle.api.Project\n\n/**\n * Gradle plugin that configures the OpenTelemetry Java Agent for Stove test tracing.\n *\n * When a test fails, Stove can display the execution trace showing exactly\n * what happened during the test -- HTTP calls, Kafka messages, database queries, etc.\n *\n * Usage in build.gradle.kts:\n * ```kotlin\n * plugins {\n *     id(\"com.trendyol.stove.tracing\")\n * }\n *\n * stoveTracing {\n *     serviceName.set(\"my-service\")\n * }\n * ```\n */\nclass StoveTracingPlugin : Plugin<Project> {\n\n  override fun apply(project: Project) {\n    val extension = project.extensions.create(\"stoveTracing\", StoveTracingExtension::class.java)\n    TestTaskConfigurator.configure(project, extension)\n  }\n}\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/src/main/kotlin/com/trendyol/stove/gradle/internal/JvmArgsBuilder.kt",
    "content": "package com.trendyol.stove.gradle.internal\n\n/**\n * Serializable snapshot of tracing config for Gradle configuration cache compatibility.\n * All objects captured in task actions must be serializable.\n */\ninternal data class ResolvedTracingConfig(\n  val protocol: String,\n  val serviceName: String,\n  val bspScheduleDelay: Int,\n  val bspMaxBatchSize: Int,\n  val captureHttpHeaders: Boolean,\n  val captureExperimentalTelemetry: Boolean,\n  val customAnnotations: List<String>,\n  val disabledInstrumentations: List<String>,\n  val additionalInstrumentations: List<String>,\n) : java.io.Serializable {\n  companion object {\n    private const val serialVersionUID: Long = 1L\n  }\n}\n\ninternal object JvmArgsBuilder {\n\n  fun build(agentPath: String, config: ResolvedTracingConfig, port: Int): List<String> = buildList {\n    add(\"-javaagent:$agentPath\")\n    addAll(coreExportArgs(config, port))\n    add(\"-Dotel.propagators=tracecontext,baggage\")\n    addAll(testOptimizationArgs(config))\n\n    if (config.captureHttpHeaders) {\n      addAll(httpHeaderCaptureArgs())\n    }\n    if (config.captureExperimentalTelemetry) {\n      addAll(experimentalTelemetryArgs())\n    }\n    if (config.customAnnotations.isNotEmpty()) {\n      add(\"-Dotel.instrumentation.annotations.methods=${config.customAnnotations.joinToString(\",\")}\")\n    }\n    addAll(instrumentationControlArgs(config))\n  }\n\n  private fun coreExportArgs(config: ResolvedTracingConfig, port: Int): List<String> = buildList {\n    val endpoint = \"http://localhost:$port\"\n    add(\"-Dotel.traces.exporter=otlp\")\n    add(\"-Dotel.exporter.otlp.protocol=${config.protocol}\")\n    add(\"-Dotel.exporter.otlp.endpoint=$endpoint\")\n    add(\"-Dotel.metrics.exporter=none\")\n    add(\"-Dotel.logs.exporter=none\")\n    add(\"-Dotel.service.name=${config.serviceName}\")\n    add(\"-Dotel.resource.attributes=service.name=${config.serviceName},deployment.environment=test\")\n\n    if (config.protocol == \"grpc\") {\n      add(\"-Dotel.instrumentation.grpc.enabled=false\")\n    }\n  }\n\n  private fun testOptimizationArgs(config: ResolvedTracingConfig): List<String> = listOf(\n    \"-Dotel.traces.sampler=always_on\",\n    \"-Dotel.bsp.schedule.delay=${config.bspScheduleDelay}\",\n    \"-Dotel.bsp.max.export.batch.size=${config.bspMaxBatchSize}\",\n  )\n\n  private fun httpHeaderCaptureArgs(): List<String> = listOf(\n    \"-Dotel.instrumentation.http.client.capture-request-headers=content-type,accept,x-stove-test-id\",\n    \"-Dotel.instrumentation.http.client.capture-response-headers=content-type\",\n    \"-Dotel.instrumentation.http.server.capture-request-headers=content-type,accept,user-agent,x-stove-test-id\",\n    \"-Dotel.instrumentation.http.server.capture-response-headers=content-type\",\n  )\n\n  private fun experimentalTelemetryArgs(): List<String> = listOf(\n    \"-Dotel.instrumentation.http.client.emit-experimental-telemetry=true\",\n    \"-Dotel.instrumentation.http.server.emit-experimental-telemetry=true\",\n    \"-Dotel.instrumentation.servlet.experimental.capture-request-parameters=*\",\n  )\n\n  private fun instrumentationControlArgs(config: ResolvedTracingConfig): List<String> = buildList {\n    if (config.disabledInstrumentations.isNotEmpty()) {\n      add(\"-Dotel.instrumentation.common.default-enabled=true\")\n      addAll(config.disabledInstrumentations.map { \"-Dotel.instrumentation.$it.enabled=false\" })\n    }\n    addAll(config.additionalInstrumentations.map { \"-Dotel.instrumentation.$it.enabled=true\" })\n  }\n}\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/src/main/kotlin/com/trendyol/stove/gradle/internal/TestTaskConfigurator.kt",
    "content": "package com.trendyol.stove.gradle.internal\n\nimport com.trendyol.stove.gradle.StoveTracingExtension\nimport org.gradle.api.Project\nimport org.gradle.api.artifacts.Configuration\nimport org.gradle.api.tasks.testing.Test\nimport java.net.ServerSocket\n\ninternal object TestTaskConfigurator {\n\n  fun configure(project: Project, extension: StoveTracingExtension) {\n    val otelAgentConfig = project.configurations.create(\"otelAgent\") {\n      isTransitive = false\n      isCanBeResolved = true\n      isCanBeConsumed = false\n      description = \"OpenTelemetry Java Agent for Stove test tracing\"\n    }\n\n    project.afterEvaluate {\n      if (!extension.enabled.get()) {\n        logger.info(\"Stove tracing is disabled, skipping configuration\")\n        return@afterEvaluate\n      }\n\n      validateProtocol(extension.protocol.get())\n\n      dependencies.add(\n        \"otelAgent\",\n        \"io.opentelemetry.javaagent:opentelemetry-javaagent:${extension.otelAgentVersion.get()}\"\n      )\n\n      val testTasks = resolveTestTasks(extension)\n      testTasks.forEach { testTask ->\n        configureTestTask(testTask, otelAgentConfig, extension)\n      }\n\n      logConfiguration(extension, testTasks)\n    }\n  }\n\n  private fun validateProtocol(protocol: String) {\n    require(protocol == TracingDefaults.SUPPORTED_PROTOCOL) {\n      \"Unsupported OTLP protocol '$protocol'. Stove tracing receiver currently supports only \" +\n        \"'${TracingDefaults.SUPPORTED_PROTOCOL}'.\"\n    }\n  }\n\n  private fun Project.resolveTestTasks(extension: StoveTracingExtension): List<Test> {\n    val taskNames = extension.testTaskNames.get()\n    return if (taskNames.isEmpty()) {\n      tasks.withType(Test::class.java).toList()\n    } else {\n      taskNames.mapNotNull { taskName -> tasks.findByName(taskName) as? Test }\n    }\n  }\n\n  private fun configureTestTask(\n    testTask: Test,\n    otelAgentConfig: Configuration,\n    extension: StoveTracingExtension,\n  ) {\n    val resolvedAgentPath: String? = otelAgentConfig.resolve().firstOrNull()?.absolutePath\n\n    val tracingConfig = ResolvedTracingConfig(\n      protocol = extension.protocol.get(),\n      serviceName = extension.serviceName.get(),\n      bspScheduleDelay = extension.bspScheduleDelay.get(),\n      bspMaxBatchSize = extension.bspMaxBatchSize.get(),\n      captureHttpHeaders = extension.captureHttpHeaders.get(),\n      captureExperimentalTelemetry = extension.captureExperimentalTelemetry.get(),\n      customAnnotations = extension.customAnnotations.get(),\n      disabledInstrumentations = extension.disabledInstrumentations.get(),\n      additionalInstrumentations = extension.additionalInstrumentations.get(),\n    )\n\n    testTask.doFirst {\n      if (resolvedAgentPath == null) {\n        testTask.logger.warn(\"No OTel agent JAR found in otelAgent configuration\")\n        return@doFirst\n      }\n\n      val port = findAvailablePort()\n      testTask.environment(TracingDefaults.STOVE_TRACING_PORT_ENV, port.toString())\n\n      val jvmArgs = JvmArgsBuilder.build(resolvedAgentPath, tracingConfig, port)\n      testTask.jvmArgs(jvmArgs)\n      testTask.logger.info(\n        \"Stove tracing: Attached OTel agent on port {} with {} JVM arguments\",\n        port,\n        jvmArgs.size,\n      )\n    }\n  }\n\n  private fun Project.logConfiguration(extension: StoveTracingExtension, testTasks: List<Test>) {\n    val taskNames = extension.testTaskNames.get()\n    val taskInfo = if (taskNames.isEmpty()) {\n      \"all test tasks\"\n    } else {\n      \"tasks: ${testTasks.joinToString(\", \") { it.name }}\"\n    }\n    logger.info(\n      \"Stove tracing configured for service '${extension.serviceName.get()}' \" +\n        \"with dynamic port assignment on $taskInfo\"\n    )\n  }\n\n  private fun findAvailablePort(): Int = ServerSocket(0).use { it.localPort }\n}\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/src/main/kotlin/com/trendyol/stove/gradle/internal/TracingDefaults.kt",
    "content": "package com.trendyol.stove.gradle.internal\n\ninternal object TracingDefaults {\n  const val DEFAULT_BSP_SCHEDULE_DELAY = 100\n  const val DEFAULT_BSP_MAX_BATCH_SIZE = 1\n  const val DEFAULT_OTEL_AGENT_VERSION = \"2.24.0\"\n  const val DEFAULT_PROTOCOL = \"grpc\"\n  const val SUPPORTED_PROTOCOL = DEFAULT_PROTOCOL\n  const val DEFAULT_SERVICE_NAME = \"stove-traced-app\"\n  const val STOVE_TRACING_PORT_ENV = \"STOVE_TRACING_PORT\"\n}\n"
  },
  {
    "path": "plugins/stove-tracing-gradle-plugin/src/test/kotlin/com/trendyol/stove/gradle/StoveTracingPluginFunctionalTest.kt",
    "content": "package com.trendyol.stove.gradle\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.string.shouldContain\nimport org.gradle.testkit.runner.GradleRunner\nimport java.io.File\n\nclass StoveTracingPluginFunctionalTest : FunSpec({\n\n  lateinit var projectDir: File\n\n  beforeEach {\n    projectDir = File.createTempFile(\"stove-plugin-test\", \"\").apply {\n      delete()\n      mkdirs()\n    }\n    projectDir.resolve(\"settings.gradle.kts\").writeText(\"\")\n  }\n\n  afterEach {\n    projectDir.deleteRecursively()\n  }\n\n  test(\"plugin can be applied and extension is configurable\") {\n    projectDir.resolve(\"build.gradle.kts\").writeText(\n      \"\"\"\n      plugins {\n          java\n          id(\"com.trendyol.stove.tracing\")\n      }\n\n      repositories {\n          mavenCentral()\n      }\n\n      stoveTracing {\n          serviceName.set(\"test-service\")\n          enabled.set(true)\n          testTaskNames.set(listOf(\"test\"))\n      }\n      \"\"\".trimIndent()\n    )\n\n    projectDir.resolve(\"src/test/java\").mkdirs()\n\n    val result = GradleRunner.create()\n      .forwardOutput()\n      .withPluginClasspath()\n      .withArguments(\"tasks\", \"--all\")\n      .withProjectDir(projectDir)\n      .build()\n\n    result.output shouldContain \"test\"\n  }\n\n  test(\"plugin registers stoveTracing extension with defaults\") {\n    projectDir.resolve(\"build.gradle.kts\").writeText(\n      \"\"\"\n      plugins {\n          java\n          id(\"com.trendyol.stove.tracing\")\n      }\n\n      repositories {\n          mavenCentral()\n      }\n\n      tasks.register(\"printConfig\") {\n          doLast {\n              val ext = project.extensions.getByType(${StoveTracingExtension::class.qualifiedName}::class.java)\n              println(\"serviceName=${'$'}{ext.serviceName.get()}\")\n              println(\"enabled=${'$'}{ext.enabled.get()}\")\n              println(\"protocol=${'$'}{ext.protocol.get()}\")\n              println(\"otelAgentVersion=${'$'}{ext.otelAgentVersion.get()}\")\n          }\n      }\n      \"\"\".trimIndent()\n    )\n\n    val result = GradleRunner.create()\n      .forwardOutput()\n      .withPluginClasspath()\n      .withArguments(\"printConfig\")\n      .withProjectDir(projectDir)\n      .build()\n\n    result.output shouldContain \"serviceName=stove-traced-app\"\n    result.output shouldContain \"enabled=true\"\n    result.output shouldContain \"protocol=grpc\"\n    result.output shouldContain \"otelAgentVersion=2.24.0\"\n  }\n\n  test(\"plugin is disabled when enabled is set to false\") {\n    projectDir.resolve(\"build.gradle.kts\").writeText(\n      \"\"\"\n      plugins {\n          java\n          id(\"com.trendyol.stove.tracing\")\n      }\n\n      repositories {\n          mavenCentral()\n      }\n\n      stoveTracing {\n          serviceName.set(\"test-service\")\n          enabled.set(false)\n      }\n      \"\"\".trimIndent()\n    )\n\n    val result = GradleRunner.create()\n      .forwardOutput()\n      .withPluginClasspath()\n      .withArguments(\"tasks\", \"--info\")\n      .withProjectDir(projectDir)\n      .build()\n\n    result.output shouldContain \"Stove tracing is disabled\"\n  }\n})\n"
  },
  {
    "path": "pre-commit.sh",
    "content": "#!/bin/sh\n#\n# Git pre-commit hook — runs lint checks on changed projects.\n# Delegates to lint.sh which auto-detects changed files.\n\nREPO_ROOT=\"$(git rev-parse --show-toplevel)\"\nexec \"$REPO_ROOT/lint.sh\" --check\n"
  },
  {
    "path": "recipes/jvm/.editorconfig",
    "content": "root = true\n\n[*]\ninsert_final_newline = true\nktlint_standard_package-name = disabled\nktlint_standard_filename = disabled\nktlint_standard_no-wildcard-imports = disabled\nktlint_standard_multiline-expression-wrapping = disabled\nktlint_standard_string-template-indent = disabled\nktlint_standard_function-signature = disabled\n\n[*.java]\nindent_style = space\nmax_line_length = 140\nindent_size = 2\n\n[{*.kt,*.kts}]\nindent_style = space\nmax_line_length = 140\nindent_size = 2\nij_kotlin_code_style_defaults = KOTLIN_OFFICIAL\nij_continuation_indent_size = 2\nij_kotlin_allow_trailing_comma = false\nij_kotlin_allow_trailing_comma_on_call_site = false\nij_kotlin_name_count_to_use_star_import = 2\nij_kotlin_name_count_to_use_star_import_for_members = 2\n\n[{**/test/**.kt,**/test-e2e/**.kt,**/test-int/**.kt}]\nmax_line_length = 240\nktlint_standard_no-consecutive-comments = disabled"
  },
  {
    "path": "recipes/jvm/.gitattributes",
    "content": "#\n# https://help.github.com/articles/dealing-with-line-endings/\n#\n# Linux start script should use lf\n/gradlew        text eol=lf\n\n# These are Windows script files and should use crlf\n*.bat           text eol=crlf\n\n"
  },
  {
    "path": "recipes/jvm/.gitignore",
    "content": ".DS_Store\n/site\n/.idea\n.idea/shelf\n/confluence/target\n/dependencies/repo\n/android.tests.dependencies\n/dependencies/android.tests.dependencies\n/dist\n/local\n/gh-pages\n/ideaSDK\n/clionSDK\n/android-studio/sdk\nout/\n/tmp\n/intellij\nworkspace.xml\n*.versionsBackup\n/idea/testData/debugger/tinyApp/classes*\n/jps-plugin/testData/kannotator\n/js/js.translator/testData/out/\n/js/js.translator/testData/out-min/\n/js/js.translator/testData/out-pir/\n.gradle/\nbuild/\n!**/src/**/build\n!**/test/**/build\n*.iml\n!**/testData/**/*.iml\n.idea/remote-targets.xml\n.idea/libraries/Gradle*.xml\n.idea/libraries/Maven*.xml\n.idea/artifacts/PILL_*.xml\n.idea/artifacts/KotlinPlugin.xml\n.idea/modules\n.idea/runConfigurations/JPS_*.xml\n.idea/runConfigurations/PILL_*.xml\n.idea/runConfigurations/_FP_*.xml\n.idea/runConfigurations/_MT_*.xml\n.idea/libraries\n.idea/modules.xml\n.idea/gradle.xml\n.idea/compiler.xml\n.idea/inspectionProfiles/profiles_settings.xml\n.idea/.name\n.idea/artifacts/dist_auto_*\n.idea/artifacts/dist.xml\n.idea/artifacts/ideaPlugin.xml\n.idea/artifacts/kotlinc.xml\n.idea/artifacts/kotlin_compiler_jar.xml\n.idea/artifacts/kotlin_plugin_jar.xml\n.idea/artifacts/kotlin_jps_plugin_jar.xml\n.idea/artifacts/kotlin_daemon_client_jar.xml\n.idea/artifacts/kotlin_imports_dumper_compiler_plugin_jar.xml\n.idea/artifacts/kotlin_main_kts_jar.xml\n.idea/artifacts/kotlin_compiler_client_embeddable_jar.xml\n.idea/artifacts/kotlin_reflect_jar.xml\n.idea/artifacts/kotlin_stdlib_js_ir_*\n.idea/artifacts/kotlin_test_js_ir_*\n.idea/artifacts/kotlin_stdlib_wasm_*\n.idea/artifacts/kotlinx_atomicfu_runtime_*\n.idea/artifacts/kotlinx_cli_jvm_*\n.idea/jarRepositories.xml\n.idea/csv-plugin.xml\n.idea/libraries-with-intellij-classes.xml\n.idea/misc.xml\n.idea/**\nnode_modules/\n.rpt2_cache/\nlibraries/tools/kotlin-test-js-runner/lib/\nlocal.properties\nbuildSrcTmp/\ndistTmp/\noutTmp/\n/test.output\n/kotlin-native/dist\nkotlin-ide/\n**/bin/**/*\n\n# Ignore Gradle project-specific cache directory\n.gradle\n\n# Ignore Gradle build output directory\nbuild\n"
  },
  {
    "path": "recipes/jvm/build.gradle.kts",
    "content": "import org.gradle.plugins.ide.idea.model.IdeaModel\nimport org.jetbrains.kotlin.gradle.dsl.JvmTarget\n\nplugins {\n  kotlin(\"jvm\").version(libs.versions.kotlin)\n  alias(libs.plugins.spotless)\n  alias(libs.plugins.testLogger)\n  alias(libs.plugins.detekt)\n  idea\n  java\n}\n\nsubprojects {\n  apply {\n    plugin(rootProject.libs.plugins.spotless.get().pluginId)\n    plugin(rootProject.libs.plugins.testLogger.get().pluginId)\n    plugin(rootProject.libs.plugins.detekt.get().pluginId)\n    plugin(\"idea\")\n    plugin(\"java\")\n    plugin(\"kotlin\")\n  }\n\n  detekt {\n    buildUponDefaultConfig = true\n    parallel = true\n    config.from(rootProject.file(\"detekt.yml\"))\n  }\n\n  dependencies {\n    testImplementation(rootProject.libs.kotest.framework.engine)\n    testImplementation(rootProject.libs.kotest.assertions.core)\n    testImplementation(rootProject.libs.kotest.runner.junit5)\n    detektPlugins(rootProject.libs.detekt.formatting)\n  }\n\n  spotless {\n    java {\n      target(\"src/**/*.java\")\n      palantirJavaFormat(\"2.86.0\").style(\"GOOGLE\").formatJavadoc(true)\n      targetExcludeIfContentContains(\"generated\")\n      targetExclude(\"build/**\", \"**/build/**\", \"**/generated/**\")\n      targetExcludeIfContentContainsRegex(\".*generated.*\")\n    }\n\n    scala {\n      scalafmt(\"3.10.6\")\n    }\n\n    kotlin {\n      target(\"src/**/*.kt\")\n      ktlint(libs.versions.ktlint.get())\n        .setEditorConfigPath(rootProject.layout.projectDirectory.file(\".editorconfig\"))\n      targetExclude(\"build/**\", \"**/build/**\", \"**/generated/**\")\n      targetExcludeIfContentContains(\"generated\")\n      targetExcludeIfContentContainsRegex(\".*generated.*\")\n    }\n  }\n\n  the<IdeaModel>().apply {\n    module {\n      isDownloadSources = true\n      isDownloadJavadoc = true\n    }\n  }\n\n  tasks {\n    test {\n      dependsOn(spotlessApply)\n      useJUnitPlatform()\n      testlogger {\n        setTheme(\"mocha\")\n        showStandardStreams = true\n        showExceptions = true\n        showCauses = true\n      }\n      reports {\n        junitXml.required.set(true)\n      }\n      jvmArgs(\"--add-opens\", \"java.base/java.util=ALL-UNNAMED\")\n    }\n\n    kotlin {\n      jvmToolchain(21)\n    }\n    java {\n      sourceCompatibility = JavaVersion.VERSION_21\n      targetCompatibility = JavaVersion.VERSION_21\n    }\n\n    withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {\n      compilerOptions {\n        jvmTarget.set(JvmTarget.JVM_21)\n        allWarningsAsErrors = true\n        freeCompilerArgs.addAll(\n          \"-Xjsr305=strict\",\n          \"-Xcontext-parameters\",\n          \"-Xsuppress-version-warnings\"\n        )\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/buildSrc/build.gradle.kts",
    "content": "plugins {\n    `kotlin-dsl`\n}\n\nrepositories {\n    gradlePluginPortal()\n}\n"
  },
  {
    "path": "recipes/jvm/buildSrc/settings.gradle.kts",
    "content": "rootProject.name = \"buildSrc\"\ndependencyResolutionManagement {\n    versionCatalogs {\n        create(\"libs\", { from(files(\"../gradle/libs.versions.toml\")) })\n    }\n}\n\n"
  },
  {
    "path": "recipes/jvm/buildSrc/src/main/kotlin/TestFolders.kt",
    "content": "object TestFolders {\n    const val integration = \"test-int\"\n    const val e2e = \"test-e2e\"\n    const val shared = \"test-shared\"\n}\n\nval runningOnCI get() = System.getenv(\"CI\") == \"true\"\n\nval runningLocally get() = !runningOnCI\n"
  },
  {
    "path": "recipes/jvm/detekt.yml",
    "content": "build:\n  maxIssues: 0\n  excludeCorrectable: false\n\nconfig:\n  validation: true\n  warningsAsErrors: true\n  excludes: ''\n\nprocessors:\n  active: true\n  exclude:\n    - 'DetektProgressListener'\n\nconsole-reports:\n  active: true\n  exclude:\n    - 'ProjectStatisticsReport'\n    - 'ComplexityReport'\n    - 'NotificationReport'\n    - 'FindingsReport'\n    - 'FileBasedFindingsReport'\n\noutput-reports:\n  active: false\n\nformatting:\n  Indentation:\n    active: false\n    indentSize: 2\n    autoCorrect: true\n  NoWildcardImports:\n    active: false\n  MaximumLineLength:\n    active: true\n    maxLineLength: 140\n    excludes: [ '**/test/**', '**/test-e2e/**' ]\n  ArgumentListWrapping:\n    maxLineLength: 140\n    autoCorrect: true\n    active: true\n    indentSize: 2\n  Filename:\n    active: false\n\ncomments:\n  active: true\n  AbsentOrWrongFileLicense:\n    active: false\n    licenseTemplateFile: 'license.template'\n    licenseTemplateIsRegex: false\n  CommentOverPrivateFunction:\n    active: false\n  CommentOverPrivateProperty:\n    active: false\n  DeprecatedBlockTag:\n    active: false\n  EndOfSentenceFormat:\n    active: false\n    endOfSentenceFormat: '([.?!][ \\t\\n\\r\\f<])|([.?!:]$)'\n  KDocReferencesNonPublicProperty:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  OutdatedDocumentation:\n    active: false\n    matchTypeParameters: true\n    matchDeclarationsOrder: true\n    allowParamOnConstructorProperties: false\n  UndocumentedPublicClass:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    searchInNestedClass: true\n    searchInInnerClass: true\n    searchInInnerObject: true\n    searchInInnerInterface: true\n  UndocumentedPublicFunction:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  UndocumentedPublicProperty:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n\ncomplexity:\n  active: true\n  ComplexCondition:\n    active: true\n    threshold: 4\n  ComplexInterface:\n    active: false\n    threshold: 10\n    includeStaticDeclarations: false\n    includePrivateDeclarations: false\n  CyclomaticComplexMethod:\n    active: true\n    threshold: 15\n    ignoreSingleWhenExpression: false\n    ignoreSimpleWhenEntries: false\n    ignoreNestingFunctions: false\n    nestingFunctions:\n      - 'also'\n      - 'apply'\n      - 'forEach'\n      - 'isNotNull'\n      - 'ifNull'\n      - 'let'\n      - 'run'\n      - 'use'\n      - 'with'\n  LabeledExpression:\n    active: false\n    ignoredLabels: [ ]\n  LargeClass:\n    active: true\n    threshold: 600\n  LongMethod:\n    active: true\n    threshold: 60\n  LongParameterList:\n    active: true\n    functionThreshold: 20\n    constructorThreshold: 20\n    ignoreDefaultParameters: false\n    ignoreDataClasses: true\n    ignoreAnnotatedParameter: [ ]\n  MethodOverloading:\n    active: false\n    threshold: 6\n  NamedArguments:\n    active: false\n    threshold: 3\n    ignoreArgumentsMatchingNames: false\n  NestedBlockDepth:\n    active: true\n    threshold: 4\n  NestedScopeFunctions:\n    active: false\n    threshold: 1\n    functions:\n      - 'kotlin.apply'\n      - 'kotlin.run'\n      - 'kotlin.with'\n      - 'kotlin.let'\n      - 'kotlin.also'\n  ReplaceSafeCallChainWithRun:\n    active: false\n  StringLiteralDuplication:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    threshold: 3\n    ignoreAnnotation: true\n    excludeStringsWithLessThan5Characters: true\n    ignoreStringsRegex: '$^'\n  TooManyFunctions:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    thresholdInFiles: 20\n    thresholdInClasses: 20\n    thresholdInInterfaces: 11\n    thresholdInObjects: 11\n    thresholdInEnums: 11\n    ignoreDeprecated: false\n    ignorePrivate: false\n    ignoreOverridden: false\n\ncoroutines:\n  active: true\n  GlobalCoroutineUsage:\n    active: false\n  InjectDispatcher:\n    active: false\n    dispatcherNames:\n      - 'IO'\n      - 'Default'\n      - 'Unconfined'\n  RedundantSuspendModifier:\n    active: false\n  SleepInsteadOfDelay:\n    active: true\n  SuspendFunWithCoroutineScopeReceiver:\n    active: false\n  SuspendFunWithFlowReturnType:\n    active: true\n\nempty-blocks:\n  active: true\n  EmptyCatchBlock:\n    active: true\n    allowedExceptionNameRegex: '_|(ignore|expected).*'\n  EmptyClassBlock:\n    active: true\n  EmptyDefaultConstructor:\n    active: true\n  EmptyDoWhileBlock:\n    active: true\n  EmptyElseBlock:\n    active: true\n  EmptyFinallyBlock:\n    active: true\n  EmptyForBlock:\n    active: true\n  EmptyFunctionBlock:\n    active: true\n    ignoreOverridden: false\n  EmptyIfBlock:\n    active: true\n  EmptyInitBlock:\n    active: true\n  EmptyKtFile:\n    active: true\n  EmptySecondaryConstructor:\n    active: true\n  EmptyTryBlock:\n    active: true\n  EmptyWhenBlock:\n    active: true\n  EmptyWhileBlock:\n    active: true\n\nexceptions:\n  active: true\n  ExceptionRaisedInUnexpectedLocation:\n    active: true\n    methodNames:\n      - 'equals'\n      - 'finalize'\n      - 'hashCode'\n      - 'toString'\n  InstanceOfCheckForException:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  NotImplementedDeclaration:\n    active: false\n  ObjectExtendsThrowable:\n    active: false\n  PrintStackTrace:\n    active: true\n  RethrowCaughtException:\n    active: true\n  ReturnFromFinally:\n    active: true\n    ignoreLabeled: false\n  SwallowedException:\n    active: true\n    ignoredExceptionTypes:\n      - 'InterruptedException'\n      - 'MalformedURLException'\n      - 'NumberFormatException'\n      - 'ParseException'\n    allowedExceptionNameRegex: '_|(ignore|expected).*'\n  ThrowingExceptionFromFinally:\n    active: true\n  ThrowingExceptionInMain:\n    active: false\n  ThrowingExceptionsWithoutMessageOrCause:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    exceptions:\n      - 'ArrayIndexOutOfBoundsException'\n      - 'Exception'\n      - 'IllegalArgumentException'\n      - 'IllegalMonitorStateException'\n      - 'IllegalStateException'\n      - 'IndexOutOfBoundsException'\n      - 'NullPointerException'\n      - 'RuntimeException'\n      - 'Throwable'\n  ThrowingNewInstanceOfSameException:\n    active: true\n  TooGenericExceptionCaught:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    exceptionNames:\n      - 'ArrayIndexOutOfBoundsException'\n      - 'Error'\n      - 'Exception'\n      - 'IllegalMonitorStateException'\n      - 'IndexOutOfBoundsException'\n      - 'NullPointerException'\n      - 'RuntimeException'\n      - 'Throwable'\n    allowedExceptionNameRegex: '_|(ignore|expected).*'\n  TooGenericExceptionThrown:\n    active: true\n    exceptionNames:\n      - 'Error'\n      - 'Exception'\n      - 'RuntimeException'\n      - 'Throwable'\n\nnaming:\n  active: true\n  BooleanPropertyNaming:\n    active: false\n    allowedPattern: '^(is|has|are)'\n  ClassNaming:\n    active: true\n    classPattern: '[A-Z][a-zA-Z0-9]*'\n  EnumNaming:\n    active: true\n    enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'\n  ForbiddenClassName:\n    active: false\n    forbiddenName: [ ]\n  FunctionMaxLength:\n    active: false\n    maximumFunctionNameLength: 30\n  FunctionMinLength:\n    active: false\n    minimumFunctionNameLength: 3\n  InvalidPackageDeclaration:\n    active: true\n    rootPackage: ''\n    requireRootInDeclaration: false\n  LambdaParameterNaming:\n    active: false\n    parameterPattern: '[a-z][A-Za-z0-9]*|_'\n  MatchingDeclarationName:\n    active: false\n    mustBeFirst: true\n  MemberNameEqualsClassName:\n    active: true\n    ignoreOverridden: true\n  NoNameShadowing:\n    active: true\n  NonBooleanPropertyPrefixedWithIs:\n    active: false\n  ObjectPropertyNaming:\n    active: true\n    constantPattern: '[A-Za-z][_A-Za-z0-9]*'\n    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'\n    privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'\n  PackageNaming:\n    active: true\n    packagePattern: '[a-z]+(\\.[a-z][A-Za-z0-9]*)*'\n  TopLevelPropertyNaming:\n    active: true\n    constantPattern: '[A-Z][_A-Z0-9]*'\n    propertyPattern: '[A-Za-z][_A-Za-z0-9]*'\n    privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'\n  VariableMaxLength:\n    active: false\n    maximumVariableNameLength: 64\n  VariableMinLength:\n    active: false\n    minimumVariableNameLength: 1\n  ConstructorParameterNaming:\n    active: false\n    parameterPattern: '[a-z][A-Za-z0-9]*|_'\n\n\nperformance:\n  active: true\n  ArrayPrimitive:\n    active: true\n  CouldBeSequence:\n    active: false\n    threshold: 3\n  ForEachOnRange:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  SpreadOperator:\n    active: false\n    excludes: [\n      '**/test/**',\n      '**/androidTest/**',\n      '**/commonTest/**',\n      '**/jvmTest/**',\n      '**/jsTest/**',\n      '**/iosTest/**',\n      '**/otel/**',\n    ]\n  UnnecessaryTemporaryInstantiation:\n    active: true\n\npotential-bugs:\n  active: true\n  AvoidReferentialEquality:\n    active: true\n    forbiddenTypePatterns:\n      - 'kotlin.String'\n  CastToNullableType:\n    active: false\n  Deprecation:\n    active: false\n  DontDowncastCollectionTypes:\n    active: false\n  DoubleMutabilityForCollection:\n    active: true\n    mutableTypes:\n      - 'kotlin.collections.MutableList'\n      - 'kotlin.collections.MutableMap'\n      - 'kotlin.collections.MutableSet'\n      - 'java.util.ArrayList'\n      - 'java.util.LinkedHashSet'\n      - 'java.util.HashSet'\n      - 'java.util.LinkedHashMap'\n      - 'java.util.HashMap'\n  ElseCaseInsteadOfExhaustiveWhen:\n    active: false\n  EqualsAlwaysReturnsTrueOrFalse:\n    active: true\n  EqualsWithHashCodeExist:\n    active: true\n  ExitOutsideMain:\n    active: false\n  ExplicitGarbageCollectionCall:\n    active: true\n  HasPlatformType:\n    active: true\n  IgnoredReturnValue:\n    active: true\n    restrictToConfig: true\n    returnValueAnnotations:\n      - '*.CheckResult'\n      - '*.CheckReturnValue'\n    ignoreReturnValueAnnotations:\n      - '*.CanIgnoreReturnValue'\n    ignoreFunctionCall: [ ]\n  ImplicitDefaultLocale:\n    active: true\n  ImplicitUnitReturnType:\n    active: false\n    allowExplicitReturnType: true\n  InvalidRange:\n    active: true\n  IteratorHasNextCallsNextMethod:\n    active: true\n  IteratorNotThrowingNoSuchElementException:\n    active: true\n  LateinitUsage:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    ignoreOnClassesPattern: ''\n  MapGetWithNotNullAssertionOperator:\n    active: true\n  MissingPackageDeclaration:\n    active: false\n    excludes: [ '**/*.kts' ]\n\n  NullCheckOnMutableProperty:\n    active: false\n  NullableToStringCall:\n    active: false\n  UnconditionalJumpStatementInLoop:\n    active: false\n  UnnecessaryNotNullOperator:\n    active: true\n  UnnecessarySafeCall:\n    active: true\n  UnreachableCatchBlock:\n    active: true\n  UnreachableCode:\n    active: true\n  UnsafeCallOnNullableType:\n    active: true\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n  UnsafeCast:\n    active: true\n  UnusedUnaryOperator:\n    active: true\n  UselessPostfixExpression:\n    active: true\n  WrongEqualsTypeParameter:\n    active: true\n\nstyle:\n  active: true\n  CanBeNonNullable:\n    active: false\n  CascadingCallWrapping:\n    active: false\n    includeElvis: true\n  ClassOrdering:\n    active: false\n  CollapsibleIfStatements:\n    active: false\n  DataClassContainsFunctions:\n    active: false\n    conversionFunctionPrefix:\n      - 'to'\n  DataClassShouldBeImmutable:\n    active: false\n  DestructuringDeclarationWithTooManyEntries:\n    active: true\n    maxDestructuringEntries: 6\n  EqualsNullCall:\n    active: true\n  EqualsOnSignatureLine:\n    active: false\n  ExplicitCollectionElementAccessMethod:\n    active: false\n  ExplicitItLambdaParameter:\n    active: true\n  ExpressionBodySyntax:\n    active: false\n    includeLineWrapping: false\n  ForbiddenComment:\n    active: false\n    comments:\n      - 'FIXME:'\n      - 'STOPSHIP:'\n      - 'TODO:'\n  ForbiddenImport:\n    active: false\n    imports: [ ]\n    forbiddenPatterns: ''\n  ForbiddenMethodCall:\n    active: false\n    methods:\n      - 'kotlin.io.print'\n      - 'kotlin.io.println'\n  ForbiddenSuppress:\n    active: false\n    rules: [ ]\n  ForbiddenVoid:\n    active: true\n    ignoreOverridden: false\n    ignoreUsageInGenerics: false\n  FunctionOnlyReturningConstant:\n    active: true\n    ignoreOverridableFunction: true\n    ignoreActualFunction: true\n    excludedFunctions:\n      - ''\n  LoopWithTooManyJumpStatements:\n    active: true\n    maxJumpCount: 1\n  MagicNumber:\n    active: false\n    excludes: [\n      '**/test/**',\n      '**/test-e2e/**',\n      '**/androidTest/**',\n      '**/commonTest/**',\n      '**/jvmTest/**',\n      '**/jsTest/**',\n      '**/iosTest/**',\n      '**/domain/**',\n      '**/core/**',\n      '**/*.kts' ]\n    ignoreNumbers:\n      - '-1'\n      - '0'\n      - '1'\n      - '2'\n    ignoreHashCodeFunction: true\n    ignorePropertyDeclaration: false\n    ignoreLocalVariableDeclaration: false\n    ignoreConstantDeclaration: true\n    ignoreCompanionObjectPropertyDeclaration: true\n    ignoreAnnotation: false\n    ignoreNamedArgument: true\n    ignoreEnums: false\n    ignoreRanges: false\n    ignoreExtensionFunctions: true\n  BracesOnIfStatements:\n    active: false\n  MandatoryBracesLoops:\n    active: false\n  MaxChainedCallsOnSameLine:\n    active: false\n    maxChainedCalls: 5\n  MaxLineLength:\n    active: true\n    maxLineLength: 140\n    excludePackageStatements: true\n    excludeImportStatements: true\n    excludeCommentStatements: false\n    excludes:\n      - '**/test/**'\n      - '**/test-e2e/**'\n      - '**/test-integration/**'\n  MayBeConst:\n    active: true\n  ModifierOrder:\n    active: true\n  MultilineLambdaItParameter:\n    active: false\n  NestedClassesVisibility:\n    active: true\n  NewLineAtEndOfFile:\n    active: true\n  NoTabs:\n    active: false\n  NullableBooleanCheck:\n    active: false\n  ObjectLiteralToLambda:\n    active: true\n  OptionalAbstractKeyword:\n    active: true\n  OptionalUnit:\n    active: false\n  BracesOnWhenStatements:\n    active: false\n  PreferToOverPairSyntax:\n    active: false\n  ProtectedMemberInFinalClass:\n    active: true\n  RedundantExplicitType:\n    active: false\n  RedundantHigherOrderMapUsage:\n    active: true\n  RedundantVisibilityModifierRule:\n    active: false\n  ReturnCount:\n    active: true\n    max: 5\n    excludedFunctions:\n      - 'equals'\n    excludeLabeled: false\n    excludeReturnFromLambda: true\n    excludeGuardClauses: false\n  SafeCast:\n    active: true\n  SerialVersionUIDInSerializableClass:\n    active: true\n  SpacingBetweenPackageAndImports:\n    active: false\n  ThrowsCount:\n    active: true\n    max: 2\n    excludeGuardClauses: false\n  TrailingWhitespace:\n    active: false\n  UnderscoresInNumericLiterals:\n    active: false\n    acceptableLength: 4\n    allowNonStandardGrouping: false\n  UnnecessaryAbstractClass:\n    active: true\n  UnnecessaryAnnotationUseSiteTarget:\n    active: false\n  UnnecessaryApply:\n    active: true\n  UnnecessaryBackticks:\n    active: false\n  UnnecessaryFilter:\n    active: true\n  UnnecessaryInheritance:\n    active: true\n  UnnecessaryInnerClass:\n    active: false\n  UnnecessaryLet:\n    active: false\n  UnnecessaryParentheses:\n    active: false\n  UntilInsteadOfRangeTo:\n    active: false\n  UnusedImports:\n    active: false\n  UnusedPrivateClass:\n    active: true\n  UnusedPrivateMember:\n    active: true\n    allowedNames: '(_|ignored|expected|serialVersionUID)'\n  UseAnyOrNoneInsteadOfFind:\n    active: true\n  UseArrayLiteralsInAnnotations:\n    active: true\n  UseCheckNotNull:\n    active: true\n  UseCheckOrError:\n    active: true\n  UseDataClass:\n    active: false\n    allowVars: false\n  UseEmptyCounterpart:\n    active: false\n  UseIfEmptyOrIfBlank:\n    active: false\n  UseIfInsteadOfWhen:\n    active: false\n  UseIsNullOrEmpty:\n    active: true\n  UseOrEmpty:\n    active: true\n  UseRequire:\n    active: true\n  UseRequireNotNull:\n    active: true\n  UselessCallOnNotNull:\n    active: true\n  UtilityClassWithPublicConstructor:\n    active: true\n  VarCouldBeVal:\n    active: true\n    ignoreLateinitVar: false\n  WildcardImport:\n    active: false\n    excludes: [ '**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**' ]\n    excludeImports:\n      - 'java.util.*'\n"
  },
  {
    "path": "recipes/jvm/gradle/libs.versions.toml",
    "content": "[versions]\nkotlin = \"2.3.21\"\nkotlinx = \"1.10.2\"\nscala2x = \"2.13.18\"\nquarkus = \"3.35.2\"\nktlint = \"1.8.0\"\n\n# Spring-Boot\nspring-boot = \"3.5.14\"\nspring-dependency-management = \"1.1.7\"\nspring-kafka = \"3.3.15\"\n\n# arrow\narrow = \"2.2.2.1\"\n\n# Jackson\njackson = \"2.21\"\n\n# Kafka\nkafka = \"4.2.0\"\nkafka-kotlin = \"0.4.1\"\n\n# Logging\nslf4j = \"2.0.17\"\nkotlinLogging = \"8.0.02\"\n\n# Ktor\nktor = \"3.4.3\"\nkoin = \"4.2.1\"\n\n# mongo\nmongodb = \"5.7.0\"\n\n# Tooling\nspotless = \"8.4.0\"\ndetekt = \"1.23.8\"\nlombok = \"1.18.46\"\n\n# Misc\nhoplite = \"2.9.0\"\nkediatr = \"4.3.0\"\n\n# OpenTelemetry\nopentelemetry = \"1.62.0\"\nopentelemetry-instrumentation = \"2.27.0\"\n\n# gRPC\ngrpc = \"1.81.0\"\ngrpc-kotlin = \"1.5.0\"\nprotobuf = \"4.34.1\"\nprotobuf-plugin = \"0.10.0\"\n\n# db-scheduler\ndb-scheduler = \"16.8.1\"\n\n# Testing\nstove = \"1.0.0.529-SNAPSHOT\"\nkotest = \"6.1.11\"\nexposed = \"1.2.0\"\npostgresql = \"42.7.11\"\nflyway = \"12.6.0\"\n\n[libraries]\n# Kotlin\nkotlinx-reactor = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-reactor\", version.ref = \"kotlinx\" }\nkotlinx-reactive = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-reactive\", version.ref = \"kotlinx\" }\nkotlinx-core = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-core\", version.ref = \"kotlinx\" }\nkotlinx-jdk8 = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-jdk8\", version.ref = \"kotlinx\" }\n\n# Arrow\narrow-core = { module = \"io.arrow-kt:arrow-core\", version.ref = \"arrow\" }\n\n# Spring\nspring-boot-webflux = { module = \"org.springframework.boot:spring-boot-starter-webflux\", version.ref = \"spring-boot\" }\nspring-boot-autoconfigure = { module = \"org.springframework.boot:spring-boot-autoconfigure\", version.ref = \"spring-boot\" }\nspring-boot-annotationProcessor = { module = \"org.springframework.boot:spring-boot-configuration-processor\", version.ref = \"spring-boot\" }\nspring-boot-kafka = { module = \"org.springframework.kafka:spring-kafka\", version.ref = \"spring-kafka\" }\nspring-boot-data-r2dbc = { module = \"org.springframework.boot:spring-boot-starter-data-r2dbc\", version.ref = \"spring-boot\" }\nspring-boot-starter-jdbc = { module = \"org.springframework.boot:spring-boot-starter-jdbc\", version.ref = \"spring-boot\" }\n\n# db-scheduler\ndb-scheduler-spring-boot-starter = { module = \"com.github.kagkarlsson:db-scheduler-spring-boot-starter\", version.ref = \"db-scheduler\" }\n\n# Quarkus\nquarkus = { module = \"io.quarkus:quarkus-bom\", version.ref = \"quarkus\" }\nquarkus-rest = { module = \"io.quarkus:quarkus-rest\", version.ref = \"quarkus\" }\nquarkus-arc = { module = \"io.quarkus.arc:arc\", version.ref = \"quarkus\" }\nquarkus-kotlin = { module = \"io.quarkus:quarkus-kotlin\", version.ref = \"quarkus\" }\n\nkafka = { module = \"org.apache.kafka:kafka-clients\", version.ref = \"kafka\" }\nkafkaKotlin = { module = \"io.github.nomisrev:kotlin-kafka\", version.ref = \"kafka-kotlin\" }\n\n# Jackson\njackson-annotations = { module = \"com.fasterxml.jackson.core:jackson-annotations\", version.ref = \"jackson\" }\n\nslf4j-api = { module = \"org.slf4j:slf4j-api\", version.ref = \"slf4j\" }\nktor-server-core-jvm = { module = \"io.ktor:ktor-server-core-jvm\", version.ref = \"ktor\" }\nktor-server-config-yml = { module = \"io.ktor:ktor-server-config-yaml\", version.ref = \"ktor\" }\nktor-server-content-negotiation-jvm = { module = \"io.ktor:ktor-server-content-negotiation-jvm\", version.ref = \"ktor\" }\nktor-serialization-jackson-json = { module = \"io.ktor:ktor-serialization-jackson\", version.ref = \"ktor\" }\nktor-server-netty-jvm = { module = \"io.ktor:ktor-server-netty-jvm\", version.ref = \"ktor\" }\nktor-server-statuspages = { module = \"io.ktor:ktor-server-status-pages\", version.ref = \"ktor\" }\nktor-server-callLogging = { module = \"io.ktor:ktor-server-call-logging\", version.ref = \"ktor\" }\nktor-server-autoHeadResponse = { module = \"io.ktor:ktor-server-auto-head-response\", version.ref = \"ktor\" }\nktor-server-cachingHeaders = { module = \"io.ktor:ktor-server-caching-headers\", version.ref = \"ktor\" }\nktor-server-callId = { module = \"io.ktor:ktor-server-call-id-jvm\", version.ref = \"ktor\" }\nktor-server-conditionalHeaders = { module = \"io.ktor:ktor-server-conditional-headers\", version.ref = \"ktor\" }\nktor-server-cors = { module = \"io.ktor:ktor-server-cors-jvm\", version.ref = \"ktor\" }\nktor-server-defaultHeaders = { module = \"io.ktor:ktor-server-default-headers\", version.ref = \"ktor\" }\nktor-swagger-ui = { module = \"io.github.smiley4:ktor-swagger-ui\", version = \"5.7.0\" }\nktor-client-core = { module = \"io.ktor:ktor-client-core\", version.ref = \"ktor\" }\nktor-client-cio = { module = \"io.ktor:ktor-client-cio\", version.ref = \"ktor\" }\nktor-client-okhttp = { module = \"io.ktor:ktor-client-okhttp\", version.ref = \"ktor\" }\nktor-client-plugins-logging = { module = \"io.ktor:ktor-client-logging\", version.ref = \"ktor\" }\nktor-client-content-negotiation = { module = \"io.ktor:ktor-client-content-negotiation\", version.ref = \"ktor\" }\nktor-client-websockets = { module = \"io.ktor:ktor-client-websockets\", version.ref = \"ktor\" }\nkoin = { module = \"io.insert-koin:koin-core\", version.ref = \"koin\" }\nkoin-ktor = { module = \"io.insert-koin:koin-ktor\", version.ref = \"koin\" }\nkotlinFpUtil = { module = \"it.czerwinski:kotlin-util\", version = \"2.1.0\" }\nmongodb-bson-kotlin = { module = \"org.mongodb:bson-kotlin\", version.ref = \"mongodb\" }\nmongodb-kotlin-coroutine = { module = \"org.mongodb:mongodb-driver-kotlin-coroutine\", version.ref = \"mongodb\" }\nkotlin-logging-jvm = { module = \"io.github.oshai:kotlin-logging-jvm\", version.ref = \"kotlinLogging\" }\nlogback-classic = { module = \"ch.qos.logback:logback-classic\", version = \"1.5.32\" }\n\ndetekt-formatting = { module = \"io.gitlab.arturbosch.detekt:detekt-formatting\", version.ref = \"detekt\" }\nhoplite = { module = \"com.sksamuel.hoplite:hoplite-core\", version.ref = \"hoplite\" }\nhoplite-yaml = { module = \"com.sksamuel.hoplite:hoplite-yaml\", version.ref = \"hoplite\" }\nktlint-cli = { module = \"com.pinterest.ktlint:ktlint-cli\", version.ref = \"ktlint\" }\n\n# kediatR\nkediatr-koin = { module = \"com.trendyol:kediatr-koin-starter\", version.ref = \"kediatr\" }\n\n# Tooling\nlombok = { module = \"org.projectlombok:lombok\", version.ref = \"lombok\" }\n\n# Testing\nkotest-framework-engine = { module = \"io.kotest:kotest-framework-engine\", version.ref = \"kotest\" }\nkotest-assertions-core = { module = \"io.kotest:kotest-assertions-core\", version.ref = \"kotest\" }\nkotest-runner-junit5 = { module = \"io.kotest:kotest-runner-junit5-jvm\", version.ref = \"kotest\" }\ntestcontainers-kafka = { module = \"org.testcontainers:kafka\", version = \"1.21.4\" }\n\nexposed-core = { module = \"org.jetbrains.exposed:exposed-core\", version.ref = \"exposed\" }\nexposed-r2dbc = { module = \"org.jetbrains.exposed:exposed-r2dbc\", version.ref = \"exposed\" }\nexposed-json = { module = \"org.jetbrains.exposed:exposed-json\", version.ref = \"exposed\" }\nexposed-javaTime = { module = \"org.jetbrains.exposed:exposed-java-time\", version.ref = \"exposed\" }\nflyway-core = { module = \"org.flywaydb:flyway-core\", version.ref = \"flyway\" }\nflyway-database-postgresql = { module = \"org.flywaydb:flyway-database-postgresql\", version.ref = \"flyway\" }\npostgresql = { module = \"org.postgresql:postgresql\", version.ref = \"postgresql\" }\npostgresql-r2dbc = { module = \"org.postgresql:r2dbc-postgresql\", version = \"1.1.1.RELEASE\" }\nr2dbc-pool = { module = \"io.r2dbc:r2dbc-pool\", version = \"1.0.2.RELEASE\" }\n\n# OpenTelemetry\nopentelemetry-extension-kotlin = { module = \"io.opentelemetry:opentelemetry-extension-kotlin\", version.ref = \"opentelemetry\" }\nopentelemetry-instrumentation-annotations = { module = \"io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations\", version.ref = \"opentelemetry-instrumentation\" }\n\n# gRPC\ngrpc-protobuf = { module = \"io.grpc:grpc-protobuf\", version.ref = \"grpc\" }\ngrpc-stub = { module = \"io.grpc:grpc-stub\", version.ref = \"grpc\" }\ngrpc-netty = { module = \"io.grpc:grpc-netty-shaded\", version.ref = \"grpc\" }\ngrpc-kotlin-stub = { module = \"io.grpc:grpc-kotlin-stub\", version.ref = \"grpc-kotlin\" }\nprotobuf-kotlin = { module = \"com.google.protobuf:protobuf-kotlin\", version.ref = \"protobuf\" }\nprotoc = { module = \"com.google.protobuf:protoc\", version.ref = \"protobuf\" }\ngrpc-protoc-gen-java = { module = \"io.grpc:protoc-gen-grpc-java\", version.ref = \"grpc\" }\ngrpc-protoc-gen-kotlin = { module = \"io.grpc:protoc-gen-grpc-kotlin\", version.ref = \"grpc-kotlin\" }\n\n# Stove\nstove-bom = { module = \"com.trendyol:stove-bom\", version.ref = \"stove\" }\n\n# Scala\nscala2-library = { module = \"org.scala-lang:scala-library\", version.ref = \"scala2x\" }\n\n[plugins]\nprotobuf = { id = \"com.google.protobuf\", version.ref = \"protobuf-plugin\" }\nspring-plugin = { id = \"org.jetbrains.kotlin.plugin.spring\", version.ref = \"kotlin\" }\nspring-boot = { id = \"org.springframework.boot\", version.ref = \"spring-boot\" }\nspring-dependencyManagement = { id = \"io.spring.dependency-management\", version.ref = \"spring-dependency-management\" }\nspotless = { id = \"com.diffplug.spotless\", version.ref = \"spotless\" }\ndetekt = { id = \"io.gitlab.arturbosch.detekt\", version.ref = \"detekt\" }\ntestLogger = { id = \"com.adarshr.test-logger\", version = \"4.0.0\" }\nquarkus = { id = \"io.quarkus\", version.ref = \"quarkus\" }\nkotest = { id = \"io.kotest\", version.ref = \"kotest\" }\n"
  },
  {
    "path": "recipes/jvm/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.5.0-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "recipes/jvm/gradle.properties",
    "content": "org.gradle.parallel=false\norg.gradle.caching=true\norg.gradle.configuration-cache=true\n\n"
  },
  {
    "path": "recipes/jvm/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "recipes/jvm/gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "recipes/jvm/java-recipes/build.gradle.kts",
    "content": "plugins {\n  java\n  kotlin(\"jvm\") version libs.versions.kotlin\n  idea\n}\n\nsubprojects {\n  apply {\n    plugin(\"java\")\n    plugin(\"kotlin\")\n    plugin(\"idea\")\n  }\n  val libs = rootProject.libs\n  sourceSets {\n    @Suppress(\"LocalVariableName\", \"ktlint:standard:property-naming\")\n    val `test-e2e` by creating {\n      compileClasspath += sourceSets.main.get().output\n      runtimeClasspath += sourceSets.main.get().output\n    }\n\n    val testE2eImplementation by configurations.getting {\n      extendsFrom(configurations.testImplementation.get())\n    }\n    configurations[\"testE2eRuntimeOnly\"].extendsFrom(configurations.runtimeOnly.get())\n  }\n\n  idea {\n    module {\n      testSources.from(sourceSets[TestFolders.e2e].allSource.sourceDirectories)\n      testResources.from(sourceSets[TestFolders.e2e].resources.sourceDirectories)\n      isDownloadJavadoc = true\n      isDownloadSources = true\n    }\n  }\n\n  dependencies {\n    compileOnly(libs.lombok)\n    annotationProcessor(libs.lombok)\n  }\n\n  dependencies {\n    testCompileOnly(libs.lombok)\n    testAnnotationProcessor(libs.lombok)\n  }\n\n  tasks.register<Test>(\"e2eTest\") {\n    description = \"Runs e2e tests.\"\n    group = \"verification\"\n    testClassesDirs = sourceSets[TestFolders.e2e].output.classesDirs\n    classpath = sourceSets[TestFolders.e2e].runtimeClasspath\n\n    useJUnitPlatform()\n    reports {\n      junitXml.required.set(true)\n      html.required.set(true)\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/build.gradle.kts",
    "content": "plugins {\n  alias(libs.plugins.quarkus)\n  id(\"com.trendyol.stove.tracing\") version libs.versions.stove.get()\n  id(\"org.jetbrains.kotlin.plugin.allopen\") version libs.versions.kotlin\n  java\n}\n\nallOpen {\n  annotation(\"jakarta.ws.rs.Path\")\n  annotation(\"jakarta.enterprise.context.ApplicationScoped\")\n  annotation(\"jakarta.persistence.Entity\")\n  annotation(\"io.quarkus.test.junit.QuarkusTest\")\n}\n\nkotlin {\n  compilerOptions {\n    jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21\n    javaParameters = true\n  }\n}\n\ntasks.e2eTest {\n  enabled = runningLocally\n}\n\ndependencies {\n  implementation(enforcedPlatform(libs.quarkus))\n  implementation(libs.quarkus.rest)\n  implementation(libs.quarkus.arc)\n  implementation(libs.quarkus.kotlin)\n  implementation(libs.logback.classic)\n  implementation(libs.slf4j.api)\n  implementation(libs.kotlinx.reactor)\n  implementation(libs.kotlinx.core)\n}\n\ndependencies {\n  testImplementation(stoveLibs.stove)\n  testImplementation(stoveLibs.stoveQuarkus)\n  testImplementation(stoveLibs.stoveCouchbase)\n  testImplementation(stoveLibs.stoveExtensionsKotest)\n  testImplementation(stoveLibs.stoveHttp)\n  testImplementation(stoveLibs.stoveTracing)\n  testImplementation(stoveLibs.stoveWiremock)\n  testImplementation(stoveLibs.stoveKafka)\n}\n\nstoveTracing {\n  serviceName.set(\"quarkus-basic-recipe\")\n  testTaskNames.set(listOf(\"e2eTest\"))\n  otelAgentVersion.set(libs.opentelemetry.instrumentation.annotations.get().version!!)\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/EnglishGreetingService.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\nimport jakarta.enterprise.context.ApplicationScoped;\n\n@ApplicationScoped\npublic class EnglishGreetingService implements GreetingService {\n  @Override\n  public String greet(String name) {\n    return \"Hello, \" + name + \"!\";\n  }\n\n  @Override\n  public String getLanguage() {\n    return \"English\";\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/GreetingResource.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\nimport jakarta.enterprise.inject.Instance;\nimport jakarta.inject.Inject;\nimport jakarta.ws.rs.GET;\nimport jakarta.ws.rs.Path;\nimport jakarta.ws.rs.Produces;\nimport jakarta.ws.rs.core.MediaType;\n\n@Path(\"/hello\")\npublic class GreetingResource {\n\n  @Inject\n  HelloService helloService;\n\n  // Inject all GreetingService implementations to ensure they're registered\n  @Inject\n  Instance<GreetingService> greetingServices;\n\n  // Inject repository to prevent dead code elimination\n  @Inject\n  ItemRepository itemRepository;\n\n  @GET\n  @Produces(MediaType.TEXT_PLAIN)\n  public String hello() {\n    return helloService.hello();\n  }\n\n  @GET\n  @Path(\"/greetings\")\n  @Produces(MediaType.TEXT_PLAIN)\n  public String greetings() {\n    StringBuilder sb = new StringBuilder();\n    for (GreetingService gs : greetingServices) {\n      sb.append(gs.getLanguage()).append(\": \").append(gs.greet(\"World\")).append(\"\\n\");\n    }\n    return sb.toString();\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/GreetingService.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\n/** Interface for greeting services - demonstrates multiple implementations pattern. */\npublic interface GreetingService {\n  String greet(String name);\n\n  String getLanguage();\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/HelloService.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\n/**\n * Interface for HelloService - using interfaces is a CDI best practice and enables type-safe\n * testing through dynamic proxies.\n */\npublic interface HelloService {\n  String hello();\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/HelloServiceImpl.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\nimport jakarta.inject.Singleton;\n\n@Singleton\npublic class HelloServiceImpl implements HelloService {\n  @Override\n  public String hello() {\n    return \"Hello from Quarkus Service\";\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/InMemoryItemRepository.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\nimport jakarta.enterprise.context.ApplicationScoped;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.ConcurrentHashMap;\n\n@ApplicationScoped\npublic class InMemoryItemRepository implements ItemRepository {\n\n  private final Map<String, String> items = new ConcurrentHashMap<>();\n\n  @Override\n  public void add(String id, String name) {\n    items.put(id, name);\n  }\n\n  @Override\n  public void addItem(Item item) {\n    items.put(item.getId(), item.getName());\n  }\n\n  @Override\n  public String getById(String id) {\n    return items.get(id);\n  }\n\n  @Override\n  public Item getItemById(String id) {\n    String name = items.get(id);\n    return name != null ? new Item(id, name) : null;\n  }\n\n  @Override\n  public List<String> getAllIds() {\n    return new ArrayList<>(items.keySet());\n  }\n\n  @Override\n  public void clear() {\n    items.clear();\n  }\n\n  @Override\n  public int count() {\n    return items.size();\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/Item.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\n/** Simple item class to demonstrate classloader limitations. */\npublic class Item {\n  private final String id;\n  private final String name;\n\n  public Item(String id, String name) {\n    this.id = id;\n    this.name = name;\n  }\n\n  public String getId() {\n    return id;\n  }\n\n  public String getName() {\n    return name;\n  }\n\n  @Override\n  public String toString() {\n    return \"Item{id='\" + id + \"', name='\" + name + \"'}\";\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/ItemRepository.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\nimport java.util.List;\n\n/** Simple repository interface for testing cross-classloader interactions. */\npublic interface ItemRepository {\n  void add(String id, String name);\n\n  void addItem(Item item); // Takes complex object - will fail across classloaders!\n\n  String getById(String id);\n\n  Item getItemById(String id); // Returns complex object\n\n  List<String> getAllIds();\n\n  void clear();\n\n  int count();\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/QuarkusMainApp.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\nimport io.quarkus.runtime.Quarkus;\nimport io.quarkus.runtime.annotations.QuarkusMain;\n\n@QuarkusMain\npublic class QuarkusMainApp {\n\n  public static void main(String[] args) {\n    Quarkus.run(args);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/SpanishGreetingService.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\nimport jakarta.enterprise.context.ApplicationScoped;\n\n@ApplicationScoped\npublic class SpanishGreetingService implements GreetingService {\n  @Override\n  public String greet(String name) {\n    return \"¡Hola, \" + name + \"!\";\n  }\n\n  @Override\n  public String getLanguage() {\n    return \"Spanish\";\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/StoveStartupSignal.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\nimport io.quarkus.runtime.ShutdownEvent;\nimport io.quarkus.runtime.StartupEvent;\nimport jakarta.enterprise.context.ApplicationScoped;\nimport jakarta.enterprise.event.Observes;\n\n@ApplicationScoped\npublic class StoveStartupSignal {\n\n  public static final String READY_PROPERTY = \"stove.quarkus.ready\";\n\n  void onStart(@Observes StartupEvent event) {\n    System.setProperty(READY_PROPERTY, \"true\");\n  }\n\n  void onStop(@Observes ShutdownEvent event) {\n    System.clearProperty(READY_PROPERTY);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/java/com/trendyol/stove/recipes/quarkus/TurkishGreetingService.java",
    "content": "package com.trendyol.stove.recipes.quarkus;\n\nimport jakarta.enterprise.context.ApplicationScoped;\n\n@ApplicationScoped\npublic class TurkishGreetingService implements GreetingService {\n  @Override\n  public String greet(String name) {\n    return \"Merhaba, \" + name + \"!\";\n  }\n\n  @Override\n  public String getLanguage() {\n    return \"Turkish\";\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/main/resources/application.properties",
    "content": "quarkus.profile=prod\nquarkus.config.profile.parent=prod\nquarkus.live-reload.enabled=false\nquarkus.http.port=8040\nquarkus.virtual-threads.enabled=true\nquarkus.banner.enabled=false\nquarkus.devservices.enabled=false\nquarkus.naming.enable-jndi=true\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/test-e2e/kotlin/com/trendyol/stove/recipes/quarkus/e2e/setup/README.md",
    "content": "# Quarkus Recipe Setup\n\nThis package provides Stove integration for the Quarkus recipe without relying on\nQuarkus bean bridging.\n\n## What It Does\n\n- keeps the public `quarkus(runner, withParameters)` DSL unchanged\n- starts Quarkus by calling the provided `main` runner on a background thread\n- waits for an explicit Quarkus startup signal before tests run\n- shuts Quarkus down cleanly after the project\n- remains compatible with Stove tracing and failure reporting\n\n## Important Files\n\n| File | Purpose |\n|------|---------|\n| `StoveConfig.kt` | Kotest project config and Stove systems |\n| `QuarkusSystem.kt` | `quarkus()` DSL and direct-main launcher |\n| `IndexTests.kt` | HTTP smoke test and tracing smoke test |\n\n## Usage\n\n```kotlin\nquarkus(\n  runner = { params ->\n    QuarkusMainApp.main(params)\n  },\n  withParameters = listOf(\n    \"quarkus.http.port=8040\"\n  )\n)\n```\n\nThe DSL shape stays the same and the recipe invokes `QuarkusMainApp.main(...)`\nfrom a dedicated launcher thread. Quarkus readiness is based on an application\nstartup signal, so the launcher can work for both HTTP apps and worker-only apps.\n\n## Tracing\n\nIf `stove-tracing` is present and the `e2eTest` task is configured with the\nOpenTelemetry Java agent, the recipe can collect spans for Quarkus request flow.\n\nThe recipe includes a tracing smoke test that verifies spans are emitted for a real\nHTTP request.\n\n## Non-Goals\n\n- No `using<T>` bridge support for Quarkus beans\n- No cross-classloader bean access\n- No Quarkus-specific DI adapter in this recipe\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/test-e2e/kotlin/com/trendyol/stove/recipes/quarkus/e2e/setup/StoveConfig.kt",
    "content": "package com.trendyol.stove.recipes.quarkus.e2e.setup\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.quarkus.quarkus\nimport com.trendyol.stove.recipes.quarkus.QuarkusMainApp\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.tracing.tracing\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\n\n/**\n * Kotest project configuration that sets up Stove TestSystem for Quarkus e2e tests.\n */\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        tracing {\n          enableSpanReceiver()\n        }\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:8040\"\n          )\n        }\n        quarkus(\n          runner = { params ->\n            QuarkusMainApp.main(params)\n          },\n          withParameters = listOf(\n            \"quarkus.http.port=8040\"\n          )\n        )\n      }.run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/test-e2e/kotlin/com/trendyol/stove/recipes/quarkus/e2e/tests/IndexTests.kt",
    "content": "package com.trendyol.stove.recipes.quarkus.e2e.tests\n\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.tracing.tracing\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass IndexTests :\n  FunSpec({\n\n    test(\"Index page should return 200\") {\n      stove {\n        http {\n          get<String>(\n            \"/hello\",\n            headers = mapOf(\n              \"Content-Type\" to \"text/plain\",\n              \"Accept\" to \"text/plain\"\n            )\n          ) { actual ->\n            actual shouldBe \"Hello from Quarkus Service\"\n          }\n        }\n      }\n    }\n\n    test(\"tracing should capture quarkus request flow\") {\n      stove {\n        http {\n          get<String>(\n            \"/hello\",\n            headers = mapOf(\n              \"Content-Type\" to \"text/plain\",\n              \"Accept\" to \"text/plain\"\n            )\n          ) { actual ->\n            actual shouldBe \"Hello from Quarkus Service\"\n          }\n        }\n\n        tracing {\n          val spans = waitForSpans(expectedCount = 2, timeoutMs = 10_000)\n\n          spans.isNotEmpty() shouldBe true\n          spanCountShouldBeAtLeast(2)\n          spans.any { span ->\n            span.operationName.contains(\"/hello\") ||\n              span.attributes.values.any { value -> value.contains(\"/hello\") }\n          } shouldBe true\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/test-e2e/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.recipes.quarkus.e2e.setup.StoveConfig\n"
  },
  {
    "path": "recipes/jvm/java-recipes/quarkus-basic-recipe/src/test-e2e/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"ERROR\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/build.gradle.kts",
    "content": "plugins {\n  alias(libs.plugins.spring.boot)\n  alias(libs.plugins.spring.plugin)\n  alias(libs.plugins.spring.dependencyManagement)\n}\n\ndependencies {\n  implementation(libs.spring.boot.webflux)\n  implementation(libs.spring.boot.autoconfigure)\n  implementation(libs.spring.boot.kafka)\n  implementation(libs.spring.boot.data.r2dbc)\n  implementation(libs.postgresql.r2dbc)\n  implementation(libs.postgresql)\n  implementation(projects.shared.application)\n  implementation(rootProject.projects.shared.domain)\n  annotationProcessor(libs.spring.boot.annotationProcessor)\n}\n\ndependencies {\n  testImplementation(stoveLibs.stove)\n  testImplementation(stoveLibs.stovePostgres)\n  testImplementation(stoveLibs.stoveHttp)\n  testImplementation(stoveLibs.stoveWiremock)\n  testImplementation(stoveLibs.stoveKafka)\n  testImplementation(stoveLibs.stoveSpring)\n  testImplementation(libs.testcontainers.kafka)\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/ExampleSpringBootApp.java",
    "content": "package com.trendyol.stove.examples.java.spring;\n\nimport java.util.function.Consumer;\nimport org.springframework.boot.SpringApplication;\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\nimport org.springframework.context.ConfigurableApplicationContext;\nimport org.springframework.kafka.annotation.EnableKafka;\n\n@SpringBootApplication\n@EnableKafka\npublic class ExampleSpringBootApp {\n  public static void main(String[] args) {\n    run(args, application -> {});\n  }\n\n  public static ConfigurableApplicationContext run(\n      String[] args, Consumer<SpringApplication> applicationConsumer) {\n    SpringApplication application = new SpringApplication(ExampleSpringBootApp.class);\n    applicationConsumer.accept(application);\n    return application.run(args);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/application/external/category/CategoryApiSpringConfiguration.java",
    "content": "package com.trendyol.stove.examples.java.spring.application.external.category;\n\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiConfiguration;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\n\n@ConfigurationProperties(prefix = \"external-apis.category\")\npublic class CategoryApiSpringConfiguration extends CategoryApiConfiguration {}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/application/external/category/CategoryHttpApi.java",
    "content": "package com.trendyol.stove.examples.java.spring.application.external.category;\n\nimport com.trendyol.stove.recipes.shared.application.BusinessException;\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse;\nimport reactor.core.publisher.Mono;\n\npublic interface CategoryHttpApi {\n  Mono<CategoryApiResponse> getCategoryById(int id) throws BusinessException;\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/application/external/category/CategoryHttpApiConfiguration.java",
    "content": "package com.trendyol.stove.examples.java.spring.application.external.category;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.trendyol.stove.recipes.shared.application.ExternalApiConfiguration;\nimport io.netty.channel.ChannelOption;\nimport io.netty.handler.timeout.ReadTimeoutHandler;\nimport org.springframework.boot.context.properties.EnableConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.http.client.reactive.ReactorClientHttpConnector;\nimport org.springframework.http.codec.json.Jackson2JsonDecoder;\nimport org.springframework.http.codec.json.Jackson2JsonEncoder;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.netty.http.client.HttpClient;\n\n@Configuration\n@EnableConfigurationProperties(CategoryApiSpringConfiguration.class)\npublic class CategoryHttpApiConfiguration {\n\n  @Bean\n  public CategoryHttpApi categoryHttpApi(\n      CategoryApiSpringConfiguration categoryApiConfiguration, ObjectMapper objectMapper) {\n    return new CategoryHttpApiImpl(webClient(categoryApiConfiguration, objectMapper));\n  }\n\n  private WebClient webClient(\n      ExternalApiConfiguration categoryApiConfiguration, ObjectMapper objectMapper) {\n    var client = HttpClient.create()\n        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, categoryApiConfiguration.getTimeout())\n        .doOnConnected(connection -> connection.addHandlerLast(\n            new ReadTimeoutHandler(categoryApiConfiguration.getTimeout())));\n    return WebClient.builder()\n        .baseUrl(categoryApiConfiguration.getUrl())\n        .clientConnector(new ReactorClientHttpConnector(client))\n        .defaultRequest(r -> r.accept(MediaType.APPLICATION_JSON))\n        .codecs(configurer -> {\n          configurer\n              .defaultCodecs()\n              .jackson2JsonEncoder(\n                  new Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON));\n          configurer\n              .defaultCodecs()\n              .jackson2JsonDecoder(\n                  new Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON));\n        })\n        .build();\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/application/external/category/CategoryHttpApiImpl.java",
    "content": "package com.trendyol.stove.examples.java.spring.application.external.category;\n\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse;\nimport org.springframework.web.reactive.function.client.WebClient;\nimport reactor.core.publisher.Mono;\n\npublic class CategoryHttpApiImpl implements CategoryHttpApi {\n  private final WebClient categoryWebClient;\n\n  public CategoryHttpApiImpl(WebClient categoryWebClient) {\n    this.categoryWebClient = categoryWebClient;\n  }\n\n  @Override\n  public Mono<CategoryApiResponse> getCategoryById(int id) {\n    return categoryWebClient\n        .get()\n        .uri(\"/categories/{id}\", id)\n        .retrieve()\n        .bodyToMono(CategoryApiResponse.class);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/application/product/command/ProductApplicationService.java",
    "content": "package com.trendyol.stove.examples.java.spring.application.product.command;\n\nimport com.trendyol.stove.examples.domain.product.Product;\nimport com.trendyol.stove.examples.java.spring.application.external.category.CategoryHttpApi;\nimport com.trendyol.stove.examples.java.spring.domain.ProductReactiveRepository;\nimport com.trendyol.stove.recipes.shared.application.BusinessException;\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\n\n@Component\npublic class ProductApplicationService {\n  private final ProductReactiveRepository productRepository;\n  private final CategoryHttpApi categoryHttpApi;\n\n  public ProductApplicationService(\n      ProductReactiveRepository productRepository, CategoryHttpApi categoryHttpApi) {\n    this.productRepository = productRepository;\n    this.categoryHttpApi = categoryHttpApi;\n  }\n\n  public Mono<Void> create(String name, double price, int categoryId) throws BusinessException {\n    return categoryHttpApi\n        .getCategoryById(categoryId)\n        .filter(CategoryApiResponse::isActive)\n        .switchIfEmpty(Mono.error(new BusinessException(\"Category is not active\")))\n        .flatMap(categoryApiResponse -> {\n          var product = Product.create(name, price, categoryApiResponse.id());\n          return productRepository.save(product);\n        });\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/application/product/messaging/ProductEventHandlerListener.java",
    "content": "package com.trendyol.stove.examples.java.spring.application.product.messaging;\n\nimport org.apache.kafka.clients.consumer.ConsumerRecord;\nimport org.springframework.kafka.annotation.KafkaListener;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class ProductEventHandlerListener {\n\n  @KafkaListener(topics = {\"${kafka.topics.product.name}\"})\n  public void listen(ConsumerRecord<?, ?> event) {\n    System.out.println(\"Received event: \" + event);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/domain/ProductReactiveRepository.java",
    "content": "package com.trendyol.stove.examples.java.spring.domain;\n\nimport com.trendyol.stove.examples.domain.product.Product;\nimport reactor.core.publisher.Mono;\n\npublic interface ProductReactiveRepository {\n  Mono<Product> findById(String id);\n\n  Mono<Void> save(Product product);\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/boilerplate/http/ControllerAdvice.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.boilerplate.http;\n\nimport com.trendyol.stove.recipes.shared.application.BusinessException;\nimport com.trendyol.stove.recipes.shared.application.ErrorResponse;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.RestControllerAdvice;\n\n@RestControllerAdvice\npublic class ControllerAdvice {\n\n  private final Logger logger = LoggerFactory.getLogger(ControllerAdvice.class);\n\n  @ExceptionHandler(BusinessException.class)\n  public ResponseEntity<?> handleException(BusinessException e) {\n    logger.error(\"Business exception occurred\", e);\n    return ResponseEntity.status(HttpStatus.CONFLICT)\n        .body(new ErrorResponse(e.getMessage(), \"409\"));\n  }\n\n  @ExceptionHandler(Exception.class)\n  public ResponseEntity<?> handleException(Exception e) {\n    logger.error(\"Exception occurred\", e);\n    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)\n        .body(new ErrorResponse(e.getMessage(), \"500\"));\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/boilerplate/kafka/KafkaBeanConfiguration.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.boilerplate.kafka;\n\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport java.time.Duration;\nimport java.util.Map;\nimport java.util.Properties;\nimport org.apache.kafka.clients.consumer.ConsumerConfig;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.apache.kafka.common.serialization.StringDeserializer;\nimport org.apache.kafka.common.serialization.StringSerializer;\nimport org.slf4j.Logger;\nimport org.springframework.boot.context.properties.ConfigurationProperties;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;\nimport org.springframework.kafka.core.KafkaTemplate;\nimport org.springframework.kafka.listener.DefaultErrorHandler;\nimport org.springframework.kafka.support.serializer.ErrorHandlingDeserializer;\nimport org.springframework.kafka.support.serializer.JsonDeserializer;\nimport org.springframework.kafka.support.serializer.JsonSerializer;\nimport org.springframework.util.backoff.FixedBackOff;\n\n@Configuration\npublic class KafkaBeanConfiguration {\n  private final Logger logger = org.slf4j.LoggerFactory.getLogger(KafkaBeanConfiguration.class);\n\n  @Bean\n  @ConfigurationProperties(prefix = \"kafka\")\n  public KafkaConfiguration kafkaConfiguration() {\n    return new KafkaConfiguration();\n  }\n\n  @Bean\n  public TopicResolver topicResolver(KafkaConfiguration kafkaConfiguration) {\n    return new TopicResolver(kafkaConfiguration);\n  }\n\n  @Bean\n  public Properties consumerProperties(KafkaConfiguration kafkaConfiguration) {\n    Properties properties = new Properties();\n    properties.put(\n        ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaConfiguration.getBootstrapServers());\n    properties.put(ConsumerConfig.GROUP_ID_CONFIG, kafkaConfiguration.getGroupId());\n    properties.put(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, (int)\n        Duration.ofSeconds(kafkaConfiguration.getRequestTimeoutSeconds()).toMillis());\n    properties.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, (int)\n        Duration.ofSeconds(kafkaConfiguration.getHeartbeatIntervalSeconds()).toMillis());\n    properties.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, (int)\n        Duration.ofSeconds(kafkaConfiguration.getSessionTimeoutSeconds()).toMillis());\n    properties.put(\n        ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG, kafkaConfiguration.isAutoCreateTopics());\n    properties.put(\n        ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, kafkaConfiguration.getAutoOffsetReset());\n\n    properties.put(\n        ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());\n    properties.put(\n        ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class.getName());\n    properties.put(\n        ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class.getName());\n    properties.put(JsonDeserializer.TRUSTED_PACKAGES, \"*\");\n    properties.put(JsonDeserializer.VALUE_DEFAULT_TYPE, Object.class.getName());\n\n    properties.put(\n        ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, kafkaConfiguration.flattenInterceptorClasses());\n    logger.info(\"Kafka consumer properties: {}\", properties);\n    return properties;\n  }\n\n  @Bean\n  public Properties producerProperties(KafkaConfiguration kafkaConfiguration) {\n    Properties properties = new Properties();\n    properties.put(\n        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaConfiguration.getBootstrapServers());\n    properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());\n    properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class.getName());\n    properties.put(\n        ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, kafkaConfiguration.flattenInterceptorClasses());\n    return properties;\n  }\n\n  @Bean\n  public KafkaTemplate<?, ?> kafkaTemplate(KafkaConfiguration kafkaConfiguration) {\n    return new KafkaTemplate<>(new org.springframework.kafka.core.DefaultKafkaProducerFactory<>(\n        toMap(producerProperties(kafkaConfiguration))));\n  }\n\n  @Bean\n  public ConcurrentKafkaListenerContainerFactory<?, ?> kafkaListenerContainerFactory(\n      KafkaConfiguration kafkaConfiguration, ObjectMapper objectMapper) {\n    ConcurrentKafkaListenerContainerFactory<?, ?> factory =\n        new ConcurrentKafkaListenerContainerFactory<>();\n    factory.setCommonErrorHandler(new DefaultErrorHandler(new FixedBackOff(10, 1)));\n    factory.setRecordMessageConverter(\n        new org.springframework.kafka.support.converter.JsonMessageConverter(objectMapper));\n    factory.setConsumerFactory(new org.springframework.kafka.core.DefaultKafkaConsumerFactory<>(\n        toMap(consumerProperties(kafkaConfiguration))));\n    return factory;\n  }\n\n  private Map<String, Object> toMap(Properties properties) {\n    return properties.entrySet().stream()\n        .collect(\n            java.util.stream.Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue));\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/boilerplate/kafka/KafkaConfiguration.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.boilerplate.kafka;\n\nimport java.util.Map;\nimport lombok.Data;\n\npublic @Data class KafkaConfiguration {\n  String bootstrapServers;\n  String groupId;\n  long requestTimeoutSeconds = 30;\n  long heartbeatIntervalSeconds = 3;\n  long sessionTimeoutSeconds = 10;\n  boolean autoCreateTopics = true;\n  String autoOffsetReset = \"earliest\";\n  String[] interceptorClasses;\n  Map<String, Topic> topics;\n\n  public String flattenInterceptorClasses() {\n    return String.join(\",\", interceptorClasses);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/boilerplate/kafka/KafkaDomainEventPublisher.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.boilerplate.kafka;\n\nimport com.trendyol.stove.examples.domain.ddd.AggregateRoot;\nimport com.trendyol.stove.examples.domain.ddd.EventPublisher;\nimport java.util.stream.Stream;\nimport org.apache.kafka.clients.producer.ProducerRecord;\nimport org.slf4j.Logger;\nimport org.springframework.kafka.core.KafkaTemplate;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class KafkaDomainEventPublisher implements EventPublisher {\n  private final KafkaTemplate<String, Object> template;\n  private final TopicResolver topicResolver;\n  private final Logger logger = org.slf4j.LoggerFactory.getLogger(KafkaDomainEventPublisher.class);\n\n  public KafkaDomainEventPublisher(\n      KafkaTemplate<String, Object> template, TopicResolver topicResolver) {\n    this.template = template;\n    this.topicResolver = topicResolver;\n  }\n\n  @Override\n  public <TId> void publishFor(AggregateRoot<TId> aggregateRoot) {\n    mapEventsToProducerRecords(aggregateRoot).forEach(template::send);\n  }\n\n  private <TId> Stream<ProducerRecord<String, Object>> mapEventsToProducerRecords(\n      AggregateRoot<TId> aggregateRoot) {\n    return aggregateRoot.domainEvents().stream().map(event -> {\n      var topic = topicResolver.resolve(aggregateRoot.getAggregateName());\n      logger.info(\"Publishing event {} to topic {}\", event, topic.getName());\n      return new ProducerRecord<>(topic.getName(), aggregateRoot.getIdAsString(), event);\n    });\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/boilerplate/kafka/Topic.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.boilerplate.kafka;\n\nimport lombok.Data;\n\npublic @Data class Topic {\n  String name;\n  String retry;\n  String deadLetter;\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/boilerplate/kafka/TopicResolver.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.boilerplate.kafka;\n\npublic class TopicResolver {\n  private final KafkaConfiguration kafkaConfiguration;\n\n  public TopicResolver(KafkaConfiguration kafkaConfiguration) {\n    this.kafkaConfiguration = kafkaConfiguration;\n  }\n\n  public Topic resolve(String aggregateName) {\n    return kafkaConfiguration.getTopics().get(aggregateName);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/boilerplate/postgres/PostgresConfiguration.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.boilerplate.postgres;\n\nimport io.r2dbc.spi.ConnectionFactory;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.r2dbc.core.DatabaseClient;\n\n@Configuration\npublic class PostgresConfiguration {\n\n  @Bean\n  public DatabaseClient databaseClient(ConnectionFactory connectionFactory) {\n    return DatabaseClient.create(connectionFactory);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/boilerplate/serialization/JacksonConfiguration.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.boilerplate.serialization;\n\nimport com.fasterxml.jackson.databind.DeserializationFeature;\nimport com.fasterxml.jackson.databind.ObjectMapper;\nimport com.fasterxml.jackson.databind.json.JsonMapper;\nimport org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.context.annotation.Primary;\n\n@Configuration\npublic class JacksonConfiguration {\n\n  public static ObjectMapper defaultObjectMapper() {\n    return JsonMapper.builder()\n        .disable(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)\n        .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)\n        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n        .disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)\n        .disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)\n        .findAndAddModules()\n        .build()\n        .findAndRegisterModules();\n  }\n\n  @Bean\n  @Primary\n  public ObjectMapper objectMapper() {\n    return defaultObjectMapper();\n  }\n\n  @Bean\n  public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {\n    return builder -> builder.configure(defaultObjectMapper());\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/components/index/IndexController.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.components.index;\n\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\n@RequestMapping(\"/\")\npublic class IndexController {\n\n  @RequestMapping\n  public String index() {\n    return \"Hello, World!\";\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/components/product/api/ProductController.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.components.product.api;\n\nimport com.trendyol.stove.examples.java.spring.application.product.command.ProductApplicationService;\nimport com.trendyol.stove.recipes.shared.application.BusinessException;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.PostMapping;\nimport org.springframework.web.bind.annotation.RequestBody;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\nimport reactor.core.publisher.Mono;\n\n@RestController\n@RequestMapping(\"/products\")\npublic class ProductController {\n  private final ProductApplicationService productService;\n\n  public ProductController(ProductApplicationService productService) {\n    this.productService = productService;\n  }\n\n  @PostMapping\n  public Mono<ResponseEntity<?>> createProduct(@RequestBody ProductCreateRequest request)\n      throws BusinessException {\n    return productService\n        .create(request.getName(), request.getPrice(), request.getCategoryId())\n        .onErrorContinue((throwable, o) -> ResponseEntity.badRequest().build())\n        .then(Mono.fromCallable(() -> ResponseEntity.ok().build()));\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/components/product/api/ProductCreateRequest.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.components.product.api;\n\nimport lombok.Data;\n\n@Data\npublic class ProductCreateRequest {\n  String name;\n  double price;\n  int categoryId;\n\n  public ProductCreateRequest(String name, double price, int categoryId) {\n    this.name = name;\n    this.price = price;\n    this.categoryId = categoryId;\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/java/com/trendyol/stove/examples/java/spring/infra/components/product/persistency/JdbcProductRepository.java",
    "content": "package com.trendyol.stove.examples.java.spring.infra.components.product.persistency;\n\nimport com.trendyol.stove.examples.domain.ddd.EventPublisher;\nimport com.trendyol.stove.examples.domain.product.Product;\nimport com.trendyol.stove.examples.java.spring.domain.ProductReactiveRepository;\nimport java.time.Instant;\nimport org.springframework.r2dbc.core.DatabaseClient;\nimport org.springframework.stereotype.Component;\nimport reactor.core.publisher.Mono;\n\n@Component\npublic class JdbcProductRepository implements ProductReactiveRepository {\n  private final DatabaseClient databaseClient;\n  private final EventPublisher eventPublisher;\n\n  public JdbcProductRepository(DatabaseClient databaseClient, EventPublisher eventPublisher) {\n    this.databaseClient = databaseClient;\n    this.eventPublisher = eventPublisher;\n  }\n\n  @Override\n  public Mono<Product> findById(String id) {\n    return databaseClient\n        .sql(\"SELECT * FROM products WHERE id = :id\")\n        .bind(\"id\", id)\n        .map(row -> {\n          String productId = row.get(\"id\", String.class);\n          String name = row.get(\"name\", String.class);\n          Double price = row.get(\"price\", Double.class);\n          Integer categoryId = row.get(\"category_id\", Integer.class);\n          Long version = row.get(\"version\", Long.class);\n\n          return Product.fromPersistency(\n              productId, name, price, categoryId, version != null ? version : 0L);\n        })\n        .one();\n  }\n\n  public Mono<Void> save(Product product) {\n    return databaseClient\n        .sql(\"\"\"\n                        INSERT INTO products (id, name, price, category_id, created_date, version)\n                        VALUES (:id, :name, :price, :categoryId, :createdDate, :version)\n                        ON CONFLICT (id) DO UPDATE SET\n                          name = EXCLUDED.name,\n                          price = EXCLUDED.price,\n                          category_id = EXCLUDED.category_id,\n                          version = EXCLUDED.version\n                        \"\"\")\n        .bind(\"id\", product.getIdAsString())\n        .bind(\"name\", product.getName())\n        .bind(\"price\", product.getPrice())\n        .bind(\"categoryId\", product.getCategoryId())\n        .bind(\"createdDate\", Instant.now())\n        .bind(\"version\", product.getVersion())\n        .fetch()\n        .rowsUpdated()\n        .doOnSuccess(result -> eventPublisher.publishFor(product))\n        .then();\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/main/resources/application.yml",
    "content": "server:\n  port: 8080\nspring:\n  r2dbc:\n    url: r2dbc:postgresql://localhost:5432/stove\n    username: postgres\n    password: postgres\nkafka:\n  bootstrap-servers: localhost:9092\n  group-id: stove-java-spring-boot\n  heartbeat-interval-seconds: 2\n  request-timeout-seconds: 30\n  session-timeout-seconds: 10\n  auto-create-topics: true\n  auto-offset-reset: earliest\n  interceptor-classes: [ ]\n  topics:\n    product:\n      name: ${kafka.group-id}.product\n      retry: ${kafka.topic.product}.retry\n      dead-letter: ${kafka.topic.product}.error\nexternal-apis:\n  category:\n    url: http://localhost:9091\n    timeout: 30\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/example/java/spring/e2e/setup/CreateProductsTableMigration.kt",
    "content": "package com.trendyol.stove.example.java.spring.e2e.setup\n\nimport com.trendyol.stove.database.migrations.DatabaseMigration\nimport com.trendyol.stove.postgres.PostgresSqlMigrationContext\nimport org.slf4j.Logger\nimport org.slf4j.LoggerFactory\n\nclass CreateProductsTableMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n  private val logger: Logger = LoggerFactory.getLogger(CreateProductsTableMigration::class.java)\n\n  override val order: Int = 1\n\n  override suspend fun execute(connection: PostgresSqlMigrationContext) {\n    logger.info(\"Creating products table\")\n    connection.operations.execute(\n      \"\"\"\n      DROP TABLE IF EXISTS products;\n      CREATE TABLE IF NOT EXISTS products (\n        id VARCHAR(255) PRIMARY KEY,\n        name VARCHAR(255) NOT NULL,\n        price DOUBLE PRECISION NOT NULL,\n        category_id INTEGER NOT NULL,\n        created_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,\n        version BIGINT NOT NULL DEFAULT 0\n      );\n      \"\"\".trimIndent()\n    )\n    logger.info(\"Products table created\")\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/example/java/spring/e2e/setup/Stove.kt",
    "content": "package com.trendyol.stove.example.java.spring.e2e.setup\n\nimport com.trendyol.stove.examples.java.spring.ExampleSpringBootApp\nimport com.trendyol.stove.examples.java.spring.infra.boilerplate.serialization.JacksonConfiguration\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.postgres.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.ktor.serialization.jackson.*\nimport org.springframework.kafka.support.serializer.JsonSerializer\n\nclass Stove : AbstractProjectConfig() {\n  init {\n    stoveKafkaBridgePortDefault = \"50052\"\n    System.setProperty(STOVE_KAFKA_BRIDGE_PORT, stoveKafkaBridgePortDefault)\n  }\n\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:8080\",\n            contentConverter = JacksonConverter(JacksonConfiguration.defaultObjectMapper())\n          )\n        }\n\n        bridge()\n        wiremock {\n          WireMockSystemOptions(\n            port = 9091,\n            serde = StoveSerde.jackson.anyByteArraySerde(JacksonConfiguration.defaultObjectMapper())\n          )\n        }\n        postgresql {\n          PostgresqlOptions(\n            databaseName = \"stove\",\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"spring.r2dbc.url=r2dbc:postgresql://${cfg.host}:${cfg.port}/stove\",\n                \"spring.r2dbc.username=${cfg.username}\",\n                \"spring.r2dbc.password=${cfg.password}\"\n              )\n            }\n          ).migrations {\n            register<CreateProductsTableMigration>()\n          }\n        }\n\n        kafka {\n          KafkaSystemOptions(\n            serde = StoveSerde.jackson.anyByteArraySerde(JacksonConfiguration.defaultObjectMapper()),\n            valueSerializer = JsonSerializer(JacksonConfiguration.defaultObjectMapper()),\n            containerOptions = KafkaContainerOptions(tag = \"8.0.3\") {\n              withStartupAttempts(3)\n            },\n            configureExposedConfiguration = {\n              listOf(\n                \"kafka.bootstrap-servers=${it.bootstrapServers}\",\n                \"kafka.interceptor-classes=${it.interceptorClass}\"\n              )\n            }\n          )\n        }\n        springBoot(\n          runner = { parameters ->\n            ExampleSpringBootApp.run(parameters) {\n            }\n          },\n          withParameters = listOf()\n        )\n      }.run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/example/java/spring/e2e/setup/TestData.kt",
    "content": "package com.trendyol.stove.example.java.spring.e2e.setup\n\nobject TestData {\n  object Random {\n    fun positiveInt() = kotlin.random.Random.nextInt(1, Int.MAX_VALUE)\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/example/java/spring/e2e/tests/IndexTests.kt",
    "content": "package com.trendyol.stove.example.java.spring.e2e.tests\n\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass IndexTests :\n  FunSpec({\n    test(\"Index page should be accessible\") {\n      stove {\n        http {\n          get<String>(\"/\") { actual ->\n            actual shouldBe \"Hello, World!\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/example/java/spring/e2e/tests/product/CreateTests.kt",
    "content": "package com.trendyol.stove.example.java.spring.e2e.tests.product\n\nimport arrow.core.some\nimport com.trendyol.stove.example.java.spring.e2e.setup.TestData\nimport com.trendyol.stove.examples.domain.product.Product\nimport com.trendyol.stove.examples.domain.product.events.ProductCreatedEvent\nimport com.trendyol.stove.examples.java.spring.domain.ProductReactiveRepository\nimport com.trendyol.stove.examples.java.spring.infra.components.product.api.ProductCreateRequest\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport org.springframework.http.HttpStatus\nimport java.util.*\nimport kotlin.time.Duration.Companion.seconds\n\nclass CreateTests :\n  FunSpec({\n    test(\"product can be created with valid category\") {\n      stove {\n        val productName = TestData.Random.positiveInt().toString()\n        val productId = UUID.nameUUIDFromBytes(productName.toByteArray())\n\n        val categoryApiResponse = CategoryApiResponse(\n          TestData.Random.positiveInt(),\n          \"category-name\",\n          true\n        )\n\n        wiremock {\n          mockGet(\n            url = \"/categories/${categoryApiResponse.id}\",\n            statusCode = 200,\n            responseBody = categoryApiResponse.some()\n          )\n        }\n\n        http {\n          val req = ProductCreateRequest(productName, 100.0, categoryApiResponse.id)\n          postAndExpectBody<Any>(\"/products\", body = req.some()) { actual ->\n            actual.status shouldBe 200\n          }\n        }\n\n        postgresql {\n          shouldQuery<Product>(\n            \"SELECT * FROM products WHERE id = '$productId'\",\n            mapper = { row ->\n              Product.fromPersistency(\n                row.string(\"id\"),\n                row.string(\"name\"),\n                row.double(\"price\"),\n                row.int(\"category_id\"),\n                row.long(\"version\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 1\n            products.first().id shouldBe productId.toString()\n            products.first().name shouldBe productName\n            products.first().price shouldBe 100.0\n          }\n        }\n\n        using<ProductReactiveRepository> {\n          val product = findById(productId.toString()).toFuture().get()\n          product.name shouldBe productName\n          product.price shouldBe 100.0\n          product.categoryId shouldBe categoryApiResponse.id\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent>(10.seconds) {\n            actual.price == 100.0 && actual.name == productName\n          }\n\n          shouldBeConsumed<ProductCreatedEvent> {\n            actual.price == 100.0 && actual.name == productName\n          }\n        }\n      }\n    }\n\n    test(\"when category is not active, product creation should fail\") {\n      stove {\n        val productName = TestData.Random.positiveInt().toString()\n        val productId = UUID.nameUUIDFromBytes(productName.toByteArray())\n        val categoryApiResponse = CategoryApiResponse(\n          TestData.Random.positiveInt(),\n          \"category-name\",\n          false\n        )\n\n        wiremock {\n          mockGet(\n            url = \"/categories/${categoryApiResponse.id}\",\n            statusCode = 200\n          )\n        }\n\n        http {\n          val req = ProductCreateRequest(productName, 100.0, categoryApiResponse.id)\n          postAndExpectBody<Any>(\"/products\", body = req.some()) { actual ->\n            actual.status shouldBe HttpStatus.CONFLICT.value()\n          }\n        }\n\n        postgresql {\n          shouldQuery<Product>(\n            \"SELECT * FROM products WHERE id = '$productId'\",\n            mapper = { row ->\n              Product.fromPersistency(\n                row.string(\"id\"),\n                row.string(\"name\"),\n                row.double(\"price\"),\n                row.int(\"category_id\"),\n                row.long(\"version\")\n              )\n            }\n          ) { products ->\n            products.size shouldBe 0\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/test-e2e/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.example.java.spring.e2e.setup.Stove\n"
  },
  {
    "path": "recipes/jvm/java-recipes/spring-boot-postgres-recipe/src/test-e2e/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/build.gradle.kts",
    "content": "plugins {\n  kotlin(\"jvm\") version libs.versions.kotlin\n  idea\n}\n\nsubprojects {\n  apply {\n    plugin(\"kotlin\")\n    plugin(\"idea\")\n  }\n\n  dependencies {\n    implementation(rootProject.projects.shared.domain)\n  }\n\n  sourceSets {\n    create(TestFolders.e2e) {\n      kotlin {\n        compileClasspath += sourceSets.main.get().output\n        runtimeClasspath += sourceSets.main.get().output\n        srcDirs(\"src/test-e2e/kotlin\")\n      }\n    }\n  }\n\n  val testE2eImplementation by configurations.getting {\n    extendsFrom(configurations.testImplementation.get())\n  }\n  configurations[\"testE2eRuntimeOnly\"].extendsFrom(configurations.runtimeOnly.get())\n\n  idea {\n    module {\n      testSources.from(sourceSets[TestFolders.e2e].allSource.sourceDirectories)\n      testResources.from(sourceSets[TestFolders.e2e].resources.sourceDirectories)\n      isDownloadJavadoc = true\n      isDownloadSources = true\n    }\n  }\n  tasks.register<Test>(\"e2eTest\") {\n    description = \"Runs e2e tests.\"\n    group = \"verification\"\n    testClassesDirs = sourceSets[TestFolders.e2e].output.classesDirs\n    classpath = sourceSets[TestFolders.e2e].runtimeClasspath\n\n    useJUnitPlatform()\n    reports {\n      junitXml.required.set(true)\n      html.required.set(true)\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/build.gradle.kts",
    "content": "dependencies {\n  implementation(projects.shared.domain)\n  implementation(projects.shared.application)\n  implementation(libs.ktor.server.core.jvm)\n  implementation(libs.ktor.server.netty.jvm)\n  implementation(libs.ktor.server.content.negotiation.jvm)\n  implementation(libs.ktor.server.statuspages)\n  implementation(libs.ktor.server.callLogging)\n  implementation(libs.ktor.server.callId)\n  implementation(libs.ktor.server.conditionalHeaders)\n  implementation(libs.ktor.server.cors)\n  implementation(libs.ktor.server.defaultHeaders)\n  implementation(libs.ktor.server.cachingHeaders)\n  implementation(libs.ktor.server.autoHeadResponse)\n  implementation(libs.ktor.server.config.yml)\n  implementation(libs.ktor.swagger.ui)\n  implementation(libs.ktor.serialization.jackson.json)\n  implementation(libs.koin)\n  implementation(libs.koin.ktor)\n  implementation(libs.slf4j.api)\n  implementation(libs.arrow.core)\n  implementation(libs.hoplite)\n  implementation(libs.hoplite.yaml)\n  implementation(libs.logback.classic)\n  implementation(libs.ktor.client.core)\n  implementation(libs.ktor.client.cio)\n  implementation(libs.ktor.client.plugins.logging)\n  implementation(libs.ktor.client.content.negotiation)\n  implementation(libs.kotlinFpUtil)\n  implementation(libs.kotlin.logging.jvm)\n  implementation(libs.kediatr.koin)\n  implementation(libs.mongodb.kotlin.coroutine)\n  implementation(libs.mongodb.bson.kotlin)\n  implementation(libs.kafkaKotlin)\n}\n\ndependencies {\n  testImplementation(stoveLibs.stove)\n  testImplementation(stoveLibs.stoveMongodb)\n  testImplementation(stoveLibs.stoveHttp)\n  testImplementation(stoveLibs.stoveWiremock)\n  testImplementation(stoveLibs.stoveKafka)\n  testImplementation(stoveLibs.stoveKtor)\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/ExampleStoveKtorApp.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.RecipeAppConfig\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.*\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.http.registerHttpClient\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.*\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kediatr.registerKediatR\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.mongo.configureMongo\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.serialization.*\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.external.registerCategoryExternalHttpApi\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.api.productApi\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.registerProductComponents\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.server.application.*\nimport io.ktor.server.plugins.autohead.*\nimport io.ktor.server.response.*\nimport io.ktor.server.routing.*\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\nimport org.koin.ktor.plugin.Koin\n\nval logger = KotlinLogging.logger(\"Stove Ktor Recipe\")\n\nobject ExampleStoveKtorApp {\n  @JvmStatic\n  fun main(args: Array<String>) {\n    run(args)\n  }\n\n  fun run(args: Array<String>, wait: Boolean = true, configure: org.koin.core.module.Module = module { }): Application {\n    val config = loadConfiguration<RecipeAppConfig>(args)\n    logger.info { \"Starting Ktor application with config: $config\" }\n    return startKtorApplication(config, wait) {\n      appModule(config, configure)\n    }\n  }\n}\n\nfun Application.appModule(\n  config: RecipeAppConfig,\n  overrides: org.koin.core.module.Module = module { }\n) {\n  install(Koin) {\n    allowOverride(true)\n    modules(\n      module {\n        single { config }\n        single { config.externalApis.category }\n      }\n    )\n    registerAppDeps()\n    registerHttpClient()\n    registerKafka(config.kafka)\n    modules(overrides)\n  }\n  configureRouting()\n  configureExceptionHandling()\n  configureContentNegotiation()\n  configureConsumerEngine()\n}\n\nfun KoinApplication.registerAppDeps() {\n  configureMongo()\n  configureJackson()\n  registerKediatR()\n  registerProductComponents()\n  registerCategoryExternalHttpApi()\n}\n\nfun Application.configureRouting() {\n  install(AutoHeadResponse)\n  routing {\n    route(\"/\") {\n      get {\n        call.respondText(\"Hello, World!\")\n      }\n    }\n    productApi()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/RecipeAppConfig.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.external.CategoryApiConfiguration\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.Topic\n\n/**\n * Represents the main configuration\n */\ndata class RecipeAppConfig(\n  val server: ServerConfig,\n  val kafka: KafkaConfiguration,\n  val mongo: MongoConfiguration,\n  val externalApis: ExternalApisConfig\n)\n\ndata class ExternalApisConfig(\n  val category: CategoryApiConfiguration\n)\n\n/**\n * Represents the configuration of the checker.\n */\ndata class ServerConfig(\n  /**\n   * Port of the server.\n   */\n  val port: Int = 8080,\n  /**\n   * Host of the server.\n   */\n  val host: String = \"\",\n  val name: String\n)\n\ndata class MongoConfiguration(\n  val uri: String,\n  val database: String\n)\n\ndata class KafkaConfiguration(\n  val bootstrapServers: String,\n  val groupId: String,\n  val requestTimeoutSeconds: Long = 30,\n  val heartbeatIntervalSeconds: Long = 3,\n  val sessionTimeoutSeconds: Long = 10,\n  val autoCreateTopics: Boolean = true,\n  val autoOffsetReset: String = \"earliest\",\n  val interceptorClasses: List<String>,\n  val topics: Map<String, Topic>\n) {\n  fun flattenInterceptorClasses(): String = interceptorClasses.joinToString(\",\")\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/external/CategoryHttpApi.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application.external\n\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse\n\ninterface CategoryHttpApi {\n  suspend fun getCategory(id: Int): CategoryApiResponse\n}\n\ndata class CategoryApiConfiguration(\n  val url: String,\n  val timeout: Long\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/external/CategoryHttpApiImpl.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application.external\n\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse\nimport io.ktor.client.*\nimport io.ktor.client.call.*\nimport io.ktor.client.plugins.*\nimport io.ktor.client.request.*\nimport io.ktor.http.*\nimport kotlin.time.Duration.Companion.seconds\n\nclass CategoryHttpApiImpl(\n  private val httpClient: HttpClient,\n  private val categoryApiConfiguration: CategoryApiConfiguration\n) : CategoryHttpApi {\n  override suspend fun getCategory(id: Int): CategoryApiResponse = httpClient\n    .get(\"${categoryApiConfiguration.url}/categories/$id\") {\n      accept(ContentType.Application.Json)\n      timeout {\n        requestTimeoutMillis = categoryApiConfiguration.timeout.seconds.inWholeMilliseconds\n        connectTimeoutMillis = categoryApiConfiguration.timeout.seconds.inWholeMilliseconds\n        socketTimeoutMillis = categoryApiConfiguration.timeout.seconds.inWholeMilliseconds\n      }\n    }.body()\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/product/command/ProductCommandHandler.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application.product.command\n\nimport com.trendyol.kediatr.*\nimport com.trendyol.stove.examples.domain.product.Product\nimport com.trendyol.stove.examples.kotlin.ktor.application.external.CategoryHttpApi\nimport com.trendyol.stove.examples.kotlin.ktor.domain.product.ProductRepository\nimport com.trendyol.stove.recipes.shared.application.BusinessException\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\ndata class CreateProductCommand(\n  val name: String,\n  val price: Double,\n  val categoryId: Int\n) : Request.Unit\n\nclass ProductCommandHandler(\n  private val productRepository: ProductRepository,\n  private val categoryHttpApi: CategoryHttpApi\n) : RequestHandler.Unit<CreateProductCommand> {\n  private val logger = KotlinLogging.logger { }\n\n  override suspend fun handle(request: CreateProductCommand) {\n    val category = categoryHttpApi.getCategory(request.categoryId)\n    if (!category.isActive) {\n      throw BusinessException(\"Category is not active\")\n    }\n\n    productRepository.save(Product.create(request.name, request.price, request.categoryId))\n    logger.info { \"Product saved: $request\" }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/product/command/handling.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application.product.command\n\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\n\nfun KoinApplication.registerProductCommandHandling() {\n  modules(\n    module {\n      single { ProductCommandHandler(get(), get()) }\n    }\n  )\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/domain/product/ProductRepository.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.domain.product\n\nimport arrow.core.Option\nimport com.trendyol.stove.examples.domain.product.Product\n\ninterface ProductRepository {\n  suspend fun save(product: Product)\n\n  suspend fun findById(id: String): Option<Product>\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/http/http.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.http\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport io.github.oshai.kotlinlogging.*\nimport io.ktor.client.*\nimport io.ktor.client.engine.cio.*\nimport io.ktor.client.plugins.*\nimport io.ktor.client.plugins.contentnegotiation.*\nimport io.ktor.client.plugins.logging.*\nimport io.ktor.client.request.*\nimport io.ktor.http.*\nimport io.ktor.serialization.jackson.*\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\n\nfun KoinApplication.registerHttpClient() {\n  modules(module { single { createHttpClient(get()) } })\n}\n\nprivate fun createHttpClient(\n  objectMapper: ObjectMapper\n): HttpClient = HttpClient(CIO) {\n  install(Logging) {\n    logger = object : Logger {\n      private val logger: KLogger = KotlinLogging.logger(\"StoveHttpClient\")\n\n      override fun log(message: String) {\n        logger.info { message }\n      }\n    }\n  }\n  install(ContentNegotiation) {\n    register(ContentType.Application.Json, JacksonConverter(objectMapper))\n  }\n  val logger = KotlinLogging.logger(\"StoveHttpClient\")\n  install(HttpTimeout) {}\n  install(HttpRequestRetry) {\n    maxRetries = 1\n    retryOnServerErrors()\n    retryOnException(retryOnTimeout = true)\n    exponentialDelay()\n    modifyRequest { request ->\n      logger.warn(cause) { \"Retrying request: ${request.url}\" }\n      request.headers.append(\"X-Retry-Count\", retryCount.toString())\n    }\n  }\n\n  defaultRequest {\n    header(HttpHeaders.ContentType, ContentType.Application.Json)\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/ConsumerEngine.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nclass ConsumerEngine(\n  private val supervisors: List<ConsumerSupervisor<*, *>>\n) {\n  fun start() {\n    supervisors.forEach { it.start() }\n  }\n\n  fun stop() {\n    supervisors.forEach { it.cancel() }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/ConsumerSupervisor.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport io.github.nomisRev.kafka.receiver.KafkaReceiver\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.flattenMerge\nimport org.apache.kafka.clients.consumer.ConsumerRecord\n\nabstract class ConsumerSupervisor<K, V>(\n  private val kafkaReceiver: KafkaReceiver<K, V>,\n  private val maxConcurrency: Int\n) {\n  private val logger = KotlinLogging.logger(\"ConsumerSupervisor[${javaClass.simpleName}]\")\n  private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n\n  abstract val topics: List<String>\n\n  fun start() = scope.launch {\n    logger.info { \"Receiving records from topics: $topics\" }\n    subscribe()\n  }\n\n  @OptIn(ExperimentalCoroutinesApi::class)\n  @Suppress(\"TooGenericExceptionCaught\")\n  private suspend fun subscribe() {\n    kafkaReceiver\n      .receiveAutoAck(topics)\n      .flattenMerge(maxConcurrency)\n      .collect {\n        try {\n          consume(it)\n        } catch (e: Exception) {\n          handleError(e, it)\n        }\n      }\n  }\n\n  abstract suspend fun consume(record: ConsumerRecord<K, V>)\n\n  protected open fun handleError(e: Exception, record: ConsumerRecord<K, V>) {\n    logger.error(e) { \"Error while processing record: $record\" }\n  }\n\n  fun cancel() {\n    logger.info { \"Cancelling consumer supervisor\" }\n    scope.cancel()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/KafkaDomainEventPublisher.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport com.trendyol.stove.examples.domain.ddd.*\nimport io.github.nomisRev.kafka.publisher.KafkaPublisher\nimport kotlinx.coroutines.runBlocking\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.slf4j.*\n\nclass KafkaDomainEventPublisher(\n  private val publisher: KafkaPublisher<String, Any>,\n  private val topicResolver: TopicResolver\n) : EventPublisher {\n  private val logger: Logger = LoggerFactory.getLogger(KafkaDomainEventPublisher::class.java)\n\n  override fun <TId> publishFor(aggregateRoot: AggregateRoot<TId>) = runBlocking {\n    mapEventsToProducerRecords(aggregateRoot)\n      .forEach { record -> publisher.publishScope { offer(record) } }\n  }\n\n  private fun <TId> mapEventsToProducerRecords(\n    aggregateRoot: AggregateRoot<TId>\n  ): List<ProducerRecord<String, Any>> = aggregateRoot\n    .domainEvents()\n    .map { event ->\n      val topic: Topic = topicResolver(aggregateRoot.aggregateName)\n      logger.info(\"Publishing event {} to topic {}\", event, topic.name)\n      ProducerRecord<String, Any>(\n        topic.name,\n        aggregateRoot.idAsString,\n        event\n      )\n    }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/SerDe.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.serialization.JacksonConfiguration\nimport org.apache.kafka.common.serialization.*\n\nprivate val kafkaObjectMapperRef = JacksonConfiguration.default\n\n@Suppress(\"UNCHECKED_CAST\")\nclass StoveKafkaValueDeserializer<T : Any> : Deserializer<T> {\n  override fun deserialize(\n    topic: String,\n    data: ByteArray\n  ): T = kafkaObjectMapperRef.readValue<Any>(data) as T\n}\n\nclass StoveKafkaValueSerializer<T : Any> : Serializer<T> {\n  override fun serialize(\n    topic: String,\n    data: T\n  ): ByteArray = kafkaObjectMapperRef.writeValueAsBytes(data)\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/Topic.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\ndata class Topic(\n  val name: String,\n  val retry: String,\n  val deadLetter: String,\n  val maxRetry: Int = 1,\n  val concurrency: Int = 1\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/TopicResolver.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.*\n\nclass TopicResolver(\n  private val kafkaConfiguration: KafkaConfiguration\n) {\n  operator fun invoke(aggregateName: String): Topic = kafkaConfiguration.topics.getValue(aggregateName)\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/kafka.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport com.mongodb.kotlin.client.coroutine.MongoClient\nimport com.trendyol.stove.examples.domain.ddd.EventPublisher\nimport com.trendyol.stove.examples.kotlin.ktor.application.KafkaConfiguration\nimport io.github.nomisRev.kafka.publisher.*\nimport io.github.nomisRev.kafka.receiver.*\nimport io.ktor.server.application.*\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.*\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.*\nimport org.koin.ktor.ext.get\nimport java.util.*\nimport kotlin.time.Duration.Companion.seconds\n\nfun KoinApplication.registerKafka(kafkaConfiguration: KafkaConfiguration) {\n  modules(\n    module {\n      single { kafkaConfiguration }\n      single { kafkaPublisher(get()) }\n      single { kafkaReceiver(get()) }\n      single { ConsumerEngine(getAll()) }\n      single { KafkaDomainEventPublisher(get(), get()) }.bind<EventPublisher>()\n      single { TopicResolver(get()) }\n    }\n  )\n}\n\nfun Application.configureConsumerEngine() {\n  this.monitor.subscribe(ApplicationStarted) {\n    val consumerEngine = get<ConsumerEngine>()\n    consumerEngine.start()\n  }\n\n  this.monitor.subscribe(ApplicationStopPreparing) {\n    val consumerEngine = get<ConsumerEngine>()\n    consumerEngine.stop()\n\n    get<MongoClient>().close()\n  }\n}\n\nprivate fun kafkaPublisher(\n  kafkaConfiguration: KafkaConfiguration\n): KafkaPublisher<String, Any> = KafkaPublisher(\n  PublisherSettings(\n    bootstrapServers = kafkaConfiguration.bootstrapServers,\n    valueSerializer = StoveKafkaValueSerializer(),\n    keySerializer = StringSerializer(),\n    properties = Properties().apply {\n      putAll(\n        mapOf(\n          ProducerConfig.INTERCEPTOR_CLASSES_CONFIG to kafkaConfiguration.flattenInterceptorClasses()\n        )\n      )\n    }\n  )\n)\n\nprivate fun kafkaReceiver(\n  kafkaConfiguration: KafkaConfiguration\n): KafkaReceiver<String, Any> = KafkaReceiver(\n  ReceiverSettings(\n    bootstrapServers = kafkaConfiguration.bootstrapServers,\n    keyDeserializer = StringDeserializer(),\n    valueDeserializer = StoveKafkaValueDeserializer(),\n    groupId = kafkaConfiguration.groupId,\n    autoOffsetReset = kafkaConfiguration.autoOffsetReset(),\n    commitStrategy = CommitStrategy.ByTime((kafkaConfiguration.heartbeatIntervalSeconds + 1).seconds),\n    pollTimeout = 2.seconds,\n    properties = Properties().apply {\n      putAll(\n        mapOf(\n          ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG to kafkaConfiguration.autoCreateTopics.toString(),\n          ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to kafkaConfiguration.heartbeatIntervalSeconds.seconds.inWholeMilliseconds\n            .toInt(),\n          ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG to kafkaConfiguration.flattenInterceptorClasses()\n        )\n      )\n    }\n  )\n)\n\nprivate fun KafkaConfiguration.autoOffsetReset(): AutoOffsetReset = when (autoOffsetReset) {\n  \"earliest\" -> AutoOffsetReset.Earliest\n  \"latest\" -> AutoOffsetReset.Latest\n  else -> throw IllegalArgumentException(\"Unknown auto offset reset value: $autoOffsetReset\")\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kediatr/kediatr.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kediatr\n\nimport com.trendyol.kediatr.*\nimport com.trendyol.kediatr.koin.KediatRKoin\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\n\nfun KoinApplication.registerKediatR() {\n  modules(\n    module {\n      single { KediatRKoin.getMediator() }\n      single { LoggingPipelineBehaviour() }\n    }\n  )\n}\n\nclass LoggingPipelineBehaviour : PipelineBehavior {\n  override suspend fun <TRequest : Message, TResponse> handle(\n    request: TRequest,\n    next: suspend (TRequest) -> TResponse\n  ): TResponse {\n    println(\"Handling request: $request\")\n    val response = next(request)\n    println(\"Handled request: $request\")\n    return response\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/mongo/mongo.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.mongo\n\nimport com.mongodb.*\nimport com.mongodb.kotlin.client.coroutine.*\nimport com.trendyol.stove.examples.kotlin.ktor.application.RecipeAppConfig\nimport org.bson.UuidRepresentation\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\n\nfun KoinApplication.configureMongo() {\n  modules(createMongoModule())\n}\n\nprivate fun createMongoModule() = module {\n  single { createMongoClient(get()) }\n  single { createMongoDatabase(get(), get()) }\n}\n\nprivate fun createMongoClient(recipeAppConfig: RecipeAppConfig): MongoClient = MongoClient.create(\n  MongoClientSettings\n    .builder()\n    .uuidRepresentation(UuidRepresentation.STANDARD)\n    .applyConnectionString(ConnectionString(recipeAppConfig.mongo.uri))\n    .readConcern(ReadConcern.MAJORITY)\n    .build()\n)\n\nprivate fun createMongoDatabase(mongoClient: MongoClient, recipeAppConfig: RecipeAppConfig): MongoDatabase = mongoClient.getDatabase(\n  recipeAppConfig.mongo.database\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/serialization/JacksonConfiguration.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.serialization\n\nimport com.fasterxml.jackson.databind.*\nimport com.fasterxml.jackson.databind.json.JsonMapper\nimport io.ktor.http.*\nimport io.ktor.serialization.jackson.*\nimport io.ktor.server.application.*\nimport io.ktor.server.plugins.contentnegotiation.*\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\nimport org.koin.ktor.ext.inject\n\nobject JacksonConfiguration {\n  val default: ObjectMapper = JsonMapper\n    .builder()\n    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)\n    .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)\n    .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n    .disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)\n    .disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)\n    .findAndAddModules()\n    .build()\n    .findAndRegisterModules()\n}\n\nfun KoinApplication.configureJackson() {\n  modules(module { single { JacksonConfiguration.default } })\n}\n\nfun Application.configureContentNegotiation() {\n  val mapper: ObjectMapper by inject()\n  install(ContentNegotiation) {\n    register(ContentType.Application.Json, JacksonConverter(mapper))\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/util.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate\n\nimport com.sksamuel.hoplite.*\nimport com.sksamuel.hoplite.env.Environment\nimport com.trendyol.stove.examples.kotlin.ktor.application.RecipeAppConfig\nimport com.trendyol.stove.recipes.shared.application.BusinessException\nimport io.github.oshai.kotlinlogging.*\nimport io.ktor.http.*\nimport io.ktor.server.application.*\nimport io.ktor.server.engine.*\nimport io.ktor.server.netty.*\nimport io.ktor.server.plugins.statuspages.*\nimport io.ktor.server.request.*\nimport io.ktor.server.response.*\nimport org.slf4j.LoggerFactory\n\n@OptIn(ExperimentalHoplite::class)\ninline fun <reified T : Any> loadConfiguration(args: Array<String> = arrayOf()): T = ConfigLoaderBuilder\n  .default()\n  .addEnvironmentSource()\n  .addCommandLineSource(args)\n  .withExplicitSealedTypes()\n  .withEnvironment(AppEnv.toEnv())\n  .apply {\n    when (AppEnv.current()) {\n      AppEnv.Local -> {\n        addResourceSource(\"/application.yaml\", optional = true)\n      }\n\n      AppEnv.Prod -> {\n        addResourceSource(\"/application-prod.yaml\", optional = true)\n        addResourceSource(\"/application.yaml\", optional = true)\n      }\n\n      else -> {\n        addResourceSource(\"/application.yaml\", optional = true)\n      }\n    }\n  }.build()\n  .loadConfigOrThrow<T>()\n\nenum class AppEnv(\n  val env: String\n) {\n  Unspecified(\"\"),\n  Local(Environment.local.name),\n  Prod(Environment.prod.name)\n  ;\n\n  companion object {\n    fun current(): AppEnv = when (System.getenv(\"ENVIRONMENT\")) {\n      Unspecified.env -> Unspecified\n      Local.env -> Local\n      Prod.env -> Prod\n      else -> Local\n    }\n\n    fun toEnv(): Environment = when (current()) {\n      Local -> Environment.local\n      Prod -> Environment.prod\n      else -> Environment.local\n    }\n  }\n\n  fun isLocal(): Boolean = this === Local\n\n  fun isProd(): Boolean = this === Prod\n}\n\nfun startKtorApplication(config: RecipeAppConfig, wait: Boolean = true, configure: Application.() -> Unit): Application {\n  val loggerName = configure.javaClass.name\n    .split('$')\n    .first()\n\n  val server = embeddedServer(\n    Netty,\n    module = {\n      this.configure()\n    },\n    configure = {\n      connector {\n        port = config.server.port\n        host = config.server.host\n      }\n    },\n    environment = applicationEnvironment {\n      log = LoggerFactory.getLogger(loggerName)\n    }\n  )\n\n  return server.start(wait = wait).application\n}\n\nfun Application.configureExceptionHandling(logging: KLogger = KotlinLogging.logger {}) {\n  install(StatusPages) {\n    exception<Throwable> { call, reason ->\n      when (reason) {\n        is BusinessException -> {\n          logging.warn(reason) { \"A business exception occurred ${call.request.uri}\" }\n          call.respond(HttpStatusCode.Conflict, reason.message.toString())\n        }\n\n        else -> {\n          logging.error(reason) { \"An unexpected error occurred ${call.request.uri}\" }\n          call.respond(HttpStatusCode.InternalServerError, \"An unexpected error occurred, please try again later\")\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/external/category.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.external\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.external.*\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.bind\n\nfun KoinApplication.registerCategoryExternalHttpApi() {\n  modules(createCategoryExternalApi())\n}\n\nprivate fun createCategoryExternalApi() = org.koin.dsl.module {\n  single { CategoryHttpApiImpl(get(), get()) }.bind<CategoryHttpApi>()\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/product/api/routing.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.product.api\n\nimport com.trendyol.kediatr.Mediator\nimport com.trendyol.stove.examples.kotlin.ktor.application.product.command.CreateProductCommand\nimport io.ktor.server.application.*\nimport io.ktor.server.request.*\nimport io.ktor.server.response.*\nimport io.ktor.server.routing.*\nimport org.koin.ktor.ext.get\n\nfun Routing.productApi() {\n  post(\"/products\") {\n    val mediator = call.get<Mediator>()\n    val req = call.receive<ProductCreateRequest>()\n    mediator.send(CreateProductCommand(req.name, req.price, req.categoryId))\n    call.respond(\"Product created\")\n  }\n}\n\ndata class ProductCreateRequest(\n  var name: String,\n  var price: Double,\n  var categoryId: Int\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/product/defs.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.product\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.product.command.registerProductCommandHandling\nimport com.trendyol.stove.examples.kotlin.ktor.domain.product.ProductRepository\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.ConsumerSupervisor\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.messaging.ProductAggregateRootEventsConsumer\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.persistency.MongoProductRepository\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.*\n\nfun KoinApplication.registerProductComponents() {\n  modules(\n    module {\n      single { MongoProductRepository(get(), get(), get()) }.bind<ProductRepository>()\n      single { ProductAggregateRootEventsConsumer(get(), get()) }.bind<ConsumerSupervisor<*, *>>()\n    }\n  )\n  registerProductCommandHandling()\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/product/messaging/ProductAggregateRootEventsConsumer.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.product.messaging\n\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.*\nimport io.github.nomisRev.kafka.receiver.KafkaReceiver\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport org.apache.kafka.clients.consumer.ConsumerRecord\n\nclass ProductAggregateRootEventsConsumer(\n  topicResolver: TopicResolver,\n  kafkaReceiver: KafkaReceiver<String, Any>,\n  topic: Topic = topicResolver(\"product\")\n) : ConsumerSupervisor<String, Any>(kafkaReceiver, topic.concurrency) {\n  private val logger = KotlinLogging.logger { }\n  override val topics: List<String> = listOf(topic.name, topic.retry)\n\n  override suspend fun consume(record: ConsumerRecord<String, Any>) {\n    logger.info { \"consumed record: $record\" }\n  }\n\n  override fun handleError(e: Exception, record: ConsumerRecord<String, Any>) {\n    logger.error(e) { \"Error while processing record: $record\" }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/product/persistency/MongoProductRepository.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.product.persistency\n\nimport arrow.core.*\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.fasterxml.jackson.module.kotlin.convertValue\nimport com.mongodb.client.model.Filters\nimport com.mongodb.kotlin.client.coroutine.MongoDatabase\nimport com.trendyol.stove.examples.domain.product.Product\nimport com.trendyol.stove.examples.kotlin.ktor.domain.product.ProductRepository\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.KafkaDomainEventPublisher\nimport kotlinx.coroutines.flow.firstOrNull\nimport org.bson.Document\nimport org.bson.json.JsonWriterSettings\nimport org.bson.types.ObjectId\n\nclass MongoProductRepository(\n  mongo: MongoDatabase,\n  private val objectMapper: ObjectMapper,\n  private val eventPublisher: KafkaDomainEventPublisher\n) : ProductRepository {\n  private val collection = mongo.getCollection<Document>(PRODUCT_COLLECTION)\n\n  override suspend fun save(product: Product) {\n    val doc = Document(objectMapper.convertValue<MutableMap<String, Any>>(product))\n    doc[RESERVED_ID] = ObjectId.get()\n    collection.insertOne(doc)\n    eventPublisher.publishFor(product)\n  }\n\n  override suspend fun findById(id: String): Option<Product> = collection\n    .find(Filters.eq(\"id\", id))\n    .firstOrNull()\n    ?.let { objectMapper.convertValue(it, Product::class.java) }\n    .toOption()\n\n  companion object {\n    private const val RESERVED_ID = \"_id\"\n    const val PRODUCT_COLLECTION = \"products\"\n  }\n}\n\nobject MongoJsonWriterSettings {\n  val default: JsonWriterSettings = JsonWriterSettings\n    .builder()\n    .objectIdConverter { value, writer -> writer.writeString(value.toHexString()) }\n    .build()\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/main/resources/application.yaml",
    "content": "server:\n  port: 8081\n  host: \"localhost\"\n  name: \"test\"\nmongo:\n  database: stove-kotlin-ktor\n  uri: localhost:27017\nkafka:\n  bootstrap-servers: localhost:9092\n  group-id: stove-kotlin-ktor\n  heartbeat-interval-seconds: 2\n  request-timeout-seconds: 30\n  session-timeout-seconds: 10\n  auto-create-topics: true\n  auto-offset-reset: earliest\n  interceptor-classes: [ ]\n  topics:\n    product:\n      name: stove-kotlin-ktor.product\n      retry: stove-kotlin-ktor.retry\n      dead-letter: stove-kotlin-ktor.error\n      concurrency: 2\n      maxRetry: 1\nexternal-apis:\n  category:\n    url: http://localhost:9090\n    timeout: 30\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/setup/StoveConfig.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.setup\n\nimport com.trendyol.stove.examples.kotlin.ktor.ExampleStoveKtorApp\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.serialization.JacksonConfiguration\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.persistency.MongoProductRepository.Companion.PRODUCT_COLLECTION\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.ktor.*\nimport com.trendyol.stove.mongodb.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.ktor.serialization.jackson.*\nimport org.koin.dsl.module\n\nprivate const val DATABASE = \"stove-kotlin-ktor\"\n\nclass StoveConfig : AbstractProjectConfig() {\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:8081\",\n            contentConverter = JacksonConverter(JacksonConfiguration.default)\n          )\n        }\n        bridge()\n        wiremock {\n          WireMockSystemOptions(\n            port = 9090\n          )\n        }\n        kafka {\n          KafkaSystemOptions(\n            serde = StoveSerde.jackson.anyByteArraySerde(JacksonConfiguration.default),\n            containerOptions = KafkaContainerOptions(tag = \"8.0.3\"),\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"kafka.bootstrapServers=${cfg.bootstrapServers}\",\n                \"kafka.interceptor-classes=${cfg.interceptorClass}\"\n              )\n            }\n          )\n        }\n        mongodb {\n          MongodbSystemOptions(\n            databaseOptions = DatabaseOptions(DatabaseOptions.DefaultDatabase(DATABASE, collection = PRODUCT_COLLECTION)),\n            container = MongoContainerOptions(),\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"mongo.database=$DATABASE\",\n                \"mongo.uri=${cfg.connectionString}/?retryWrites=true&w=majority\"\n              )\n            }\n          )\n        }\n        ktor(\n          runner = { parameters ->\n            ExampleStoveKtorApp.run(\n              parameters,\n              wait = false,\n              module {\n              }\n            )\n          },\n          withParameters = listOf(\n            \"server.name=${Thread.currentThread().name}\"\n          )\n        )\n      }.run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/setup/TestData.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.setup\n\nobject TestData {\n  object Random {\n    fun positiveInt() = kotlin.random.Random.nextInt(1, Int.MAX_VALUE)\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/tests/IndexTests.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.tests\n\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass IndexTests :\n  FunSpec({\n    test(\"Index page should return 200\") {\n      stove {\n        http {\n          getResponse<String>(\"/\") { actual ->\n            actual.status shouldBe 200\n            actual.body() shouldBe \"Hello, World!\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/tests/configuration/ConfigurationTests.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.tests.configuration\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.RecipeAppConfig\nimport com.trendyol.stove.system.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldNotBe\n\nclass ConfigurationTests :\n  FunSpec({\n    test(\"configuration can be changed from app\") {\n      stove {\n        using<RecipeAppConfig> {\n          this.server.name shouldNotBe \"test\"\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/tests/product/CreateTests.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.tests.product\n\nimport arrow.core.*\nimport com.mongodb.client.model.Filters\nimport com.trendyol.stove.examples.domain.product.Product\nimport com.trendyol.stove.examples.domain.product.events.ProductCreatedEvent\nimport com.trendyol.stove.examples.kotlin.ktor.domain.product.ProductRepository\nimport com.trendyol.stove.examples.kotlin.ktor.e2e.setup.TestData\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.api.ProductCreateRequest\nimport com.trendyol.stove.functional.get\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.mongodb.mongodb\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport java.util.*\nimport kotlin.time.Duration.Companion.seconds\n\nclass CreateTests :\n  FunSpec({\n    test(\"product can be created with valid category\") {\n      stove {\n        val productName = TestData.Random.positiveInt().toString()\n        val productId = UUID.nameUUIDFromBytes(productName.toByteArray())\n\n        val categoryApiResponse = CategoryApiResponse(\n          TestData.Random.positiveInt(),\n          \"category-name\",\n          true\n        )\n\n        wiremock {\n          mockGet(\n            url = \"/categories/${categoryApiResponse.id}\",\n            statusCode = 200,\n            responseBody = categoryApiResponse.some()\n          )\n        }\n\n        http {\n          val req = ProductCreateRequest(productName, 100.0, categoryApiResponse.id)\n          postAndExpectBody<Any>(\"/products\", body = req.some()) { actual ->\n            actual.status shouldBe 200\n          }\n        }\n\n        mongodb {\n          shouldQuery<Product>(Filters.eq(\"id\", productId.toString()).toBsonDocument().toJson()) { actual ->\n            actual.size shouldBe 1\n            actual[0].name shouldBe productName\n            actual[0].price shouldBe 100.0\n          }\n        }\n\n        using<ProductRepository> {\n          val product = findById(productId.toString()).get()\n          product.name shouldBe productName\n          product.price shouldBe 100.0\n          product.categoryId shouldBe categoryApiResponse.id\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent>(10.seconds) {\n            actual.price == 100.0 && actual.name == productName\n          }\n\n          shouldBeConsumed<ProductCreatedEvent>(10.seconds) {\n            actual.price == 100.0 && actual.name == productName\n          }\n        }\n      }\n    }\n\n    test(\"when category is not active, product creation should fail\") {\n      stove {\n        val productName = TestData.Random.positiveInt().toString()\n        val productId = UUID.nameUUIDFromBytes(productName.toByteArray())\n        val categoryApiResponse = CategoryApiResponse(\n          TestData.Random.positiveInt(),\n          \"category-name\",\n          false\n        )\n\n        wiremock {\n          mockGet(\n            url = \"/categories/${categoryApiResponse.id}\",\n            statusCode = 200,\n            responseBody = categoryApiResponse.some()\n          )\n        }\n\n        http {\n          val req = ProductCreateRequest(productName, 100.0, categoryApiResponse.id)\n          postAndExpectBody<Any>(\"/products\", body = req.some()) { actual ->\n            actual.status shouldBe 409\n          }\n        }\n\n        mongodb {\n          shouldQuery<Product>(Filters.eq(\"id\", productId.toString()).toBsonDocument().toJson()) { actual ->\n            actual.size shouldBe 0\n          }\n        }\n\n        using<ProductRepository> {\n          findById(productId.toString()) shouldBe None\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/test-e2e/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.examples.kotlin.ktor.e2e.setup.StoveConfig\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-mongo-recipe/src/test-e2e/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"ERROR\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/build.gradle.kts",
    "content": "dependencies {\n  implementation(projects.shared.domain)\n  implementation(projects.shared.application)\n  implementation(libs.ktor.server.core.jvm)\n  implementation(libs.ktor.server.netty.jvm)\n  implementation(libs.ktor.server.content.negotiation.jvm)\n  implementation(libs.ktor.server.statuspages)\n  implementation(libs.ktor.server.callLogging)\n  implementation(libs.ktor.server.callId)\n  implementation(libs.ktor.server.conditionalHeaders)\n  implementation(libs.ktor.server.cors)\n  implementation(libs.ktor.server.defaultHeaders)\n  implementation(libs.ktor.server.cachingHeaders)\n  implementation(libs.ktor.server.autoHeadResponse)\n  implementation(libs.ktor.server.config.yml)\n  implementation(libs.ktor.swagger.ui)\n  implementation(libs.ktor.serialization.jackson.json)\n  implementation(libs.koin)\n  implementation(libs.koin.ktor)\n  implementation(libs.slf4j.api)\n  implementation(libs.arrow.core)\n  implementation(libs.hoplite)\n  implementation(libs.hoplite.yaml)\n  implementation(libs.logback.classic)\n  implementation(libs.ktor.client.core)\n  implementation(libs.ktor.client.cio)\n  implementation(libs.ktor.client.plugins.logging)\n  implementation(libs.ktor.client.content.negotiation)\n  implementation(libs.kotlinFpUtil)\n  implementation(libs.kotlin.logging.jvm)\n  implementation(libs.kediatr.koin)\n\n  implementation(libs.exposed.core)\n  implementation(libs.exposed.r2dbc)\n  implementation(libs.exposed.json)\n  implementation(libs.exposed.javaTime)\n  implementation(libs.postgresql.r2dbc)\n  implementation(libs.postgresql)\n  implementation(libs.r2dbc.pool)\n  implementation(libs.flyway.core)\n  implementation(libs.flyway.database.postgresql)\n\n  implementation(libs.kafkaKotlin)\n}\n\ndependencies {\n  testImplementation(stoveLibs.stove)\n  testImplementation(stoveLibs.stovePostgres)\n  testImplementation(stoveLibs.stoveHttp)\n  testImplementation(stoveLibs.stoveWiremock)\n  testImplementation(stoveLibs.stoveKafka)\n  testImplementation(stoveLibs.stoveKtor)\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/ExampleStoveKtorApp.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.RecipeAppConfig\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.*\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.http.registerHttpClient\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.*\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kediatr.registerKediatR\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.serialization.*\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.external.registerCategoryExternalHttpApi\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.api.productApi\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.registerProductComponents\nimport com.trendyol.stove.examples.kotlin.ktor.infra.postgres.*\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.ktor.server.application.*\nimport io.ktor.server.plugins.autohead.*\nimport io.ktor.server.response.*\nimport io.ktor.server.routing.*\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\nimport org.koin.ktor.plugin.Koin\n\nval logger = KotlinLogging.logger(\"Stove Ktor Recipe\")\n\nobject ExampleStoveKtorApp {\n  @JvmStatic\n  fun main(args: Array<String>) {\n    run(args)\n  }\n\n  fun run(args: Array<String>, wait: Boolean = true, configure: org.koin.core.module.Module = module { }): Application {\n    val config = loadConfiguration<RecipeAppConfig>(args)\n    logger.info { \"Starting Ktor application with config: $config\" }\n    return startKtorApplication(config, wait) {\n      appModule(config, configure)\n    }\n  }\n}\n\nfun Application.appModule(\n  config: RecipeAppConfig,\n  overrides: org.koin.core.module.Module = module { }\n) {\n  install(Koin) {\n    allowOverride(true)\n    modules(\n      module {\n        single { config }\n        single { config.externalApis.category }\n        single { config.db }\n      }\n    )\n    registerAppDeps()\n    registerHttpClient()\n    registerKafka(config.kafka)\n    modules(overrides)\n  }\n  configureRouting()\n  configureExceptionHandling()\n  configureContentNegotiation()\n  configureConsumerEngine()\n  configureFlyway()\n}\n\nfun KoinApplication.registerAppDeps() {\n  configurePostgres()\n  configureJackson()\n  registerKediatR()\n  registerProductComponents()\n  registerCategoryExternalHttpApi()\n}\n\nfun Application.configureRouting() {\n  install(AutoHeadResponse)\n  routing {\n    route(\"/\") {\n      get {\n        call.respondText(\"Hello, World!\")\n      }\n    }\n    productApi()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/RecipeAppConfig.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.external.CategoryApiConfiguration\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.Topic\nimport com.trendyol.stove.examples.kotlin.ktor.infra.postgres.R2dbcProperties\n\n/**\n * Represents the main configuration\n */\ndata class RecipeAppConfig(\n  val server: ServerConfig,\n  val kafka: KafkaConfiguration,\n  val db: R2dbcProperties,\n  val externalApis: ExternalApisConfig\n)\n\ndata class ExternalApisConfig(\n  val category: CategoryApiConfiguration\n)\n\n/**\n * Represents the configuration of the checker.\n */\ndata class ServerConfig(\n  /**\n   * Port of the server.\n   */\n  val port: Int = 8082,\n  /**\n   * Host of the server.\n   */\n  val host: String = \"\",\n  val name: String\n)\n\ndata class KafkaConfiguration(\n  val bootstrapServers: String,\n  val groupId: String,\n  val requestTimeoutSeconds: Long = 30,\n  val heartbeatIntervalSeconds: Long = 3,\n  val sessionTimeoutSeconds: Long = 10,\n  val autoCreateTopics: Boolean = true,\n  val autoOffsetReset: String = \"earliest\",\n  val interceptorClasses: List<String>,\n  val topics: Map<String, Topic>\n) {\n  fun flattenInterceptorClasses(): String = interceptorClasses.joinToString(\",\")\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/external/CategoryHttpApi.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application.external\n\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse\n\ninterface CategoryHttpApi {\n  suspend fun getCategory(id: Int): CategoryApiResponse\n}\n\ndata class CategoryApiConfiguration(\n  val url: String,\n  val timeout: Long\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/external/CategoryHttpApiImpl.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application.external\n\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse\nimport io.ktor.client.*\nimport io.ktor.client.call.*\nimport io.ktor.client.plugins.*\nimport io.ktor.client.request.*\nimport io.ktor.http.*\nimport kotlin.time.Duration.Companion.seconds\n\nclass CategoryHttpApiImpl(\n  private val httpClient: HttpClient,\n  private val categoryApiConfiguration: CategoryApiConfiguration\n) : CategoryHttpApi {\n  override suspend fun getCategory(id: Int): CategoryApiResponse = httpClient\n    .get(\"${categoryApiConfiguration.url}/categories/$id\") {\n      accept(ContentType.Application.Json)\n      timeout {\n        requestTimeoutMillis = categoryApiConfiguration.timeout.seconds.inWholeMilliseconds\n        connectTimeoutMillis = categoryApiConfiguration.timeout.seconds.inWholeMilliseconds\n        socketTimeoutMillis = categoryApiConfiguration.timeout.seconds.inWholeMilliseconds\n      }\n    }.body()\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/product/command/ProductCommandHandler.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application.product.command\n\nimport com.trendyol.kediatr.*\nimport com.trendyol.stove.examples.domain.product.Product\nimport com.trendyol.stove.examples.kotlin.ktor.application.external.CategoryHttpApi\nimport com.trendyol.stove.examples.kotlin.ktor.domain.product.ProductRepository\nimport com.trendyol.stove.recipes.shared.application.BusinessException\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\ndata class CreateProductCommand(\n  val name: String,\n  val price: Double,\n  val categoryId: Int\n) : Request.Unit\n\nclass ProductCommandHandler(\n  private val productRepository: ProductRepository,\n  private val categoryHttpApi: CategoryHttpApi\n) : RequestHandler.Unit<CreateProductCommand> {\n  private val logger = KotlinLogging.logger { }\n\n  override suspend fun handle(request: CreateProductCommand) {\n    val category = categoryHttpApi.getCategory(request.categoryId)\n    if (!category.isActive) {\n      throw BusinessException(\"Category is not active\")\n    }\n\n    productRepository.save(Product.create(request.name, request.price, request.categoryId))\n    logger.info { \"Product saved: $request\" }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/application/product/command/handling.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.application.product.command\n\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\n\nfun KoinApplication.registerProductCommandHandling() {\n  modules(\n    module {\n      single { ProductCommandHandler(get(), get()) }\n    }\n  )\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/domain/product/ProductRepository.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.domain.product\n\nimport arrow.core.Option\nimport com.trendyol.stove.examples.domain.product.Product\n\ninterface ProductRepository {\n  suspend fun save(product: Product)\n\n  suspend fun findById(id: String): Option<Product>\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/http/http.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.http\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport io.github.oshai.kotlinlogging.*\nimport io.ktor.client.*\nimport io.ktor.client.engine.cio.*\nimport io.ktor.client.plugins.*\nimport io.ktor.client.plugins.contentnegotiation.*\nimport io.ktor.client.plugins.logging.*\nimport io.ktor.client.request.*\nimport io.ktor.http.*\nimport io.ktor.serialization.jackson.*\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\n\nfun KoinApplication.registerHttpClient() {\n  modules(module { single { createHttpClient(get()) } })\n}\n\nprivate fun createHttpClient(\n  objectMapper: ObjectMapper\n): HttpClient = HttpClient(CIO) {\n  install(Logging) {\n    logger = object : Logger {\n      private val logger: KLogger = KotlinLogging.logger(\"StoveHttpClient\")\n\n      override fun log(message: String) {\n        logger.info { message }\n      }\n    }\n  }\n  install(ContentNegotiation) {\n    register(ContentType.Application.Json, JacksonConverter(objectMapper))\n  }\n  val logger = KotlinLogging.logger(\"StoveHttpClient\")\n  install(HttpTimeout) {}\n  install(HttpRequestRetry) {\n    maxRetries = 1\n    retryOnServerErrors()\n    retryOnException(retryOnTimeout = true)\n    exponentialDelay()\n    modifyRequest { request ->\n      logger.warn(cause) { \"Retrying request: ${request.url}\" }\n      request.headers.append(\"X-Retry-Count\", retryCount.toString())\n    }\n  }\n\n  defaultRequest {\n    header(HttpHeaders.ContentType, ContentType.Application.Json)\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/ConsumerEngine.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nclass ConsumerEngine(\n  private val supervisors: List<ConsumerSupervisor<*, *>>\n) {\n  fun start() {\n    supervisors.forEach { it.start() }\n  }\n\n  fun stop() {\n    supervisors.forEach { it.cancel() }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/ConsumerSupervisor.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport io.github.nomisRev.kafka.receiver.KafkaReceiver\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.flow.flattenMerge\nimport org.apache.kafka.clients.consumer.ConsumerRecord\n\nabstract class ConsumerSupervisor<K, V>(\n  private val kafkaReceiver: KafkaReceiver<K, V>,\n  private val maxConcurrency: Int\n) {\n  private val logger = KotlinLogging.logger(\"ConsumerSupervisor[${javaClass.simpleName}]\")\n  private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n\n  abstract val topics: List<String>\n\n  fun start() = scope.launch {\n    logger.info { \"Receiving records from topics: $topics\" }\n    subscribe()\n  }\n\n  @OptIn(ExperimentalCoroutinesApi::class)\n  @Suppress(\"TooGenericExceptionCaught\")\n  private suspend fun subscribe() {\n    kafkaReceiver\n      .receiveAutoAck(topics)\n      .flattenMerge(maxConcurrency)\n      .collect {\n        try {\n          consume(it)\n        } catch (e: Exception) {\n          handleError(e, it)\n        }\n      }\n  }\n\n  abstract suspend fun consume(record: ConsumerRecord<K, V>)\n\n  protected open fun handleError(e: Exception, record: ConsumerRecord<K, V>) {\n    logger.error(e) { \"Error while processing record: $record\" }\n  }\n\n  fun cancel() {\n    logger.info { \"Cancelling consumer supervisor\" }\n    scope.cancel()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/KafkaDomainEventPublisher.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport com.trendyol.stove.examples.domain.ddd.*\nimport io.github.nomisRev.kafka.publisher.KafkaPublisher\nimport kotlinx.coroutines.runBlocking\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.slf4j.*\n\nclass KafkaDomainEventPublisher(\n  private val publisher: KafkaPublisher<String, Any>,\n  private val topicResolver: TopicResolver\n) : EventPublisher {\n  private val logger: Logger = LoggerFactory.getLogger(KafkaDomainEventPublisher::class.java)\n\n  override fun <TId> publishFor(aggregateRoot: AggregateRoot<TId>) = runBlocking {\n    mapEventsToProducerRecords(aggregateRoot)\n      .forEach { record -> publisher.publishScope { offer(record) } }\n  }\n\n  private fun <TId> mapEventsToProducerRecords(\n    aggregateRoot: AggregateRoot<TId>\n  ): List<ProducerRecord<String, Any>> = aggregateRoot\n    .domainEvents()\n    .map { event ->\n      val topic: Topic = topicResolver(aggregateRoot.aggregateName)\n      logger.info(\"Publishing event {} to topic {}\", event, topic.name)\n      ProducerRecord<String, Any>(\n        topic.name,\n        aggregateRoot.idAsString,\n        event\n      )\n    }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/SerDe.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport com.fasterxml.jackson.module.kotlin.readValue\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.serialization.JacksonConfiguration\nimport org.apache.kafka.common.serialization.*\n\nprivate val kafkaObjectMapperRef = JacksonConfiguration.default\n\n@Suppress(\"UNCHECKED_CAST\")\nclass StoveKafkaValueDeserializer<T : Any> : Deserializer<T> {\n  override fun deserialize(\n    topic: String,\n    data: ByteArray\n  ): T = kafkaObjectMapperRef.readValue<Any>(data) as T\n}\n\nclass StoveKafkaValueSerializer<T : Any> : Serializer<T> {\n  override fun serialize(\n    topic: String,\n    data: T\n  ): ByteArray = kafkaObjectMapperRef.writeValueAsBytes(data)\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/Topic.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\ndata class Topic(\n  val name: String,\n  val retry: String,\n  val deadLetter: String,\n  val maxRetry: Int = 1,\n  val concurrency: Int = 1\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/TopicResolver.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.*\n\nclass TopicResolver(\n  private val kafkaConfiguration: KafkaConfiguration\n) {\n  operator fun invoke(aggregateName: String): Topic = kafkaConfiguration.topics.getValue(aggregateName)\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kafka/kafka.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka\n\nimport com.trendyol.stove.examples.domain.ddd.EventPublisher\nimport com.trendyol.stove.examples.kotlin.ktor.application.KafkaConfiguration\nimport io.github.nomisRev.kafka.publisher.*\nimport io.github.nomisRev.kafka.receiver.*\nimport io.ktor.server.application.*\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.*\nimport org.flywaydb.core.Flyway\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.*\nimport org.koin.ktor.ext.get\nimport java.util.*\nimport kotlin.time.Duration.Companion.seconds\n\nfun KoinApplication.registerKafka(kafkaConfiguration: KafkaConfiguration) {\n  modules(\n    module {\n      single { kafkaConfiguration }\n      single { kafkaPublisher(get()) }\n      single { kafkaReceiver(get()) }\n      single { ConsumerEngine(getAll()) }\n      single { KafkaDomainEventPublisher(get(), get()) }.bind<EventPublisher>()\n      single { TopicResolver(get()) }\n    }\n  )\n}\n\nfun Application.configureConsumerEngine() {\n  this.monitor.subscribe(ApplicationStarted) {\n    val consumerEngine = get<ConsumerEngine>()\n    consumerEngine.start()\n\n    val flyway = get<Flyway>()\n    flyway.migrate()\n  }\n\n  this.monitor.subscribe(ApplicationStopPreparing) {\n    val consumerEngine = get<ConsumerEngine>()\n    consumerEngine.stop()\n  }\n}\n\nprivate fun kafkaPublisher(\n  kafkaConfiguration: KafkaConfiguration\n): KafkaPublisher<String, Any> = KafkaPublisher(\n  PublisherSettings(\n    bootstrapServers = kafkaConfiguration.bootstrapServers,\n    valueSerializer = StoveKafkaValueSerializer(),\n    keySerializer = StringSerializer(),\n    properties = Properties().apply {\n      putAll(\n        mapOf(\n          ProducerConfig.INTERCEPTOR_CLASSES_CONFIG to kafkaConfiguration.flattenInterceptorClasses()\n        )\n      )\n    }\n  )\n)\n\nprivate fun kafkaReceiver(\n  kafkaConfiguration: KafkaConfiguration\n): KafkaReceiver<String, Any> = KafkaReceiver(\n  ReceiverSettings(\n    bootstrapServers = kafkaConfiguration.bootstrapServers,\n    keyDeserializer = StringDeserializer(),\n    valueDeserializer = StoveKafkaValueDeserializer(),\n    groupId = kafkaConfiguration.groupId,\n    autoOffsetReset = kafkaConfiguration.autoOffsetReset(),\n    commitStrategy = CommitStrategy.ByTime((kafkaConfiguration.heartbeatIntervalSeconds + 1).seconds),\n    pollTimeout = 2.seconds,\n    properties = Properties().apply {\n      putAll(\n        mapOf(\n          ConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG to kafkaConfiguration.autoCreateTopics.toString(),\n          ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to kafkaConfiguration.heartbeatIntervalSeconds.seconds.inWholeMilliseconds\n            .toInt(),\n          ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG to kafkaConfiguration.flattenInterceptorClasses()\n        )\n      )\n    }\n  )\n)\n\nprivate fun KafkaConfiguration.autoOffsetReset(): AutoOffsetReset = when (autoOffsetReset) {\n  \"earliest\" -> AutoOffsetReset.Earliest\n  \"latest\" -> AutoOffsetReset.Latest\n  else -> throw IllegalArgumentException(\"Unknown auto offset reset value: $autoOffsetReset\")\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/kediatr/kediatr.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kediatr\n\nimport com.trendyol.kediatr.*\nimport com.trendyol.kediatr.koin.KediatRKoin\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\n\nfun KoinApplication.registerKediatR() {\n  modules(\n    module {\n      single { KediatRKoin.getMediator() }\n      single { LoggingPipelineBehaviour() }\n    }\n  )\n}\n\nclass LoggingPipelineBehaviour : PipelineBehavior {\n  override suspend fun <TRequest : Message, TResponse> handle(\n    request: TRequest,\n    next: suspend (TRequest) -> TResponse\n  ): TResponse {\n    println(\"Handling request: $request\")\n    val response = next(request)\n    println(\"Handled request: $request\")\n    return response\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/serialization/JacksonConfiguration.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.serialization\n\nimport com.fasterxml.jackson.databind.*\nimport com.fasterxml.jackson.databind.json.JsonMapper\nimport io.ktor.http.*\nimport io.ktor.serialization.jackson.*\nimport io.ktor.server.application.*\nimport io.ktor.server.plugins.contentnegotiation.*\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\nimport org.koin.ktor.ext.inject\n\nobject JacksonConfiguration {\n  val default: ObjectMapper = JsonMapper\n    .builder()\n    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)\n    .disable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)\n    .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)\n    .disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)\n    .disable(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE)\n    .findAndAddModules()\n    .build()\n    .findAndRegisterModules()\n}\n\nfun KoinApplication.configureJackson() {\n  modules(module { single { JacksonConfiguration.default } })\n}\n\nfun Application.configureContentNegotiation() {\n  val mapper: ObjectMapper by inject()\n  install(ContentNegotiation) {\n    register(ContentType.Application.Json, JacksonConverter(mapper))\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/boilerplate/util.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate\n\nimport com.sksamuel.hoplite.*\nimport com.sksamuel.hoplite.env.Environment\nimport com.trendyol.stove.examples.kotlin.ktor.application.RecipeAppConfig\nimport com.trendyol.stove.recipes.shared.application.BusinessException\nimport io.github.oshai.kotlinlogging.*\nimport io.ktor.http.*\nimport io.ktor.server.application.*\nimport io.ktor.server.engine.*\nimport io.ktor.server.netty.*\nimport io.ktor.server.plugins.statuspages.*\nimport io.ktor.server.request.*\nimport io.ktor.server.response.*\nimport org.slf4j.LoggerFactory\n\n@OptIn(ExperimentalHoplite::class)\ninline fun <reified T : Any> loadConfiguration(args: Array<String> = arrayOf()): T = ConfigLoaderBuilder\n  .default()\n  .addEnvironmentSource()\n  .addCommandLineSource(args)\n  .withExplicitSealedTypes()\n  .withEnvironment(AppEnv.toEnv())\n  .apply {\n    when (AppEnv.current()) {\n      AppEnv.Local -> {\n        addResourceSource(\"/application.yaml\", optional = true)\n      }\n\n      AppEnv.Prod -> {\n        addResourceSource(\"/application-prod.yaml\", optional = true)\n        addResourceSource(\"/application.yaml\", optional = true)\n      }\n\n      else -> {\n        addResourceSource(\"/application.yaml\", optional = true)\n      }\n    }\n  }.build()\n  .loadConfigOrThrow<T>()\n\nenum class AppEnv(\n  val env: String\n) {\n  Unspecified(\"\"),\n  Local(Environment.local.name),\n  Prod(Environment.prod.name)\n  ;\n\n  companion object {\n    fun current(): AppEnv = when (System.getenv(\"ENVIRONMENT\")) {\n      Unspecified.env -> Unspecified\n      Local.env -> Local\n      Prod.env -> Prod\n      else -> Local\n    }\n\n    fun toEnv(): Environment = when (current()) {\n      Local -> Environment.local\n      Prod -> Environment.prod\n      else -> Environment.local\n    }\n  }\n\n  fun isLocal(): Boolean = this === Local\n\n  fun isProd(): Boolean = this === Prod\n}\n\nfun startKtorApplication(config: RecipeAppConfig, wait: Boolean = true, configure: Application.() -> Unit): Application {\n  val loggerName = configure.javaClass.name\n    .split('$')\n    .first()\n\n  val server = embeddedServer(\n    Netty,\n    module = {\n      this.configure()\n    },\n    configure = {\n      connector {\n        port = config.server.port\n        host = config.server.host\n      }\n    },\n    environment = applicationEnvironment {\n      log = LoggerFactory.getLogger(loggerName)\n    }\n  )\n\n  return server.start(wait = wait).application\n}\n\nfun Application.configureExceptionHandling(logging: KLogger = KotlinLogging.logger {}) {\n  install(StatusPages) {\n    exception<Throwable> { call, reason ->\n      when (reason) {\n        is BusinessException -> {\n          logging.warn(reason) { \"A business exception occurred ${call.request.uri}\" }\n          call.respond(HttpStatusCode.Conflict, reason.message.toString())\n        }\n\n        else -> {\n          logging.error(reason) { \"An unexpected error occurred ${call.request.uri}\" }\n          call.respond(HttpStatusCode.InternalServerError, \"An unexpected error occurred, please try again later\")\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/external/category.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.external\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.external.*\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.bind\n\nfun KoinApplication.registerCategoryExternalHttpApi() {\n  modules(createCategoryExternalApi())\n}\n\nprivate fun createCategoryExternalApi() = org.koin.dsl.module {\n  single { CategoryHttpApiImpl(get(), get()) }.bind<CategoryHttpApi>()\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/product/api/routing.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.product.api\n\nimport com.trendyol.kediatr.Mediator\nimport com.trendyol.stove.examples.kotlin.ktor.application.product.command.CreateProductCommand\nimport io.ktor.server.application.*\nimport io.ktor.server.request.*\nimport io.ktor.server.response.*\nimport io.ktor.server.routing.*\nimport org.koin.ktor.ext.get\n\nfun Routing.productApi() {\n  post(\"/products\") {\n    val mediator = call.get<Mediator>()\n    val req = call.receive<ProductCreateRequest>()\n    mediator.send(CreateProductCommand(req.name, req.price, req.categoryId))\n    call.respond(\"Product created\")\n  }\n}\n\ndata class ProductCreateRequest(\n  var name: String,\n  var price: Double,\n  var categoryId: Int\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/product/defs.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.product\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.product.command.registerProductCommandHandling\nimport com.trendyol.stove.examples.kotlin.ktor.domain.product.ProductRepository\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.ConsumerSupervisor\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.messaging.ProductAggregateRootEventsConsumer\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.persistency.PostgresProductRepository\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.*\n\nfun KoinApplication.registerProductComponents() {\n  modules(\n    module {\n      single { PostgresProductRepository(get()) }.bind<ProductRepository>()\n      single { ProductAggregateRootEventsConsumer(get(), get()) }.bind<ConsumerSupervisor<*, *>>()\n    }\n  )\n  registerProductCommandHandling()\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/product/messaging/ProductAggregateRootEventsConsumer.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.product.messaging\n\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.*\nimport io.github.nomisRev.kafka.receiver.KafkaReceiver\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport org.apache.kafka.clients.consumer.ConsumerRecord\n\nclass ProductAggregateRootEventsConsumer(\n  topicResolver: TopicResolver,\n  kafkaReceiver: KafkaReceiver<String, Any>,\n  topic: Topic = topicResolver(\"product\")\n) : ConsumerSupervisor<String, Any>(kafkaReceiver, topic.concurrency) {\n  private val logger = KotlinLogging.logger { }\n  override val topics: List<String> = listOf(topic.name, topic.retry)\n\n  override suspend fun consume(record: ConsumerRecord<String, Any>) {\n    logger.info { \"consumed record: $record\" }\n  }\n\n  override fun handleError(e: Exception, record: ConsumerRecord<String, Any>) {\n    logger.error(e) { \"Error while processing record: $record\" }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/product/persistency/PostgresProductRepository.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.product.persistency\n\nimport arrow.core.*\nimport com.trendyol.stove.examples.domain.product.Product\nimport com.trendyol.stove.examples.kotlin.ktor.domain.product.ProductRepository\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.kafka.KafkaDomainEventPublisher\nimport kotlinx.coroutines.flow.*\nimport org.jetbrains.exposed.v1.core.eq\nimport org.jetbrains.exposed.v1.r2dbc.*\nimport org.jetbrains.exposed.v1.r2dbc.transactions.suspendTransaction\n\nclass PostgresProductRepository(\n  private val eventPublisher: KafkaDomainEventPublisher\n) : ProductRepository {\n  override suspend fun save(product: Product) = suspendTransaction {\n    if (product.isNew) {\n      saveInternal(product)\n    } else {\n      update(product)\n    }\n    eventPublisher.publishFor(product)\n  }\n\n  private suspend fun saveInternal(product: Product) {\n    ProductTable.insert {\n      it[id] = product.id\n      it[name] = product.name\n      it[price] = product.price\n      it[categoryId] = product.categoryId\n    }\n  }\n\n  private suspend fun update(product: Product) {\n    val updatedRows = ProductTable.update({ ProductTable.id eq product.id }) {\n      it[name] = product.name\n      it[price] = product.price\n      it[categoryId] = product.categoryId\n      it[version] = product.version\n    }\n    if (updatedRows == 0) {\n      error(\"Product with id ${product.id} was updated concurrently.\")\n    }\n  }\n\n  override suspend fun findById(\n    id: String\n  ): Option<Product> = suspendTransaction {\n    ProductTable\n      .selectAll()\n      .where { ProductTable.id eq id }\n      .map {\n        Product.fromPersistency(\n          it[ProductTable.id],\n          it[ProductTable.name],\n          it[ProductTable.price],\n          it[ProductTable.categoryId],\n          it[ProductTable.version]\n        )\n      }.firstOrNull()\n      .toOption()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/components/product/persistency/Product.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.components.product.persistency\n\nimport org.jetbrains.exposed.v1.core.Table\n\n/**\n * [com.trendyol.stove.examples.domain.product.Product]\n */\nobject ProductTable : Table(\"products\") {\n  val id = text(\"id\")\n  val name = text(\"name\")\n  val price = double(\"price\")\n  val categoryId = integer(\"category_id\")\n  val version = long(\"version\")\n\n  override val primaryKey = PrimaryKey(id)\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/postgres/flyway.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.postgres\n\nimport io.ktor.server.application.*\nimport org.flywaydb.core.Flyway\nimport org.koin.ktor.ext.get\n\nfun Application.configureFlyway() {\n  this.monitor.subscribe(ApplicationStarted) {\n    val logger = environment.log\n    val options = get<R2dbcProperties>()\n    if (options.flyway.enabled) {\n      logger.info(\"Flyway enabled, starting migration...\")\n      val flyway = get<Flyway>()\n      flyway.migrate()\n    } else {\n      logger.info(\"Flyway disabled, skipping migration...\")\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/kotlin/com/trendyol/stove/examples/kotlin/ktor/infra/postgres/postgres.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.infra.postgres\n\nimport org.flywaydb.core.Flyway\nimport org.jetbrains.exposed.v1.r2dbc.R2dbcDatabase\nimport org.koin.core.KoinApplication\nimport org.koin.dsl.module\nimport org.postgresql.ds.PGSimpleDataSource\n\ndata class R2dbcProperties(\n  val url: String,\n  val username: String,\n  val password: String,\n  val flyway: Flyway,\n  val driverClassName: String = \"postgresql\"\n) {\n  fun jdbcUrl(): String = url.replace(\"r2dbc:\", \"jdbc:\")\n\n  data class Flyway(\n    val enabled: Boolean,\n    val logLevel: String = \"INFO\",\n    val table: String = \"flyway_schema_history\",\n    val locations: String = \"classpath:db/migration\"\n  )\n}\n\nfun KoinApplication.configurePostgres() {\n  modules(postgresModule())\n}\n\nprivate fun postgresModule() = module {\n  single(createdAtStart = true) { exposedDatabase(get()) }\n  single { flyway(get()) }\n}\n\nfun exposedDatabase(\n  postgresDbConfiguration: R2dbcProperties\n): R2dbcDatabase = R2dbcDatabase.connect(\n  url = postgresDbConfiguration.url,\n  driver = postgresDbConfiguration.driverClassName,\n  user = postgresDbConfiguration.username,\n  password = postgresDbConfiguration.password\n)\n\nfun flyway(\n  r2dbcProperties: R2dbcProperties\n): Flyway {\n  val dataSource = PGSimpleDataSource().apply {\n    setURL(r2dbcProperties.jdbcUrl())\n    user = r2dbcProperties.username\n    password = r2dbcProperties.password\n  }\n  return Flyway\n    .configure()\n    .dataSource(dataSource)\n    .locations(r2dbcProperties.flyway.locations)\n    .baselineOnMigrate(true)\n    .baselineVersion(\"0\")\n    .loggers(\"slf4j\")\n    .load()\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/resources/application.yaml",
    "content": "server:\n  port: 8082\n  host: \"localhost\"\n  name: \"test\"\n\ndb:\n  url: r2dbc:postgresql://localhost:5432/stove-kotlin-ktor\n  password: password\n  user: admin\n  flyway:\n    enabled: true\n    locations: classpath:db/migration\n    table: flyway_schema_history\n\nkafka:\n  bootstrap-servers: localhost:9092\n  group-id: stove-kotlin-ktor\n  heartbeat-interval-seconds: 2\n  request-timeout-seconds: 30\n  session-timeout-seconds: 10\n  auto-create-topics: true\n  auto-offset-reset: earliest\n  interceptor-classes: [ ]\n  topics:\n    product:\n      name: stove-kotlin-ktor.product\n      retry: stove-kotlin-ktor.retry\n      dead-letter: stove-kotlin-ktor.error\n      concurrency: 2\n      maxRetry: 1\n\nexternal-apis:\n  category:\n    url: http://localhost:9095\n    timeout: 30\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/main/resources/db/migration/V1__init.sql",
    "content": "create table products\n(\n    id          text primary key,\n    name        varchar(100)   not null,\n    price       numeric(10, 2) not null,\n    category_id integer        not null,\n    version     bigint         not null default 0\n);\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/setup/StoveConfig.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.setup\n\nimport com.trendyol.stove.examples.kotlin.ktor.ExampleStoveKtorApp\nimport com.trendyol.stove.examples.kotlin.ktor.infra.boilerplate.serialization.JacksonConfiguration\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.ktor.*\nimport com.trendyol.stove.postgres.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.ktor.serialization.jackson.*\nimport org.koin.dsl.module\n\nprivate const val DATABASE = \"stove-kotlin-ktor\"\n\nclass StoveConfig : AbstractProjectConfig() {\n  init {\n    stoveKafkaBridgePortDefault = \"50054\"\n  }\n\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:8082\",\n            contentConverter = JacksonConverter(JacksonConfiguration.default)\n          )\n        }\n        bridge()\n        wiremock {\n          WireMockSystemOptions(\n            port = 9095\n          )\n        }\n        kafka {\n          KafkaSystemOptions(\n            serde = StoveSerde.jackson.anyByteArraySerde(JacksonConfiguration.default),\n            containerOptions = KafkaContainerOptions(tag = \"8.0.3\"),\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"kafka.bootstrapServers=${cfg.bootstrapServers}\",\n                \"kafka.interceptor-classes=${cfg.interceptorClass}\"\n              )\n            }\n          )\n        }\n        postgresql {\n          PostgresqlOptions(\n            databaseName = DATABASE,\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"db.url=${toR2dbcUrl(cfg.jdbcUrl)}\",\n                \"db.username=${cfg.username}\",\n                \"db.password=${cfg.password}\",\n                \"db.flyway.enabled=true\",\n                \"db.flyway.logLevel=INFO\"\n              )\n            }\n          )\n        }\n        ktor(\n          runner = { parameters ->\n            ExampleStoveKtorApp.run(\n              parameters,\n              wait = false,\n              module {\n              }\n            )\n          },\n          withParameters = listOf(\n            \"server.name=${Thread.currentThread().name}\"\n          )\n        )\n      }.run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n\n  private fun toR2dbcUrl(url: String): String = url.replace(\"jdbc:\", \"r2dbc:\")\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/setup/TestData.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.setup\n\nimport com.trendyol.stove.examples.domain.product.Product\nimport kotliquery.Row\n\nobject TestData {\n  object Random {\n    fun positiveInt() = kotlin.random.Random.nextInt(1, Int.MAX_VALUE)\n  }\n}\n\nobject ProductFrom {\n  operator fun invoke(row: Row): Product = Product.fromPersistency(\n    row.string(\"id\"),\n    row.string(\"name\"),\n    row.double(\"price\"),\n    row.int(\"category_id\"),\n    row.long(\"version\")\n  )\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/tests/IndexTests.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.tests\n\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass IndexTests :\n  FunSpec({\n    test(\"Index page should return 200\") {\n      stove {\n        http {\n          getResponse<String>(\"/\") { actual ->\n            actual.status shouldBe 200\n            actual.body() shouldBe \"Hello, World!\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/tests/configuration/ConfigurationTests.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.tests.configuration\n\nimport com.trendyol.stove.examples.kotlin.ktor.application.RecipeAppConfig\nimport com.trendyol.stove.system.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldNotBe\n\nclass ConfigurationTests :\n  FunSpec({\n    test(\"configuration can be changed from app\") {\n      stove {\n        using<RecipeAppConfig> {\n          this.server.name shouldNotBe \"test\"\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/ktor/e2e/tests/product/CreateTests.kt",
    "content": "package com.trendyol.stove.examples.kotlin.ktor.e2e.tests.product\n\nimport arrow.core.*\nimport com.trendyol.stove.examples.domain.product.Product\nimport com.trendyol.stove.examples.domain.product.events.ProductCreatedEvent\nimport com.trendyol.stove.examples.kotlin.ktor.domain.product.ProductRepository\nimport com.trendyol.stove.examples.kotlin.ktor.e2e.setup.*\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.api.ProductCreateRequest\nimport com.trendyol.stove.examples.kotlin.ktor.infra.components.product.persistency.ProductTable\nimport com.trendyol.stove.functional.get\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.recipes.shared.application.category.CategoryApiResponse\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport java.util.*\nimport kotlin.time.Duration.Companion.seconds\n\nclass CreateTests :\n  FunSpec({\n    test(\"product can be created with valid category\") {\n      stove {\n        val productName = TestData.Random.positiveInt().toString()\n        val productId = UUID.nameUUIDFromBytes(productName.toByteArray())\n\n        val categoryApiResponse = CategoryApiResponse(\n          TestData.Random.positiveInt(),\n          \"category-name\",\n          true\n        )\n\n        wiremock {\n          mockGet(\n            url = \"/categories/${categoryApiResponse.id}\",\n            statusCode = 200,\n            responseBody = categoryApiResponse.some()\n          )\n        }\n\n        http {\n          val req = ProductCreateRequest(productName, 100.0, categoryApiResponse.id)\n          postAndExpectBody<Any>(\"/products\", body = req.some()) { actual ->\n            actual.status shouldBe 200\n          }\n        }\n\n        postgresql {\n          shouldQuery<Product>(\n            \"SELECT * FROM ${ProductTable.tableName} WHERE ${ProductTable.id.name} = '$productId'\",\n            parameters = emptyList(),\n            mapper = ProductFrom::invoke\n          ) { actual ->\n            actual.size shouldBe 1\n            actual[0].name shouldBe productName\n            actual[0].price shouldBe 100.0\n          }\n        }\n\n        using<ProductRepository> {\n          val product = findById(productId.toString()).get()\n          product.name shouldBe productName\n          product.price shouldBe 100.0\n          product.categoryId shouldBe categoryApiResponse.id\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent>(10.seconds) {\n            actual.price == 100.0 && actual.name == productName\n          }\n\n          shouldBeConsumed<ProductCreatedEvent>(10.seconds) {\n            actual.price == 100.0 && actual.name == productName\n          }\n        }\n      }\n    }\n\n    test(\"when category is not active, product creation should fail\") {\n      stove {\n        val productName = TestData.Random.positiveInt().toString()\n        val productId = UUID.nameUUIDFromBytes(productName.toByteArray())\n        val categoryApiResponse = CategoryApiResponse(\n          TestData.Random.positiveInt(),\n          \"category-name\",\n          false\n        )\n\n        wiremock {\n          mockGet(\n            url = \"/categories/${categoryApiResponse.id}\",\n            statusCode = 200,\n            responseBody = categoryApiResponse.some()\n          )\n        }\n\n        http {\n          val req = ProductCreateRequest(productName, 100.0, categoryApiResponse.id)\n          postAndExpectBody<Any>(\"/products\", body = req.some()) { actual ->\n            actual.status shouldBe 409\n          }\n        }\n\n        postgresql {\n          shouldQuery<Product>(\n            \"SELECT * FROM ${ProductTable.tableName} WHERE ${ProductTable.id.name} = '$productId'\",\n            parameters = emptyList(),\n            ProductFrom::invoke\n          ) { actual ->\n            actual.size shouldBe 0\n          }\n        }\n\n        using<ProductRepository> {\n          findById(productId.toString()) shouldBe None\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/test-e2e/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.examples.kotlin.ktor.e2e.setup.StoveConfig\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/ktor-postgres-recipe/src/test-e2e/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"ERROR\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/build.gradle.kts",
    "content": "plugins {\n  alias(libs.plugins.spring.plugin)\n  alias(libs.plugins.protobuf)\n  id(\"com.trendyol.stove.tracing\") version libs.versions.stove.get()\n}\n\ndependencies {\n  // Spring Boot\n  implementation(libs.spring.boot.webflux)\n  implementation(libs.spring.boot.autoconfigure)\n  implementation(libs.spring.boot.data.r2dbc)\n  implementation(libs.spring.boot.kafka)\n\n  // Database\n  implementation(libs.postgresql.r2dbc)\n  implementation(libs.r2dbc.pool)\n  implementation(libs.spring.boot.starter.jdbc) // Required for db-scheduler\n  implementation(libs.postgresql) // JDBC driver for db-scheduler\n\n  // Scheduling\n  implementation(libs.db.scheduler.spring.boot.starter)\n\n  // Kotlin Coroutines\n  implementation(libs.kotlinx.core)\n  implementation(libs.kotlinx.reactive)\n  implementation(libs.kotlinx.reactor)\n  implementation(libs.kotlinx.jdk8)\n\n  // Logging\n  implementation(libs.kotlin.logging.jvm)\n\n  // OpenTelemetry - for production-grade tracing\n  implementation(libs.opentelemetry.extension.kotlin)\n  implementation(libs.opentelemetry.instrumentation.annotations)\n\n  // gRPC\n  implementation(libs.grpc.protobuf)\n  implementation(libs.grpc.stub)\n  implementation(libs.grpc.netty)\n  implementation(libs.grpc.kotlin.stub)\n  implementation(libs.protobuf.kotlin)\n\n  annotationProcessor(libs.spring.boot.annotationProcessor)\n}\n\ndependencies {\n  // Stove Testing\n  testImplementation(stoveLibs.stove)\n  testImplementation(stoveLibs.stovePostgres)\n  testImplementation(stoveLibs.stoveHttp)\n  testImplementation(stoveLibs.stoveWiremock)\n  testImplementation(stoveLibs.stoveKafka)\n  testImplementation(stoveLibs.stoveSpring)\n  testImplementation(stoveLibs.stoveTracing)\n  testImplementation(stoveLibs.stoveGrpc)      // For testing our gRPC server\n  testImplementation(stoveLibs.stoveGrpcMock)  // For mocking external gRPC services\n  testImplementation(stoveLibs.stoveExtensionsKotest)\n\n  // Ktor client for streaming tests\n  testImplementation(libs.ktor.client.websockets)\n  testImplementation(libs.ktor.client.okhttp)\n  testImplementation(libs.ktor.client.content.negotiation)\n  testImplementation(libs.ktor.serialization.jackson.json)\n\n  // Testcontainers\n  testImplementation(libs.testcontainers.kafka)\n}\n\n// ============================================================================\n// PROTOBUF CONFIGURATION - gRPC Code Generation\n// ============================================================================\nprotobuf {\n  protoc {\n    artifact = libs.protoc.get().toString()\n  }\n  plugins {\n    create(\"grpc\") {\n      artifact = libs.grpc.protoc.gen.java.get().toString()\n    }\n    create(\"grpckt\") {\n      artifact = \"${libs.grpc.protoc.gen.kotlin.get()}:jdk8@jar\"\n    }\n  }\n  generateProtoTasks {\n    all().forEach { task ->\n      task.plugins {\n        create(\"grpc\")\n        create(\"grpckt\")\n      }\n      task.builtins {\n        create(\"kotlin\")\n      }\n    }\n  }\n}\n\n// ============================================================================\n// TRACING SETUP - OpenTelemetry Java Agent\n// ============================================================================\nstoveTracing {\n  serviceName.set(\"stove-kotlin-spring-showcase\")\n  testTaskNames.set(listOf(\"e2eTest\"))\n  otelAgentVersion.set(libs.opentelemetry.instrumentation.annotations.get().version!!)\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/ExampleStoveSpringBootApp.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring\n\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.*\nimport org.springframework.boot.*\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.context.ConfigurableApplicationContext\nimport org.springframework.http.MediaType\nimport org.springframework.web.bind.annotation.*\n\n@SpringBootApplication\nclass ExampleStoveSpringBootApp\n\nfun main(args: Array<String>) {\n  run(args)\n}\n\nfun run(args: Array<String>, init: SpringApplication.() -> Unit = {}): ConfigurableApplicationContext =\n  runApplication<ExampleStoveSpringBootApp>(*args) {\n    init()\n  }\n\ndata class ExampleData(\n  val id: Int,\n  val name: String\n)\n\n@RestController\n@RequestMapping(\"/api/streaming\")\nclass StreamingController {\n  @GetMapping(\n    \"json\",\n    produces = [\n      MediaType.APPLICATION_NDJSON_VALUE\n    ]\n  )\n  fun json(\n    @RequestParam load: Int = 100,\n    @RequestParam delay: Long = 1\n  ): Flow<ExampleData> = (1..load)\n    .asFlow()\n    .onEach { delay(delay) }\n    .map { ExampleData(it, \"name$it\") }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/domain/order/Order.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.domain.order\n\nimport java.time.Instant\nimport java.util.UUID\n\ndata class Order(\n  val id: String = UUID.randomUUID().toString(),\n  val userId: String,\n  val productId: String,\n  val amount: Double,\n  val status: OrderStatus = OrderStatus.PENDING,\n  val paymentTransactionId: String? = null,\n  val createdAt: Instant = Instant.now()\n) {\n  fun confirm(paymentTransactionId: String): Order = copy(\n    status = OrderStatus.CONFIRMED,\n    paymentTransactionId = paymentTransactionId\n  )\n\n  fun fail(): Order = copy(status = OrderStatus.FAILED)\n}\n\nenum class OrderStatus {\n  PENDING,\n  CONFIRMED,\n  FAILED\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/domain/order/OrderController.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.domain.order\n\nimport org.springframework.http.HttpStatus\nimport org.springframework.web.bind.annotation.*\n\n@RestController\n@RequestMapping(\"/api/orders\")\nclass OrderController(\n  private val orderService: OrderService\n) {\n  @PostMapping\n  @ResponseStatus(HttpStatus.CREATED)\n  suspend fun createOrder(\n    @RequestBody request: CreateOrderRequest\n  ): OrderResponse {\n    val order = orderService.createOrder(\n      userId = request.userId,\n      productId = request.productId,\n      amount = request.amount\n    )\n    return OrderResponse(\n      orderId = order.id,\n      userId = order.userId,\n      productId = order.productId,\n      amount = order.amount,\n      status = order.status.name\n    )\n  }\n\n  @GetMapping(\"/{id}\")\n  suspend fun getOrder(\n    @PathVariable id: String\n  ): OrderResponse? {\n    val order = orderService.getOrder(id) ?: return null\n    return OrderResponse(\n      orderId = order.id,\n      userId = order.userId,\n      productId = order.productId,\n      amount = order.amount,\n      status = order.status.name\n    )\n  }\n}\n\ndata class CreateOrderRequest(\n  val userId: String,\n  val productId: String,\n  val amount: Double\n)\n\ndata class OrderResponse(\n  val orderId: String,\n  val userId: String,\n  val productId: String,\n  val amount: Double,\n  val status: String\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/domain/order/OrderRepository.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.domain.order\n\ninterface OrderRepository {\n  suspend fun save(order: Order): Order\n\n  suspend fun findById(id: String): Order?\n\n  suspend fun findByUserId(userId: String): List<Order>\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/domain/order/OrderService.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.domain.order\n\nimport com.trendyol.stove.examples.kotlin.spring.events.OrderCreatedEvent\nimport com.trendyol.stove.examples.kotlin.spring.events.PaymentProcessedEvent\nimport com.trendyol.stove.examples.kotlin.spring.infra.clients.FraudDetectedException\nimport com.trendyol.stove.examples.kotlin.spring.infra.clients.FraudDetectionClient\nimport com.trendyol.stove.examples.kotlin.spring.infra.clients.InventoryClient\nimport com.trendyol.stove.examples.kotlin.spring.infra.clients.InventoryNotAvailableException\nimport com.trendyol.stove.examples.kotlin.spring.infra.clients.PaymentClient\nimport com.trendyol.stove.examples.kotlin.spring.infra.clients.PaymentFailedException\nimport com.trendyol.stove.examples.kotlin.spring.infra.clients.PaymentResult\nimport com.trendyol.stove.examples.kotlin.spring.infra.kafka.OrderEventPublisher\nimport com.trendyol.stove.examples.kotlin.spring.infra.scheduling.EmailSchedulerService\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport org.springframework.stereotype.Service\nimport java.util.UUID\n\nprivate val logger = KotlinLogging.logger {}\n\n/**\n * Order Service - The main orchestrator for order creation.\n *\n * Each step maps directly to a Stove DSL section in TheShowcase.kt:\n *\n * ┌─────────────────────────────────────────────────────────────────────┐\n * │ SERVICE METHOD                  │  STOVE DSL                        │\n * ├─────────────────────────────────────────────────────────────────────┤\n * │ 1. checkFraudViaGrpc()          │  grpcMock { mockUnary(...) }      │\n * │ 2. checkInventoryViaRest()      │  wiremock { mockGet(...) }        │\n * │ 3. processPaymentViaRest()      │  wiremock { mockPost(...) }       │\n * │ 4. saveOrderToDatabase()        │  postgresql { shouldQuery(...) }  │\n * │ 5. publishEventsToKafka()       │  kafka { shouldBePublished(...) } │\n * │ 6. scheduleConfirmationEmail()  │  tasks { shouldBeExecuted(...) }  │\n * └─────────────────────────────────────────────────────────────────────┘\n */\n@Service\nclass OrderService(\n  private val orderRepository: OrderRepository,\n  private val inventoryClient: InventoryClient,\n  private val paymentClient: PaymentClient,\n  private val fraudDetectionClient: FraudDetectionClient,\n  private val eventPublisher: OrderEventPublisher,\n  private val emailSchedulerService: EmailSchedulerService\n) {\n  /**\n   * Creates a new order - the main flow that demonstrates all integrations.\n   *\n   * Flow:\n   * 1. Check fraud via gRPC        → Stove: grpcMock {}\n   * 2. Check inventory via REST    → Stove: wiremock {}\n   * 3. Process payment via REST    → Stove: wiremock {}\n   * 4. Save order to database      → Stove: postgresql {}\n   * 5. Publish events to Kafka     → Stove: kafka {}\n   * 6. Schedule confirmation email → Stove: tasks {}\n   */\n  @WithSpan(\"OrderService.createOrder\")\n  suspend fun createOrder(\n    userId: String,\n    productId: String,\n    amount: Double\n  ): Order {\n    logger.info { \"═══ Creating order for user=$userId, product=$productId, amount=$amount ═══\" }\n    val orderId = UUID.randomUUID().toString()\n\n    // Step 1: Check fraud via gRPC service → grpcMock { mockUnary(...) }\n    checkFraudViaGrpc(orderId, userId, amount, productId)\n\n    // Step 2: Check inventory via REST API → wiremock { mockGet(...) }\n    checkInventoryViaRest(productId)\n\n    // Step 3: Process payment via REST API → wiremock { mockPost(...) }\n    val payment = processPaymentViaRest(userId, amount)\n\n    // Step 4: Save order to database → postgresql { shouldQuery(...) }\n    val savedOrder = saveOrderToDatabase(orderId, userId, productId, amount, payment.transactionId!!)\n\n    // Step 5: Publish events to Kafka → kafka { shouldBePublished(...) }\n    publishEventsToKafka(savedOrder, payment.transactionId)\n\n    // Step 6: Schedule confirmation email → tasks { shouldBeExecuted(...) }\n    scheduleConfirmationEmail(savedOrder)\n\n    logger.info { \"═══ Order completed: id=${savedOrder.id}, status=${savedOrder.status} ═══\" }\n    return savedOrder\n  }\n\n  // ════════════════════════════════════════════════════════════════════════════\n  // Step 1: gRPC Integration → Tested with: grpcMock { mockUnary(...) }\n  // ════════════════════════════════════════════════════════════════════════════\n\n  @WithSpan(\"OrderService.checkFraudViaGrpc\")\n  private suspend fun checkFraudViaGrpc(\n    orderId: String,\n    userId: String,\n    amount: Double,\n    productId: String\n  ) {\n    logger.info { \"→ Checking fraud via gRPC for order=$orderId\" }\n    val fraudCheck = fraudDetectionClient.checkFraud(orderId, userId, amount, productId)\n    if (fraudCheck.isFraudulent) {\n      logger.warn { \"✗ Fraud detected: reason=${fraudCheck.reason}\" }\n      throw FraudDetectedException(fraudCheck.reason)\n    }\n    logger.info { \"✓ Fraud check passed: riskScore=${fraudCheck.riskScore}\" }\n  }\n\n  // ════════════════════════════════════════════════════════════════════════════\n  // Step 2: REST Integration (Inventory) → Tested with: wiremock { mockGet(...) }\n  // ════════════════════════════════════════════════════════════════════════════\n\n  @WithSpan(\"OrderService.checkInventoryViaRest\")\n  private suspend fun checkInventoryViaRest(productId: String) {\n    logger.info { \"→ Checking inventory via REST for product=$productId\" }\n    val inventory = inventoryClient.checkAvailability(productId)\n    if (!inventory.available) {\n      logger.warn { \"✗ Inventory not available for product=$productId\" }\n      throw InventoryNotAvailableException(productId)\n    }\n    logger.info { \"✓ Inventory available: quantity=${inventory.quantity}\" }\n  }\n\n  // ════════════════════════════════════════════════════════════════════════════\n  // Step 3: REST Integration (Payment) → Tested with: wiremock { mockPost(...) }\n  // ════════════════════════════════════════════════════════════════════════════\n\n  @WithSpan(\"OrderService.processPaymentViaRest\")\n  private suspend fun processPaymentViaRest(userId: String, amount: Double): PaymentResult {\n    logger.info { \"→ Processing payment via REST for user=$userId, amount=$amount\" }\n    val payment = paymentClient.charge(userId, amount)\n    if (!payment.success) {\n      logger.error { \"✗ Payment failed: reason=${payment.errorMessage}\" }\n      throw PaymentFailedException(payment.errorMessage ?: \"Unknown error\")\n    }\n    logger.info { \"✓ Payment successful: transactionId=${payment.transactionId}\" }\n    return payment\n  }\n\n  // ════════════════════════════════════════════════════════════════════════════\n  // Step 4: Database → Tested with: postgresql { shouldQuery(...) }\n  // ════════════════════════════════════════════════════════════════════════════\n\n  @WithSpan(\"OrderService.saveOrderToDatabase\")\n  private suspend fun saveOrderToDatabase(\n    orderId: String,\n    userId: String,\n    productId: String,\n    amount: Double,\n    transactionId: String\n  ): Order {\n    logger.info { \"→ Saving order to database: id=$orderId\" }\n    val order = Order(\n      id = orderId,\n      userId = userId,\n      productId = productId,\n      amount = amount\n    ).confirm(transactionId)\n\n    val savedOrder = orderRepository.save(order)\n    logger.info { \"✓ Order saved: id=${savedOrder.id}, status=${savedOrder.status}\" }\n    return savedOrder\n  }\n\n  // ════════════════════════════════════════════════════════════════════════════\n  // Step 5: Kafka → Tested with: kafka { shouldBePublished(...) }\n  // ════════════════════════════════════════════════════════════════════════════\n\n  @WithSpan(\"OrderService.publishEventsToKafka\")\n  private suspend fun publishEventsToKafka(order: Order, transactionId: String) {\n    logger.info { \"→ Publishing events to Kafka for order=${order.id}\" }\n\n    eventPublisher.publish(\n      OrderCreatedEvent(\n        orderId = order.id,\n        userId = order.userId,\n        productId = order.productId,\n        amount = order.amount\n      )\n    )\n    logger.info { \"  ✓ OrderCreatedEvent published\" }\n\n    eventPublisher.publish(\n      PaymentProcessedEvent(\n        orderId = order.id,\n        transactionId = transactionId,\n        amount = order.amount,\n        success = true\n      )\n    )\n    logger.info { \"  ✓ PaymentProcessedEvent published\" }\n  }\n\n  // ════════════════════════════════════════════════════════════════════════════\n  // Step 6: db-scheduler → Tested with: tasks { shouldBeExecuted(...) }\n  // ════════════════════════════════════════════════════════════════════════════\n\n  @WithSpan(\"OrderService.scheduleConfirmationEmail\")\n  private suspend fun scheduleConfirmationEmail(order: Order) {\n    logger.info { \"→ Scheduling confirmation email for order=${order.id}\" }\n    emailSchedulerService.scheduleOrderConfirmationEmail(\n      orderId = order.id,\n      userId = order.userId,\n      amount = order.amount,\n      productId = order.productId\n    )\n    logger.info { \"  ✓ Email task scheduled\" }\n  }\n\n  @WithSpan(\"OrderService.getOrder\")\n  suspend fun getOrder(id: String): Order? = orderRepository.findById(id)\n\n  @WithSpan(\"OrderService.getOrderByUserId\")\n  suspend fun getOrderByUserId(userId: String): Order? =\n    orderRepository.findByUserId(userId).firstOrNull()\n\n  @WithSpan(\"OrderService.getOrdersByUserId\")\n  suspend fun getOrdersByUserId(userId: String): List<Order> =\n    orderRepository.findByUserId(userId)\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/domain/statistics/UserOrderStatistics.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.domain.statistics\n\nimport java.time.Instant\n\n/**\n * Read model for user order statistics.\n * Updated asynchronously when OrderCreatedEvent is consumed.\n */\ndata class UserOrderStatistics(\n  val userId: String,\n  val totalOrders: Int = 0,\n  val totalAmount: Double = 0.0,\n  val lastOrderAt: Instant? = null\n) {\n  fun addOrder(amount: Double, orderTime: Instant): UserOrderStatistics = copy(\n    totalOrders = totalOrders + 1,\n    totalAmount = totalAmount + amount,\n    lastOrderAt = orderTime\n  )\n}\n\ninterface UserOrderStatisticsRepository {\n  suspend fun findByUserId(userId: String): UserOrderStatistics?\n\n  suspend fun save(statistics: UserOrderStatistics): UserOrderStatistics\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/events/OrderCreatedEvent.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.events\n\nimport java.time.Instant\n\ndata class OrderCreatedEvent(\n  val orderId: String,\n  val userId: String,\n  val productId: String,\n  val amount: Double,\n  val createdAt: Instant = Instant.now()\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/events/PaymentProcessedEvent.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.events\n\nimport java.time.Instant\n\ndata class PaymentProcessedEvent(\n  val orderId: String,\n  val transactionId: String,\n  val amount: Double,\n  val success: Boolean,\n  val createdAt: Instant = Instant.now()\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/GlobalErrorHandler.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra\n\nimport com.trendyol.stove.examples.kotlin.spring.infra.clients.*\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.api.trace.*\nimport org.springframework.http.*\nimport org.springframework.web.bind.annotation.*\n\nprivate val logger = KotlinLogging.logger {}\n\n@RestControllerAdvice\nclass GlobalErrorHandler {\n  @ExceptionHandler(InventoryNotAvailableException::class)\n  fun handleInventoryNotAvailable(ex: InventoryNotAvailableException): ResponseEntity<ErrorResponse> {\n    logger.warn(ex) { \"Inventory not available\" }\n    Span.current().apply {\n      recordException(ex)\n      setStatus(StatusCode.ERROR, ex.message ?: \"Unknown error\")\n    }\n    return ResponseEntity\n      .status(HttpStatus.CONFLICT)\n      .body(ErrorResponse(message = ex.message ?: \"Inventory not available\", errorCode = \"INVENTORY_NOT_AVAILABLE\"))\n  }\n\n  @ExceptionHandler(PaymentFailedException::class)\n  fun handlePaymentFailed(ex: PaymentFailedException): ResponseEntity<ErrorResponse> {\n    logger.error(ex) { \"Payment failed\" }\n    Span.current().apply {\n      recordException(ex)\n      setStatus(StatusCode.ERROR, ex.message ?: \"Unknown error\")\n    }\n    return ResponseEntity\n      .status(HttpStatus.BAD_GATEWAY)\n      .body(ErrorResponse(message = ex.message ?: \"Payment failed\", errorCode = \"PAYMENT_FAILED\"))\n  }\n\n  @ExceptionHandler(Exception::class)\n  fun handleGenericException(ex: Exception): ResponseEntity<ErrorResponse> {\n    logger.error(ex) { \"Unexpected error occurred\" }\n    Span.current().apply {\n      recordException(ex)\n      setStatus(StatusCode.ERROR, ex.message ?: \"Unknown error\")\n    }\n    return ResponseEntity\n      .status(HttpStatus.INTERNAL_SERVER_ERROR)\n      .body(ErrorResponse(message = \"Internal server error\", errorCode = \"INTERNAL_ERROR\"))\n  }\n}\n\ndata class ErrorResponse(\n  val message: String,\n  val errorCode: String\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/clients/FraudDetectionClient.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.clients\n\nimport com.trendyol.stove.examples.kotlin.spring.grpc.CheckFraudRequest\nimport com.trendyol.stove.examples.kotlin.spring.grpc.CheckFraudResponse\nimport com.trendyol.stove.examples.kotlin.spring.grpc.FraudDetectionServiceGrpcKt\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.grpc.ManagedChannel\nimport io.grpc.ManagedChannelBuilder\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport jakarta.annotation.PreDestroy\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Component\n\nprivate val logger = KotlinLogging.logger {}\n\n@Component\nclass FraudDetectionClient(\n  @param:Value(\"\\${external-apis.fraud-detection.host}\") private val host: String,\n  @param:Value(\"\\${external-apis.fraud-detection.port}\") private val port: Int\n) {\n  private val channel: ManagedChannel = ManagedChannelBuilder\n    .forAddress(host, port)\n    .usePlaintext()\n    .build()\n\n  private val stub = FraudDetectionServiceGrpcKt.FraudDetectionServiceCoroutineStub(channel)\n\n  @WithSpan(\"FraudDetectionClient.checkFraud\")\n  suspend fun checkFraud(\n    orderId: String,\n    userId: String,\n    amount: Double,\n    productId: String\n  ): FraudCheckResult {\n    logger.info { \"Checking fraud for order=$orderId, user=$userId, amount=$amount\" }\n\n    val request = CheckFraudRequest\n      .newBuilder()\n      .setOrderId(orderId)\n      .setUserId(userId)\n      .setAmount(amount)\n      .setProductId(productId)\n      .build()\n\n    val response: CheckFraudResponse = stub.checkFraud(request)\n\n    return FraudCheckResult(\n      isFraudulent = response.isFraudulent,\n      riskScore = response.riskScore,\n      reason = response.reason\n    )\n  }\n\n  @PreDestroy\n  fun shutdown() {\n    channel.shutdown()\n  }\n}\n\ndata class FraudCheckResult(\n  val isFraudulent: Boolean,\n  val riskScore: Double,\n  val reason: String\n)\n\nclass FraudDetectedException(\n  reason: String\n) : RuntimeException(\"Order flagged as fraudulent: $reason\")\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/clients/InventoryClient.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.clients\n\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Component\nimport org.springframework.web.reactive.function.client.WebClient\nimport org.springframework.web.reactive.function.client.awaitBody\n\nprivate val logger = KotlinLogging.logger {}\n\n@Component\nclass InventoryClient(\n  @param:Value(\"\\${external-apis.inventory.url}\") private val baseUrl: String\n) {\n  private val webClient = WebClient\n    .builder()\n    .baseUrl(baseUrl)\n    .build()\n\n  @WithSpan(\"InventoryClient.checkAvailability\")\n  suspend fun checkAvailability(productId: String): InventoryResponse {\n    logger.info { \"Checking inventory for product=$productId\" }\n    return webClient\n      .get()\n      .uri(\"/inventory/$productId\")\n      .retrieve()\n      .awaitBody<InventoryResponse>()\n  }\n}\n\ndata class InventoryResponse(\n  val productId: String,\n  val available: Boolean,\n  val quantity: Int = 0\n)\n\nclass InventoryNotAvailableException(\n  productId: String\n) : RuntimeException(\"Inventory not available for product: $productId\")\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/clients/PaymentClient.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.clients\n\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.stereotype.Component\nimport org.springframework.web.reactive.function.client.WebClient\nimport org.springframework.web.reactive.function.client.awaitBody\n\nprivate val logger = KotlinLogging.logger {}\n\n@Component\nclass PaymentClient(\n  @param:Value(\"\\${external-apis.payment.url}\") private val baseUrl: String\n) {\n  private val webClient = WebClient\n    .builder()\n    .baseUrl(baseUrl)\n    .build()\n\n  @WithSpan(\"PaymentClient.charge\")\n  suspend fun charge(userId: String, amount: Double): PaymentResult {\n    logger.info { \"Processing payment for user=$userId, amount=$amount\" }\n    return webClient\n      .post()\n      .uri(\"/payments/charge\")\n      .bodyValue(PaymentRequest(userId, amount))\n      .retrieve()\n      .awaitBody<PaymentResult>()\n  }\n}\n\ndata class PaymentRequest(\n  val userId: String,\n  val amount: Double\n)\n\ndata class PaymentResult(\n  val success: Boolean,\n  val transactionId: String? = null,\n  val amount: Double = 0.0,\n  val errorMessage: String? = null\n)\n\nclass PaymentFailedException(\n  message: String\n) : RuntimeException(\"Payment failed: $message\")\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/grpc/GrpcErrorSpanInterceptor.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.grpc\n\nimport io.grpc.Context\nimport io.grpc.Contexts\nimport io.grpc.ForwardingServerCall\nimport io.grpc.Metadata\nimport io.grpc.ServerCall\nimport io.grpc.ServerCallHandler\nimport io.grpc.ServerInterceptor\nimport io.grpc.Status\nimport io.opentelemetry.api.trace.Span\nimport io.opentelemetry.api.trace.StatusCode\nimport org.springframework.stereotype.Component\n\n/**\n * gRPC server interceptor that records errors on the current OpenTelemetry span.\n *\n * When any gRPC call fails (non-OK status), the error is recorded on the span\n * so it appears in traces for debugging and observability.\n */\n@Component\nclass GrpcErrorSpanInterceptor : ServerInterceptor {\n  override fun <ReqT, RespT> interceptCall(\n    call: ServerCall<ReqT, RespT>,\n    headers: Metadata,\n    next: ServerCallHandler<ReqT, RespT>\n  ): ServerCall.Listener<ReqT> {\n    val wrappedCall = object : ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {\n      override fun close(status: Status, trailers: Metadata) {\n        if (!status.isOk) {\n          Span.current().apply {\n            recordException(status.asRuntimeException())\n            setStatus(StatusCode.ERROR, status.description ?: status.code.name)\n          }\n        }\n        super.close(status, trailers)\n      }\n    }\n    return Contexts.interceptCall(\n      Context.current(),\n      wrappedCall,\n      headers,\n      next\n    )\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/grpc/GrpcServerConfig.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.grpc\n\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.grpc.Server\nimport io.grpc.ServerBuilder\nimport jakarta.annotation.PostConstruct\nimport jakarta.annotation.PreDestroy\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.context.annotation.Configuration\n\nprivate val logger = KotlinLogging.logger {}\n\n/**\n * Configuration for the gRPC server.\n *\n * Starts a gRPC server that exposes the OrderQueryService.\n */\n@Configuration\nclass GrpcServerConfig(\n  private val orderQueryGrpcService: OrderQueryGrpcService,\n  private val grpcErrorSpanInterceptor: GrpcErrorSpanInterceptor,\n  @param:Value(\"\\${grpc.server.port:50051}\") private val port: Int\n) {\n  private lateinit var server: Server\n\n  @PostConstruct\n  fun start() {\n    server = ServerBuilder\n      .forPort(port)\n      .intercept(grpcErrorSpanInterceptor)\n      .addService(orderQueryGrpcService)\n      .build()\n      .start()\n\n    logger.info { \"gRPC server started on port $port\" }\n  }\n\n  @PreDestroy\n  fun stop() {\n    server.shutdown()\n    logger.info { \"gRPC server stopped\" }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/grpc/OrderQueryGrpcService.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.grpc\n\nimport com.trendyol.stove.examples.kotlin.spring.domain.order.OrderRepository\nimport com.trendyol.stove.examples.kotlin.spring.grpc.*\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport org.springframework.stereotype.Service\n\nprivate val logger = KotlinLogging.logger {}\n\n/**\n * gRPC service implementation for querying orders.\n *\n * This demonstrates exposing our application's functionality via gRPC,\n * which Stove can test using the `stove-grpc` module.\n */\n@Service\nclass OrderQueryGrpcService(\n  private val orderRepository: OrderRepository\n) : OrderQueryServiceGrpcKt.OrderQueryServiceCoroutineImplBase() {\n  @WithSpan(\"OrderQueryGrpcService.getOrder\")\n  override suspend fun getOrder(request: GetOrderRequest): GetOrderResponse {\n    logger.info { \"gRPC: GetOrder called for id=${request.orderId}\" }\n\n    val order = orderRepository.findById(request.orderId)\n\n    return if (order != null) {\n      GetOrderResponse\n        .newBuilder()\n        .setFound(true)\n        .setOrder(order.toProto())\n        .build()\n    } else {\n      GetOrderResponse\n        .newBuilder()\n        .setFound(false)\n        .build()\n    }\n  }\n\n  @WithSpan(\"OrderQueryGrpcService.getOrdersByUser\")\n  override suspend fun getOrdersByUser(request: GetOrdersByUserRequest): GetOrdersResponse {\n    logger.info { \"gRPC: GetOrdersByUser called for userId=${request.userId}\" }\n\n    val orders = orderRepository.findByUserId(request.userId)\n\n    return GetOrdersResponse\n      .newBuilder()\n      .addAllOrders(orders.map { it.toProto() })\n      .build()\n  }\n\n  private fun com.trendyol.stove.examples.kotlin.spring.domain.order.Order.toProto(): OrderProto =\n    OrderProto\n      .newBuilder()\n      .setId(id)\n      .setUserId(userId)\n      .setProductId(productId)\n      .setAmount(amount)\n      .setStatus(status.name)\n      .setPaymentTransactionId(paymentTransactionId ?: \"\")\n      .setCreatedAt(createdAt.toEpochMilli())\n      .build()\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/kafka/KafkaConfig.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.kafka\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.trendyol.stove.examples.kotlin.spring.events.OrderCreatedEvent\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.consumer.ConsumerRecord\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.TopicPartition\nimport org.apache.kafka.common.serialization.ByteArraySerializer\nimport org.apache.kafka.common.serialization.StringDeserializer\nimport org.apache.kafka.common.serialization.StringSerializer\nimport org.springframework.boot.autoconfigure.kafka.KafkaProperties\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.ConsumerFactory\nimport org.springframework.kafka.core.DefaultKafkaConsumerFactory\nimport org.springframework.kafka.core.DefaultKafkaProducerFactory\nimport org.springframework.kafka.core.KafkaOperations\nimport org.springframework.kafka.core.KafkaTemplate\nimport org.springframework.kafka.core.ProducerFactory\nimport org.springframework.kafka.listener.DeadLetterPublishingRecoverer\nimport org.springframework.kafka.listener.DefaultErrorHandler\nimport org.springframework.kafka.support.serializer.JsonDeserializer\nimport org.springframework.kafka.support.serializer.JsonSerializer\nimport org.springframework.util.backoff.FixedBackOff\n\nprivate val logger = KotlinLogging.logger {}\n\nprivate const val DLQ_SUFFIX = \".DLT\"\nprivate const val MAX_RETRY_ATTEMPTS = 3L\nprivate const val RETRY_INTERVAL_MS = 1000L\n\n@Configuration\nclass KafkaConfig {\n  @Bean\n  fun producerFactory(\n    kafkaProperties: KafkaProperties,\n    objectMapper: ObjectMapper\n  ): ProducerFactory<String, Any> {\n    val props = kafkaProperties.buildProducerProperties(null).toMutableMap()\n    props[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java\n    props[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = JsonSerializer::class.java\n\n    val factory = DefaultKafkaProducerFactory<String, Any>(props)\n    factory.setValueSerializer(JsonSerializer(objectMapper))\n    return factory\n  }\n\n  @Bean\n  fun kafkaTemplate(\n    producerFactory: ProducerFactory<String, Any>\n  ): KafkaTemplate<String, Any> = KafkaTemplate(producerFactory)\n\n  /**\n   * Dedicated producer factory for Dead Letter Queue.\n   * Uses ByteArraySerializer to forward raw message bytes.\n   */\n  @Bean\n  fun dlqProducerFactory(\n    kafkaProperties: KafkaProperties\n  ): ProducerFactory<String, ByteArray> {\n    val props = kafkaProperties.buildProducerProperties(null).toMutableMap()\n    props[ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java\n    props[ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG] = ByteArraySerializer::class.java\n    return DefaultKafkaProducerFactory(props)\n  }\n\n  @Bean\n  fun dlqKafkaTemplate(\n    dlqProducerFactory: ProducerFactory<String, ByteArray>\n  ): KafkaTemplate<String, ByteArray> = KafkaTemplate(dlqProducerFactory)\n\n  @Bean\n  fun consumerFactory(\n    kafkaProperties: KafkaProperties,\n    objectMapper: ObjectMapper\n  ): ConsumerFactory<String, OrderCreatedEvent> {\n    // buildConsumerProperties includes spring.kafka.consumer.properties.* (e.g., interceptor.classes)\n    val props = kafkaProperties.buildConsumerProperties(null).toMutableMap()\n    props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java\n    props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = JsonDeserializer::class.java\n    // Only set default if not already configured\n    props.putIfAbsent(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \"earliest\")\n\n    val jsonDeserializer = JsonDeserializer(OrderCreatedEvent::class.java, objectMapper)\n    jsonDeserializer.addTrustedPackages(\"*\")\n\n    return DefaultKafkaConsumerFactory(\n      props,\n      StringDeserializer(),\n      jsonDeserializer\n    )\n  }\n\n  /**\n   * Dead Letter Publishing Recoverer - sends failed messages to DLQ topic.\n   * Topic name: original-topic.DLT\n   */\n  @Bean\n  fun deadLetterPublishingRecoverer(\n    dlqKafkaTemplate: KafkaTemplate<String, ByteArray>\n  ): DeadLetterPublishingRecoverer {\n    @Suppress(\"UNCHECKED_CAST\")\n    val recoverer = DeadLetterPublishingRecoverer(\n      dlqKafkaTemplate as KafkaOperations<Any, Any>\n    ) { record: ConsumerRecord<*, *>, _: Exception ->\n      TopicPartition(\"${record.topic()}$DLQ_SUFFIX\", record.partition())\n    }\n    return recoverer\n  }\n\n  /**\n   * Error handler with retry and dead letter queue support.\n   * After MAX_RETRY_ATTEMPTS failures, message is sent to DLQ.\n   */\n  @Bean\n  fun kafkaErrorHandler(\n    deadLetterPublishingRecoverer: DeadLetterPublishingRecoverer\n  ): DefaultErrorHandler {\n    val errorHandler = DefaultErrorHandler(\n      deadLetterPublishingRecoverer,\n      FixedBackOff(RETRY_INTERVAL_MS, MAX_RETRY_ATTEMPTS)\n    )\n    // Log level is INFO by default, which is fine for our use case\n    errorHandler.addNotRetryableExceptions(IllegalArgumentException::class.java)\n    return errorHandler\n  }\n\n  @Bean\n  fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, OrderCreatedEvent>,\n    kafkaErrorHandler: DefaultErrorHandler\n  ): ConcurrentKafkaListenerContainerFactory<String, OrderCreatedEvent> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, OrderCreatedEvent>()\n    factory.consumerFactory = consumerFactory\n    factory.setCommonErrorHandler(kafkaErrorHandler)\n    return factory\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/kafka/OrderCreatedEventListener.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.kafka\n\nimport com.trendyol.stove.examples.kotlin.spring.domain.statistics.*\nimport com.trendyol.stove.examples.kotlin.spring.events.OrderCreatedEvent\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport kotlinx.coroutines.runBlocking\nimport org.springframework.kafka.annotation.KafkaListener\nimport org.springframework.stereotype.Component\n\nprivate val logger = KotlinLogging.logger {}\n\n/**\n * Kafka listener that consumes OrderCreatedEvent and updates the read model.\n *\n * This demonstrates the Event Sourcing / CQRS pattern where:\n * - Commands create orders (write model)\n * - Events update statistics (read model)\n *\n * In the showcase test, we verify:\n * 1. The event was consumed (shouldBeConsumed)\n * 2. The side effect happened (statistics were updated)\n */\n@Component\nclass OrderCreatedEventListener(\n  private val statisticsRepository: UserOrderStatisticsRepository\n) {\n  @KafkaListener(\n    topics = [\"\\${kafka.topics.orders-created}\"],\n    groupId = \"order-statistics-updater\",\n    containerFactory = \"kafkaListenerContainerFactory\"\n  )\n  @WithSpan(\"OrderCreatedEventListener.onOrderCreated\")\n  fun onOrderCreated(event: OrderCreatedEvent) = runBlocking {\n    logger.info { \"Received OrderCreatedEvent: orderId=${event.orderId}, userId=${event.userId}\" }\n    updateStatistics(event)\n    logger.info { \"Statistics updated for user=${event.userId}\" }\n  }\n\n  private suspend fun updateStatistics(event: OrderCreatedEvent) {\n    val existing = statisticsRepository.findByUserId(event.userId)\n    val updated = (existing ?: UserOrderStatistics(userId = event.userId))\n      .addOrder(event.amount, event.createdAt)\n\n    statisticsRepository.save(updated)\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/kafka/OrderEventPublisher.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.kafka\n\nimport com.trendyol.stove.examples.kotlin.spring.events.OrderCreatedEvent\nimport com.trendyol.stove.examples.kotlin.spring.events.PaymentProcessedEvent\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.kafka.core.KafkaTemplate\nimport org.springframework.stereotype.Component\n\nprivate val logger = KotlinLogging.logger {}\n\n@Component\nclass OrderEventPublisher(\n  private val kafkaTemplate: KafkaTemplate<String, Any>,\n  @param:Value(\"\\${kafka.topics.orders-created}\") private val ordersCreatedTopic: String,\n  @param:Value(\"\\${kafka.topics.payments-processed}\") private val paymentsProcessedTopic: String\n) {\n  @WithSpan(\"OrderEventPublisher.publishOrderCreated\")\n  fun publish(event: OrderCreatedEvent) {\n    logger.info { \"Publishing OrderCreatedEvent: orderId=${event.orderId}\" }\n    kafkaTemplate.send(ordersCreatedTopic, event.orderId, event)\n  }\n\n  @WithSpan(\"OrderEventPublisher.publishPaymentProcessed\")\n  fun publish(event: PaymentProcessedEvent) {\n    logger.info { \"Publishing PaymentProcessedEvent: orderId=${event.orderId}\" }\n    kafkaTemplate.send(paymentsProcessedTopic, event.orderId, event)\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/persistence/DataSourceConfig.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.persistence\n\nimport com.zaxxer.hikari.HikariDataSource\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport javax.sql.DataSource\n\n/**\n * Configuration for JDBC DataSource.\n * Required for db-scheduler which needs JDBC, while the app uses R2DBC for reactive operations.\n * Derives JDBC URL from R2DBC URL for consistency.\n */\n@Configuration\nclass DataSourceConfig {\n  @Bean\n  fun dataSource(\n    @Value(\"\\${spring.r2dbc.url}\") r2dbcUrl: String,\n    @Value(\"\\${spring.r2dbc.username}\") username: String,\n    @Value(\"\\${spring.r2dbc.password}\") password: String\n  ): DataSource = HikariDataSource().apply {\n    // Convert R2DBC URL to JDBC URL\n    jdbcUrl = r2dbcUrl.replace(\"r2dbc:\", \"jdbc:\")\n    this.username = username\n    this.password = password\n    driverClassName = \"org.postgresql.Driver\"\n    maximumPoolSize = 5\n    poolName = \"db-scheduler-pool\"\n    validate()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/persistence/PostgresOrderRepository.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.persistence\n\nimport com.trendyol.stove.examples.kotlin.spring.domain.order.*\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.SpanAttribute\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport kotlinx.coroutines.flow.toList\nimport kotlinx.coroutines.reactive.*\nimport org.springframework.r2dbc.core.DatabaseClient\nimport org.springframework.r2dbc.core.bind\nimport org.springframework.stereotype.Repository\nimport java.time.Instant\n\nprivate val logger = KotlinLogging.logger {}\n\n@Repository\nclass PostgresOrderRepository(\n  private val databaseClient: DatabaseClient\n) : OrderRepository {\n  @WithSpan(\"PostgresOrderRepository.save\")\n  override suspend fun save(order: Order): Order {\n    logger.info { \"Saving order: id=${order.id}\" }\n\n    // ══════════════════════════════════════════════════════════════════════════\n    // 🐛 DEMO BUG: Uncomment below to simulate a deep production bug\n    // This bug only triggers for high-value orders (> $1000), making it hard\n    // to catch in simple unit tests. The trace will show exactly where it fails!\n    // ══════════════════════════════════════════════════════════════════════════\n    // validateOrderAmount(order)\n\n    databaseClient\n      .sql(\n        \"\"\"\n      INSERT INTO orders (id, user_id, product_id, amount, status, payment_transaction_id, created_at)\n      VALUES (:id, :userId, :productId, :amount, :status, :paymentTransactionId, :createdAt)\n      ON CONFLICT (id) DO UPDATE SET\n        status = :status,\n        payment_transaction_id = :paymentTransactionId\n        \"\"\".trimIndent()\n      ).bind(\"id\", order.id)\n      .bind(\"userId\", order.userId)\n      .bind(\"productId\", order.productId)\n      .bind(\"amount\", order.amount)\n      .bind(\"status\", order.status.name)\n      .bind(\"paymentTransactionId\", order.paymentTransactionId)\n      .bind(\"createdAt\", order.createdAt)\n      .fetch()\n      .rowsUpdated()\n      .awaitSingle()\n\n    return order\n  }\n\n  @WithSpan(\"PostgresOrderRepository.findById\")\n  override suspend fun findById(\n    @SpanAttribute(\"order.id\") id: String\n  ): Order? = databaseClient\n    .sql(\n      \"\"\"\n      SELECT id, user_id, product_id, amount, status, payment_transaction_id, created_at\n      FROM orders\n      WHERE id = :id\n      \"\"\".trimIndent()\n    ).bind(\"id\", id)\n    .map { row, _ -> mapToOrder(row) }\n    .first()\n    .awaitFirstOrNull()\n\n  @WithSpan(\"PostgresOrderRepository.findByUserId\")\n  override suspend fun findByUserId(\n    @SpanAttribute(\"order.userId\") userId: String\n  ): List<Order> = databaseClient\n    .sql(\n      \"\"\"\n      SELECT id, user_id, product_id, amount, status, payment_transaction_id, created_at\n      FROM orders\n      WHERE user_id = :userId\n      \"\"\".trimIndent()\n    ).bind(\"userId\", userId)\n    .map { row, _ -> mapToOrder(row) }\n    .all()\n    .asFlow()\n    .toList()\n\n  private fun mapToOrder(row: io.r2dbc.spi.Row): Order = Order(\n    id = row.get(\"id\", String::class.java)!!,\n    userId = row.get(\"user_id\", String::class.java)!!,\n    productId = row.get(\"product_id\", String::class.java)!!,\n    amount = (row.get(\"amount\", java.math.BigDecimal::class.java)!!).toDouble(),\n    status = OrderStatus.valueOf(row.get(\"status\", String::class.java)!!),\n    paymentTransactionId = row.get(\"payment_transaction_id\", String::class.java),\n    createdAt = row.get(\"created_at\", Instant::class.java)!!\n  )\n\n  // ══════════════════════════════════════════════════════════════════════════\n  // 🐛 DEMO BUG: Simulates a bug deep in the persistence layer\n  // In production, this might be: connection pool exhaustion, constraint\n  // violation, or business rule that wasn't properly documented.\n  // ══════════════════════════════════════════════════════════════════════════\n  @Suppress(\"UnusedPrivateMember\", \"ThrowsCount\")\n  private fun validateOrderAmount(order: Order) {\n    if (order.amount > 1000) {\n      // Simulating a bug: maybe the payment gateway has an undocumented limit,\n      // or there's a database constraint we didn't know about\n      throw OrderPersistenceException(\n        \"Failed to persist order ${order.id}: amount exceeds internal threshold\"\n      )\n    }\n  }\n}\n\nclass OrderPersistenceException(\n  message: String\n) : RuntimeException(message)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/persistence/PostgresUserOrderStatisticsRepository.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.persistence\n\nimport com.trendyol.stove.examples.kotlin.spring.domain.statistics.UserOrderStatistics\nimport com.trendyol.stove.examples.kotlin.spring.domain.statistics.UserOrderStatisticsRepository\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport kotlinx.coroutines.reactive.awaitFirstOrNull\nimport kotlinx.coroutines.reactive.awaitSingle\nimport org.springframework.r2dbc.core.DatabaseClient\nimport org.springframework.r2dbc.core.bind\nimport org.springframework.stereotype.Repository\nimport java.time.Instant\n\nprivate val logger = KotlinLogging.logger {}\n\n@Repository\nclass PostgresUserOrderStatisticsRepository(\n  private val databaseClient: DatabaseClient\n) : UserOrderStatisticsRepository {\n  @WithSpan(\"UserOrderStatisticsRepository.findByUserId\")\n  override suspend fun findByUserId(userId: String): UserOrderStatistics? = databaseClient\n    .sql(\n      \"\"\"\n      SELECT user_id, total_orders, total_amount, last_order_at\n      FROM user_order_statistics\n      WHERE user_id = :userId\n      \"\"\".trimIndent()\n    ).bind(\"userId\", userId)\n    .map { row, _ ->\n      @Suppress(\"PLATFORM_CLASS_MAPPED_TO_KOTLIN\")\n      UserOrderStatistics(\n        userId = row.get(\"user_id\", String::class.java)!!,\n        totalOrders = (row.get(\"total_orders\", java.lang.Integer::class.java) as Int?) ?: 0,\n        totalAmount = row.get(\"total_amount\", java.math.BigDecimal::class.java)!!.toDouble(),\n        lastOrderAt = row.get(\"last_order_at\", Instant::class.java)\n      )\n    }.first()\n    .awaitFirstOrNull()\n\n  @WithSpan(\"UserOrderStatisticsRepository.save\")\n  override suspend fun save(statistics: UserOrderStatistics): UserOrderStatistics {\n    logger.info { \"Saving user statistics: userId=${statistics.userId}, totalOrders=${statistics.totalOrders}\" }\n\n    databaseClient\n      .sql(\n        \"\"\"\n        INSERT INTO user_order_statistics (user_id, total_orders, total_amount, last_order_at)\n        VALUES (:userId, :totalOrders, :totalAmount, :lastOrderAt)\n        ON CONFLICT (user_id) DO UPDATE SET\n          total_orders = :totalOrders,\n          total_amount = :totalAmount,\n          last_order_at = :lastOrderAt\n        \"\"\".trimIndent()\n      ).bind(\"userId\", statistics.userId)\n      .bind(\"totalOrders\", statistics.totalOrders)\n      .bind(\"totalAmount\", statistics.totalAmount)\n      .bind(\"lastOrderAt\", statistics.lastOrderAt)\n      .fetch()\n      .rowsUpdated()\n      .awaitSingle()\n\n    return statistics\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/scheduling/DbSchedulerConfig.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.scheduling\n\nimport com.github.kagkarlsson.scheduler.boot.config.DbSchedulerCustomizer\nimport com.github.kagkarlsson.scheduler.event.AbstractSchedulerListener\nimport com.github.kagkarlsson.scheduler.task.ExecutionComplete\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport org.springframework.stereotype.Component\n\nprivate val logger = KotlinLogging.logger {}\n\n/**\n * Customizer for db-scheduler configuration.\n */\n@Component\nclass DbSchedulerCustomizerConfig : DbSchedulerCustomizer\n\n/**\n * Listener for db-scheduler task executions.\n * Logs task execution results for observability.\n */\n@Component\nclass DbSchedulerLoggingListener : AbstractSchedulerListener() {\n  override fun onExecutionComplete(executionComplete: ExecutionComplete) {\n    logger.info {\n      \"Task execution completed: \" +\n        \"task=${executionComplete.execution.taskInstance.taskName}, \" +\n        \"instanceId=${executionComplete.execution.taskInstance.id}, \" +\n        \"result=${executionComplete.result}\"\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/scheduling/EmailSchedulerService.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.scheduling\n\nimport com.github.kagkarlsson.scheduler.Scheduler\nimport com.github.kagkarlsson.scheduler.task.Task\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.SpanAttribute\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport org.springframework.stereotype.Service\nimport java.time.Instant\nimport java.util.UUID\n\nprivate val logger = KotlinLogging.logger {}\n\n/**\n * Service for scheduling order-related email tasks.\n * Uses db-scheduler for persistent, reliable task scheduling.\n *\n * This demonstrates:\n * - Integration with db-scheduler for persistent scheduling\n * - OpenTelemetry instrumentation for observability\n * - Stove testing with DbSchedulerSystem\n */\n@Service\nclass EmailSchedulerService(\n  private val scheduler: Scheduler,\n  private val sendOrderEmailTask: Task<OrderEmailPayload>\n) {\n  /**\n   * Schedules an order confirmation email to be sent.\n   *\n   * @param orderId The order ID\n   * @param userId The user ID\n   * @param email The email address (defaults to userId@example.com)\n   * @param amount The order amount\n   * @param productId The product ID\n   * @param executeAt When to send the email (defaults to now)\n   */\n  @WithSpan(\"EmailSchedulerService.scheduleOrderConfirmationEmail\")\n  fun scheduleOrderConfirmationEmail(\n    @SpanAttribute(\"orderId\") orderId: String,\n    @SpanAttribute(\"userId\") userId: String,\n    email: String = \"$userId@example.com\",\n    amount: Double,\n    productId: String,\n    executeAt: Instant = Instant.now()\n  ) {\n    val payload = OrderEmailPayload(\n      orderId = orderId,\n      userId = userId,\n      email = email,\n      amount = amount,\n      productId = productId\n    )\n\n    val taskInstanceId = \"order-email-$orderId-${UUID.randomUUID()}\"\n\n    logger.info {\n      \"Scheduling order confirmation email for order $orderId to $email at $executeAt\"\n    }\n\n    scheduler.scheduleIfNotExists(\n      sendOrderEmailTask.instance(taskInstanceId, payload),\n      executeAt\n    )\n\n    logger.info { \"Successfully scheduled email task: $taskInstanceId\" }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/kotlin/com/trendyol/stove/examples/kotlin/spring/infra/scheduling/SendOrderEmailTask.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.infra.scheduling\n\nimport com.github.kagkarlsson.scheduler.task.Task\nimport com.github.kagkarlsson.scheduler.task.helper.Tasks\nimport io.github.oshai.kotlinlogging.KotlinLogging\nimport io.opentelemetry.instrumentation.annotations.WithSpan\nimport org.springframework.context.annotation.Bean\nimport org.springframework.context.annotation.Configuration\nimport java.io.Serializable\n\n/**\n * Payload for the order email task.\n * Contains all information needed to send an order confirmation email.\n */\ndata class OrderEmailPayload(\n  val orderId: String,\n  val userId: String,\n  val email: String,\n  val amount: Double,\n  val productId: String\n) : Serializable {\n  companion object {\n    private const val serialVersionUID: Long = 1L\n  }\n}\n\nprivate val logger = KotlinLogging.logger {}\n\n@Configuration\nclass SendOrderEmailTaskConfig {\n  @Bean\n  fun sendOrderEmailTask(): Task<OrderEmailPayload> =\n    Tasks\n      .oneTime(\"send-order-email\", OrderEmailPayload::class.java)\n      .execute { taskInstance, _ ->\n        val payload = taskInstance.data\n        sendEmail(payload)\n      }\n\n  @WithSpan(\"SendOrderEmailTask.sendEmail\")\n  private fun sendEmail(payload: OrderEmailPayload) {\n    // Simulate sending email - in production this would call an email service\n    logger.info {\n      \"\"\"\n      |============================================\n      | SENDING ORDER CONFIRMATION EMAIL\n      |============================================\n      | To: ${payload.email}\n      | Order ID: ${payload.orderId}\n      | User ID: ${payload.userId}\n      | Product: ${payload.productId}\n      | Amount: $${payload.amount}\n      |============================================\n      \"\"\".trimMargin()\n    }\n\n    // Simulate email sending delay\n    Thread.sleep(100)\n\n    logger.info { \"Email sent successfully for order ${payload.orderId}\" }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/proto/fraud_detection.proto",
    "content": "syntax = \"proto3\";\n\npackage frauddetection;\n\noption java_package = \"com.trendyol.stove.examples.kotlin.spring.grpc\";\noption java_multiple_files = true;\n\n// Request to check if an order is fraudulent\nmessage CheckFraudRequest {\n  string order_id = 1;\n  string user_id = 2;\n  double amount = 3;\n  string product_id = 4;\n}\n\n// Response with fraud check result\nmessage CheckFraudResponse {\n  bool is_fraudulent = 1;\n  double risk_score = 2;  // 0.0 - 1.0\n  string reason = 3;      // e.g., \"high_value_first_order\", \"suspicious_pattern\"\n}\n\n// External Fraud Detection service (simulates a real microservice)\nservice FraudDetectionService {\n  // Check if an order is potentially fraudulent\n  rpc CheckFraud(CheckFraudRequest) returns (CheckFraudResponse);\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/proto/order_query.proto",
    "content": "syntax = \"proto3\";\n\npackage orderquery;\n\noption java_multiple_files = true;\noption java_package = \"com.trendyol.stove.examples.kotlin.spring.grpc\";\n\n// Request to get order by ID\nmessage GetOrderRequest {\n  string order_id = 1;\n}\n\n// Request to get orders by user\nmessage GetOrdersByUserRequest {\n  string user_id = 1;\n}\n\n// Order details in response\nmessage OrderProto {\n  string id = 1;\n  string user_id = 2;\n  string product_id = 3;\n  double amount = 4;\n  string status = 5;\n  string payment_transaction_id = 6;\n  int64 created_at = 7; // Unix timestamp millis\n}\n\n// Single order response\nmessage GetOrderResponse {\n  bool found = 1;\n  OrderProto order = 2;\n}\n\n// Multiple orders response\nmessage GetOrdersResponse {\n  repeated OrderProto orders = 1;\n}\n\n// Order Query Service - exposed by our application\nservice OrderQueryService {\n  // Get a single order by ID\n  rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);\n\n  // Get all orders for a user\n  rpc GetOrdersByUser(GetOrdersByUserRequest) returns (GetOrdersResponse);\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/main/resources/application.yml",
    "content": "server:\n  port: 8024\n\ngrpc:\n  server:\n    port: 50051\n\nspring:\n  application:\n    name: stove-kotlin-spring-showcase\n  r2dbc:\n    url: r2dbc:postgresql://localhost:5432/stove\n    username: postgres\n    password: postgres\n  datasource:\n    url: jdbc:postgresql://localhost:5432/stove\n    username: postgres\n    password: postgres\n    driver-class-name: org.postgresql.Driver\n  kafka:\n    bootstrap-servers: localhost:9092\n    producer:\n      key-serializer: org.apache.kafka.common.serialization.StringSerializer\n      value-serializer: org.springframework.kafka.support.serializer.JsonSerializer\n\nkafka:\n  topics:\n    orders-created: showcase.orders.created\n    payments-processed: showcase.payments.processed\n\nexternal-apis:\n  inventory:\n    url: http://localhost:9091\n  payment:\n    url: http://localhost:9091\n  fraud-detection:\n    host: localhost\n    port: 9092\n\nlogging:\n  level:\n    com.trendyol.stove: DEBUG\n    org.springframework.r2dbc: DEBUG\n    com.github.kagkarlsson.scheduler: DEBUG\n\n# db-scheduler configuration\ndb-scheduler:\n  enabled: true\n  table-name: scheduled_tasks\n  threads: 2\n  polling-interval: 100ms\n  polling-strategy: lock-and-fetch\n  polling-strategy-lower-limit-fraction-of-threads: 0.5\n  polling-strategy-upper-limit-fraction-of-threads: 2.0\n  immediate-execution-enabled: true\n  always-persist-timestamp-in-utc: true"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/spring/e2e/painful/BaseIntegrationTest.kt",
    "content": "@file:Suppress(\"all\")\n\npackage com.trendyol.stove.examples.kotlin.spring.e2e.painful\n\n/*\n\n@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)\n@Testcontainers\nabstract class BaseIntegrationTest {\n\n  companion object {\n\n    @Container\n    val postgres = PostgreSQLContainer(\"postgres:16-alpine\")\n      .withDatabaseName(\"test\")\n      .withUsername(\"test\")\n      .withPassword(\"test\")\n\n    @Container\n    val kafka = KafkaContainer(DockerImageName.parse(\"confluentinc/cp-kafka:7.8.1\"))\n      .withStartupAttempts(3)\n\n    val wiremock = WireMockServer(WireMockConfiguration.options().dynamicPort())\n\n    @JvmStatic\n    @BeforeAll\n    fun startWiremock() {\n      wiremock.start()\n    }\n\n    @JvmStatic\n    @AfterAll\n    fun stopWiremock() {\n      wiremock.stop()\n    }\n\n    @JvmStatic\n    @DynamicPropertySource\n    fun configureProperties(registry: DynamicPropertyRegistry) {\n      registry.add(\"spring.r2dbc.url\") {\n        \"r2dbc:postgresql://${postgres.host}:${postgres.firstMappedPort}/${postgres.databaseName}\"\n      }\n      registry.add(\"spring.r2dbc.username\") { postgres.username }\n      registry.add(\"spring.r2dbc.password\") { postgres.password }\n\n      registry.add(\"spring.kafka.bootstrap-servers\") { kafka.bootstrapServers }\n      registry.add(\"spring.kafka.producer.properties.interceptor.classes\") { \"\" }\n\n\n      registry.add(\"external-apis.inventory.url\") { \"http://localhost:${wiremock.port()}\" }\n      registry.add(\"external-apis.payment.url\") { \"http://localhost:${wiremock.port()}\" }\n    }\n  }\n\n  @LocalServerPort\n  protected var port: Int = 0\n\n  @Autowired\n  protected lateinit var r2dbcEntityTemplate: R2dbcEntityTemplate\n\n  @Autowired\n  protected lateinit var kafkaTemplate: KafkaTemplate<String, Any>\n\n  @Autowired\n  protected lateinit var orderRepository: OrderRepository\n\n  @Autowired\n  protected lateinit var objectMapper: ObjectMapper\n\n  @BeforeEach\n  fun setup() {\n    RestAssured.port = port\n    RestAssured.baseURI = \"http://localhost\"\n    wiremock.resetAll()\n\n    // Database cleanup - hope you didn't miss a table!\n    runBlocking {\n      r2dbcEntityTemplate.databaseClient\n        .sql(\"TRUNCATE orders CASCADE\")\n        .fetch()\n        .rowsUpdated()\n        .awaitSingle()\n    }\n\n    // Kafka cleanup? Good luck with that...\n  }\n}\n\n// ════════════════════════════════════════════════════════════════════════════════\n// NOW you can write a test... but with THREE different assertion APIs\n// ════════════════════════════════════════════════════════════════════════════════\n\nclass OrderControllerPainfulTest : BaseIntegrationTest() {\n\n  @Test\n  suspend fun `should create order with all verifications`() {\n    val userId = UUID.randomUUID().toString()\n    val productId = \"macbook-pro-16\"\n    val amount = 2499.99\n\n    // ── WireMock setup (its own API) ────────────────────────────────────\n    wiremock.stubFor(\n      get(urlEqualTo(\"/inventory/$productId\"))\n        .willReturn(\n          aResponse()\n            .withStatus(200)\n            .withHeader(\"Content-Type\", \"application/json\")\n            .withBody(\n              \"\"\"\n                            {\n                                \"productId\": \"$productId\",\n                                \"available\": true,\n                                \"quantity\": 10\n                            }\n                        \"\"\".trimIndent()\n            )\n        )\n    )\n\n    wiremock.stubFor(\n      post(urlEqualTo(\"/payments/charge\"))\n        .willReturn(\n          aResponse()\n            .withStatus(200)\n            .withHeader(\"Content-Type\", \"application/json\")\n            .withBody(\n              \"\"\"\n                            {\n                                \"success\": true,\n                                \"transactionId\": \"txn-123\",\n                                \"amount\": $amount\n                            }\n                        \"\"\".trimIndent()\n            )\n        )\n    )\n\n    // ── HTTP call with RestAssured (API #1) ─────────────────────────────\n    val response = given()\n      .contentType(ContentType.JSON)\n      .body(\n        \"\"\"\n                {\n                    \"userId\": \"$userId\",\n                    \"productId\": \"$productId\",\n                    \"amount\": $amount\n                }\n            \"\"\".trimIndent()\n      )\n      .`when`()\n      .post(\"/api/orders\")\n      .then()\n      .statusCode(201)\n      .extract()\n      .asString()\n\n    val orderResponse = objectMapper.readValue(response, OrderResponse::class.java)\n\n    // ── Database verification with R2DBC (API #2) ───────────────────────\n    // Completely different syntax than HTTP assertions\n\n    val orders = r2dbcEntityTemplate.databaseClient\n      .sql(\"SELECT * FROM orders WHERE user_id = :userId\")\n      .bind(\"userId\", userId)\n      .fetch()\n      .all()\n      .asFlow()\n      .toList()\n\n    assertEquals(1, orders.size)\n    assertEquals(\"CONFIRMED\", orders.first()[\"status\"])\n    assertEquals(amount, (orders.first()[\"amount\"] as BigDecimal).toDouble())\n\n    // ── Kafka verification with KafkaTestUtils (API #3) ─────────────────\n    // Yet another API pattern to learn\n    val consumer = createConsumer()\n    consumer.subscribe(listOf(\"showcase.orders.created\"))\n\n    val records = KafkaTestUtils.getRecords(consumer, Duration.ofSeconds(10))\n    assertTrue(records.count() > 0) { \"Expected at least one Kafka message\" }\n\n    val event = objectMapper.readValue(\n      records.first().value() as String,\n      OrderCreatedEvent::class.java\n    )\n    assertEquals(userId, event.userId)\n    assertEquals(productId, event.productId)\n\n    consumer.close()\n  }\n\n  private fun createConsumer(): KafkaConsumer<String, String> {\n    // 10 more lines of Kafka consumer setup...\n    val props = Properties().apply {\n      put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.bootstrapServers)\n      put(ConsumerConfig.GROUP_ID_CONFIG, \"test-group-${UUID.randomUUID()}\")\n      put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, \"earliest\")\n      put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer::class.java)\n      put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer::class.java)\n    }\n    return KafkaConsumer(props)\n  }\n}\n\n*/\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/spring/e2e/setup/DbSchedulerSystem.kt",
    "content": "@file:Suppress(\"UNCHECKED_CAST\")\n\npackage com.trendyol.stove.examples.kotlin.spring.e2e.setup\n\nimport arrow.core.*\nimport com.github.kagkarlsson.scheduler.event.AbstractSchedulerListener\nimport com.github.kagkarlsson.scheduler.task.*\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport kotlinx.coroutines.*\nimport org.springframework.beans.factory.getBean\nimport org.springframework.context.ApplicationContext\nimport java.time.Instant\nimport java.util.concurrent.*\nimport kotlin.reflect.KClass\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * Listener that tracks db-scheduler task executions for testing purposes.\n * Captures scheduled, completed, and failed task executions.\n */\nclass StoveDbSchedulerListener : AbstractSchedulerListener() {\n  private val completedExecutions: ConcurrentMap<String, ExecutionComplete> = ConcurrentHashMap()\n  private val failedExecutions: ConcurrentMap<String, ExecutionComplete> = ConcurrentHashMap()\n  private val scheduledExecutions: ConcurrentMap<String, Instant> = ConcurrentHashMap()\n\n  override fun onExecutionComplete(executionComplete: ExecutionComplete) {\n    val instanceId = executionComplete.execution.taskInstance.id\n    completedExecutions[instanceId] = executionComplete\n\n    // Track failures separately for easy access\n    if (executionComplete.result == ExecutionComplete.Result.FAILED) {\n      failedExecutions[instanceId] = executionComplete\n    }\n  }\n\n  override fun onExecutionScheduled(taskInstanceId: TaskInstanceId, executionTime: Instant) {\n    scheduledExecutions[taskInstanceId.id] = executionTime\n  }\n\n  /**\n   * Returns a snapshot of completed executions for reporting.\n   */\n  fun getCompletedExecutionsSnapshot(): List<Map<String, Any?>> =\n    completedExecutions.map { (id, execution) ->\n      mapOf(\n        \"instanceId\" to id,\n        \"taskName\" to execution.execution.taskInstance.taskName,\n        \"result\" to execution.result.toString(),\n        \"payloadType\" to execution.execution.taskInstance.data\n          ?.javaClass\n          ?.simpleName\n      )\n    }\n\n  /**\n   * Returns a snapshot of failed executions for reporting.\n   */\n  fun getFailedExecutionsSnapshot(): List<Map<String, Any?>> =\n    failedExecutions.map { (id, execution) ->\n      mapOf(\n        \"instanceId\" to id,\n        \"taskName\" to execution.execution.taskInstance.taskName,\n        \"result\" to execution.result.toString(),\n        \"cause\" to execution.cause.orElse(null)?.message,\n        \"payloadType\" to execution.execution.taskInstance.data\n          ?.javaClass\n          ?.simpleName\n      )\n    }\n\n  /**\n   * Returns a snapshot of scheduled executions for reporting.\n   */\n  fun getScheduledExecutionsSnapshot(): List<Map<String, Any?>> =\n    scheduledExecutions.map { (id, time) ->\n      mapOf(\"instanceId\" to id, \"executionTime\" to time.toString())\n    }\n\n  /**\n   * Waits until a task execution with the specified payload type and condition is observed.\n   * Throws assertion error if the task execution failed.\n   */\n  suspend fun <T : Any> waitUntilObservedSuccessfully(\n    atLeastIn: Duration,\n    clazz: KClass<T>,\n    condition: (T) -> Boolean\n  ): Collection<ExecutionComplete> = coroutineScope {\n    val matchingExecutions = waitForMatchingExecutions(atLeastIn, clazz, condition)\n\n    // Check if any matching execution failed\n    val failedMatches = matchingExecutions.filter { it.result == ExecutionComplete.Result.FAILED }\n    if (failedMatches.isNotEmpty()) {\n      val failures = failedMatches.map { exec ->\n        val cause = exec.cause.orElse(null)\n        \"Task '${exec.execution.taskInstance.taskName}' \" +\n          \"(instance: ${exec.execution.taskInstance.id}) \" +\n          \"FAILED: ${cause?.message ?: \"Unknown error\"}\"\n      }\n      throw AssertionError(\n        \"Task execution(s) failed:\\n${failures.joinToString(\"\\n\")}\\n\" +\n          \"Expected: successful execution of ${clazz.simpleName}\"\n      )\n    }\n\n    matchingExecutions\n  }\n\n  private suspend fun <T : Any> waitForMatchingExecutions(\n    atLeastIn: Duration,\n    clazz: KClass<T>,\n    condition: (T) -> Boolean\n  ): Collection<ExecutionComplete> {\n    val getExecutions = { completedExecutions.values.toList() }\n\n    return getExecutions.waitUntilConditionMet(\n      atLeastIn,\n      \"While OBSERVING ${clazz.java.simpleName}\"\n    ) { execution ->\n      val data = execution.execution.taskInstance?.data ?: return@waitUntilConditionMet false\n      when {\n        clazz.java.isAssignableFrom(data.javaClass) -> condition(data as T)\n        else -> false\n      }\n    }\n  }\n\n  private suspend fun <T> (() -> Collection<T>).waitUntilConditionMet(\n    duration: Duration,\n    subject: String,\n    condition: (T) -> Boolean\n  ): Collection<T> = runCatching {\n    val collectionFunc = this\n    withTimeout(duration) { while (!collectionFunc().any { condition(it) }) delay(50) }\n    return collectionFunc().filter { condition(it) }\n  }.recoverCatching {\n    when (it) {\n      is TimeoutCancellationException -> throw AssertionError(\"GOT A TIMEOUT: $subject.\")\n      is ConcurrentModificationException -> Result.success(waitUntilConditionMet(duration, subject, condition))\n      else -> throw it\n    }.getOrThrow()\n  }.getOrThrow()\n}\n\n/**\n * Stove system for testing db-scheduler task executions.\n * Allows assertions on scheduled tasks being executed with expected payloads.\n */\nclass DbSchedulerSystem(\n  override val stove: Stove\n) : PluggedSystem,\n  AfterRunAwareWithContext<ApplicationContext>,\n  Reports {\n  lateinit var listener: StoveDbSchedulerListener\n\n  override val reportSystemName: String = \"DbScheduler\"\n\n  override suspend fun afterRun(context: ApplicationContext) {\n    listener = context.getBean()\n  }\n\n  override fun snapshot(): SystemSnapshot = SystemSnapshot(\n    system = reportSystemName,\n    state = mapOf(\n      \"completedExecutions\" to listener.getCompletedExecutionsSnapshot(),\n      \"failedExecutions\" to listener.getFailedExecutionsSnapshot(),\n      \"scheduledExecutions\" to listener.getScheduledExecutionsSnapshot()\n    ),\n    summary = buildString {\n      val completed = listener.getCompletedExecutionsSnapshot()\n      val failed = listener.getFailedExecutionsSnapshot()\n      val scheduled = listener.getScheduledExecutionsSnapshot()\n      append(\"Completed: ${completed.size} task(s)\")\n      if (completed.isNotEmpty()) {\n        append(\" [${completed.joinToString { it[\"taskName\"].toString() }}]\")\n      }\n      if (failed.isNotEmpty()) {\n        append(\", FAILED: ${failed.size} task(s)\")\n        append(\" [${failed.joinToString { \"${it[\"taskName\"]}: ${it[\"cause\"]}\" }}]\")\n      }\n      append(\", Scheduled: ${scheduled.size} task(s)\")\n    }\n  )\n\n  /**\n   * Asserts that a task with the specified payload type was executed successfully.\n   * Fails if the task execution itself failed (e.g., threw an exception).\n   *\n   * @param atLeastIn Maximum time to wait for the task execution\n   * @param condition Predicate to match the task payload\n   */\n  suspend inline fun <reified T : Any> shouldBeExecuted(\n    atLeastIn: Duration = 5.seconds,\n    noinline condition: T.() -> Boolean\n  ): DbSchedulerSystem = report(\n    action = \"Assert task executed successfully: ${T::class.simpleName}\",\n    expected = \"Task with ${T::class.simpleName} payload executed successfully\".some(),\n    metadata = mapOf(\"timeout\" to atLeastIn.toString())\n  ) { listener.waitUntilObservedSuccessfully(atLeastIn, T::class, condition) }.let { this }\n\n  override fun close() = Unit\n}\n\n// ============================================================================\n// DSL Extensions\n// ============================================================================\n\n/**\n * Registers the DbSchedulerSystem with Stove.\n */\nfun Stove.withDbSchedulerListener(): Stove = getOrRegister(DbSchedulerSystem(this)).let { this }\n\n/**\n * Gets the registered DbSchedulerSystem.\n */\nfun Stove.dbScheduler(): DbSchedulerSystem =\n  getOrNone<DbSchedulerSystem>().getOrElse { throw SystemNotRegisteredException(DbSchedulerSystem::class) }\n\n/**\n * DSL extension for registering DbSchedulerSystem during Stove setup.\n */\nfun WithDsl.dbScheduler(): Stove = this.stove.withDbSchedulerListener()\n\n/**\n * DSL extension for asserting on scheduled tasks during validation.\n */\nsuspend fun ValidationDsl.tasks(validation: suspend DbSchedulerSystem.() -> Unit): Unit =\n  validation(this.stove.dbScheduler())\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/spring/e2e/setup/OrderExampleInitialMigration.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.e2e.setup\n\nimport com.trendyol.stove.database.migrations.DatabaseMigration\nimport com.trendyol.stove.postgres.PostgresSqlMigrationContext\nimport io.github.oshai.kotlinlogging.KotlinLogging\n\nprivate val logger = KotlinLogging.logger {}\n\nclass OrderExampleInitialMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: PostgresSqlMigrationContext) {\n    logger.info { \"Creating orders table\" }\n    connection.operations.execute(\n      \"\"\"\n    ${orders()}\n    ${orderStatistics()}\n    ${dbScheduler()}\n      \"\"\".trimIndent()\n    )\n    logger.info { \"Orders, user_order_statistics, and scheduled_tasks tables created\" }\n  }\n\n  private fun dbScheduler(): String = \"\"\" -- db-scheduler table for scheduled tasks\n        -- Schema from: https://github.com/kagkarlsson/db-scheduler/blob/master/db-scheduler/src/test/resources/postgresql_tables.sql\n        DROP TABLE IF EXISTS scheduled_tasks;\n        CREATE TABLE IF NOT EXISTS scheduled_tasks (\n          task_name TEXT NOT NULL,\n          task_instance TEXT NOT NULL,\n          task_data BYTEA,\n          execution_time TIMESTAMP WITH TIME ZONE NOT NULL,\n          picked BOOLEAN NOT NULL,\n          picked_by TEXT,\n          last_success TIMESTAMP WITH TIME ZONE,\n          last_failure TIMESTAMP WITH TIME ZONE,\n          consecutive_failures INT,\n          last_heartbeat TIMESTAMP WITH TIME ZONE,\n          version BIGINT NOT NULL,\n          priority SMALLINT,\n          PRIMARY KEY (task_name, task_instance)\n        );\n        CREATE INDEX IF NOT EXISTS execution_time_idx ON scheduled_tasks (execution_time);\n        CREATE INDEX IF NOT EXISTS last_heartbeat_idx ON scheduled_tasks (last_heartbeat);\n        CREATE INDEX IF NOT EXISTS priority_execution_time_idx ON scheduled_tasks (priority DESC, execution_time ASC);\"\"\"\n\n  private fun orderStatistics(): String = \"\"\"  DROP TABLE IF EXISTS user_order_statistics;\n        CREATE TABLE IF NOT EXISTS user_order_statistics (\n          user_id VARCHAR(255) PRIMARY KEY,\n          total_orders INT NOT NULL DEFAULT 0,\n          total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0,\n          last_order_at TIMESTAMP\n        );\n        \"\"\"\n\n  private fun orders(): String = \"\"\"  DROP TABLE IF EXISTS orders;\n        CREATE TABLE IF NOT EXISTS orders (\n          id VARCHAR(255) PRIMARY KEY,\n          user_id VARCHAR(255) NOT NULL,\n          product_id VARCHAR(255) NOT NULL,\n          amount DECIMAL(10, 2) NOT NULL,\n          status VARCHAR(50) NOT NULL,\n          payment_transaction_id VARCHAR(255),\n          created_at TIMESTAMP NOT NULL DEFAULT NOW()\n        );\"\"\"\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/spring/e2e/setup/StoveConfig.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.e2e.setup\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.grpc.*\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.postgres.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.testing.grpcmock.*\nimport com.trendyol.stove.tracing.tracing\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.springframework.kafka.support.serializer.JsonSerializer\n\nconst val GRPC_MOCK_PORT = 9092\nconst val GRPC_SERVER_PORT = 50051\n\nclass StoveConfig : AbstractProjectConfig() {\n  init {\n    stoveKafkaBridgePortDefault = \"50053\"\n    System.setProperty(STOVE_KAFKA_BRIDGE_PORT, stoveKafkaBridgePortDefault)\n  }\n\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:8024\"\n          )\n        }\n\n        bridge()\n\n        // Enable tracing - starts OTLP gRPC receiver\n        // Service name is automatically extracted from incoming spans (set by OTel agent)\n        tracing {\n          enableSpanReceiver()\n        }\n\n        // gRPC Mock for external gRPC services (Fraud Detection)\n        grpcMock {\n          GrpcMockSystemOptions(port = GRPC_MOCK_PORT)\n        }\n\n        // gRPC Client for testing OUR gRPC server (OrderQueryService)\n        grpc {\n          GrpcSystemOptions(\n            host = \"localhost\",\n            port = GRPC_SERVER_PORT\n          )\n        }\n\n        wiremock {\n          WireMockSystemOptions(\n            port = 0, // Dynamic port allocation for CI compatibility\n            serde = StoveSerde.jackson.anyByteArraySerde(),\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"external-apis.inventory.url=${cfg.baseUrl}\",\n                \"external-apis.payment.url=${cfg.baseUrl}\"\n              )\n            }\n          )\n        }\n\n        postgresql {\n          PostgresqlOptions(\n            databaseName = \"stove\",\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                // R2DBC configuration for reactive database access\n                \"spring.r2dbc.url=r2dbc:postgresql://${cfg.host}:${cfg.port}/stove\",\n                \"spring.r2dbc.username=${cfg.username}\",\n                \"spring.r2dbc.password=${cfg.password}\",\n                // JDBC configuration for db-scheduler\n                \"spring.datasource.url=jdbc:postgresql://${cfg.host}:${cfg.port}/stove\",\n                \"spring.datasource.username=${cfg.username}\",\n                \"spring.datasource.password=${cfg.password}\"\n              )\n            }\n          ).migrations {\n            register<OrderExampleInitialMigration>()\n          }\n        }\n\n        kafka {\n          KafkaSystemOptions(\n            serde = StoveSerde.jackson.anyByteArraySerde(),\n            valueSerializer = JsonSerializer(),\n            containerOptions = KafkaContainerOptions(tag = \"8.0.3\") {\n              withStartupAttempts(3)\n            },\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"spring.kafka.bootstrap-servers=${cfg.bootstrapServers}\",\n                \"spring.kafka.producer.properties.interceptor.classes=${cfg.interceptorClass}\",\n                \"spring.kafka.consumer.properties.interceptor.classes=${cfg.interceptorClass}\"\n              )\n            }\n          )\n        }\n\n        // db-scheduler system for testing scheduled tasks\n        dbScheduler()\n\n        springBoot(\n          runner = { params ->\n            com.trendyol.stove.examples.kotlin.spring\n              .run(params) {\n                // Register test-specific beans for db-scheduler\n                addTestDependencies {\n                  bean<StoveDbSchedulerListener>(isPrimary = true)\n                }\n              }\n          },\n          withParameters = listOf(\n            \"server.port=8024\",\n            \"grpc.server.port=$GRPC_SERVER_PORT\",\n            \"external-apis.fraud-detection.host=localhost\",\n            \"external-apis.fraud-detection.port=$GRPC_MOCK_PORT\"\n            // WireMock URLs are set via configureExposedConfiguration for dynamic port support\n          )\n        )\n      }.run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/spring/e2e/tests/StreamingTests.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.e2e.tests\n\nimport com.trendyol.stove.examples.kotlin.spring.ExampleData\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.serialization.StoveSerde.Companion.deserialize\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.ktor.client.*\nimport io.ktor.client.engine.okhttp.*\nimport io.ktor.client.plugins.contentnegotiation.*\nimport io.ktor.client.request.*\nimport io.ktor.client.statement.*\nimport io.ktor.http.*\nimport io.ktor.serialization.jackson.*\nimport io.ktor.utils.io.*\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.*\nimport java.time.Duration\nimport kotlin.time.Duration.Companion.seconds\nimport kotlin.time.toJavaDuration\n\nclass StreamingTests :\n  FunSpec({\n    test(\"streaming\") {\n      stove {\n        http {\n          streamClient()\n            .prepareGet {\n              url(\"http://localhost:8024/api/streaming/json\")\n              parameter(\"load\", 100)\n              parameter(\"delay\", 1)\n              contentType(ContentType.parse(\"application/x-ndjson\"))\n            }.also { response ->\n              response\n                .readJsonStream { line ->\n                  StoveSerde.jackson.anyJsonStringSerde().deserialize<ExampleData>(line)\n                }.collect { data ->\n                  println(data)\n                }\n            }\n        }\n      }\n    }\n  })\n\n@OptIn(InternalAPI::class)\nfun <T> HttpStatement.readJsonStream(transform: (String) -> T): Flow<T> = flow {\n  execute {\n    while (!it.rawContent.isClosedForRead) {\n      val line = it.rawContent.readLineStrict()\n      if (line != null) {\n        emit(transform(line))\n      }\n    }\n  }\n}.flowOn(Dispatchers.IO)\n\nprivate fun streamClient(timeout: Duration = 30.seconds.toJavaDuration()): HttpClient {\n  val client = HttpClient(OkHttp) {\n    engine {\n      config {\n        followRedirects(true)\n        callTimeout(timeout)\n        connectTimeout(timeout)\n        readTimeout(timeout)\n        writeTimeout(timeout)\n      }\n    }\n    install(ContentNegotiation) {\n      register(ContentType.Application.Json, JacksonConverter())\n      register(ContentType.Application.ProblemJson, JacksonConverter())\n      register(ContentType.parse(\"application/x-ndjson\"), JacksonConverter())\n    }\n  }\n  return client\n}\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/test-e2e/kotlin/com/trendyol/stove/examples/kotlin/spring/e2e/tests/TheShowcase.kt",
    "content": "package com.trendyol.stove.examples.kotlin.spring.e2e.tests\n\nimport arrow.core.some\nimport com.trendyol.stove.examples.kotlin.spring.domain.order.*\nimport com.trendyol.stove.examples.kotlin.spring.e2e.setup.tasks\nimport com.trendyol.stove.examples.kotlin.spring.events.*\nimport com.trendyol.stove.examples.kotlin.spring.grpc.*\nimport com.trendyol.stove.examples.kotlin.spring.infra.clients.*\nimport com.trendyol.stove.examples.kotlin.spring.infra.scheduling.OrderEmailPayload\nimport com.trendyol.stove.grpc.grpc\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.testing.grpcmock.grpcMock\nimport com.trendyol.stove.wiremock.wiremock\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.*\nimport java.util.*\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * THE SHOWCASE - One comprehensive test demonstrating all Stove features.\n *\n * Walk through this test section-by-section during your presentation.\n *\n * ══════════════════════════════════════════════════════════════════════════════\n * 🎯 TWO WAYS TO DEMO FAILURE REPORTS:\n * ══════════════════════════════════════════════════════════════════════════════\n *\n * Option 1: ASSERTION FAILURE (simple)\n *   → Edit line ~110: change \"CONFIRMED\" to \"WRONG_STATUS\"\n *   → Shows: Test assertion failed, with full trace of what happened\n *\n * Option 2: DEEP APPLICATION BUG (realistic) ⭐ RECOMMENDED\n *   → Go to PostgresOrderRepository.kt\n *   → Uncomment the line: // validateOrderAmount(order)\n *   → Shows: Bug deep in persistence layer, test assertions are correct!\n *   → The trace reveals the exact failure point in the call stack\n *\n * The amount ($2499.99) is intentionally > $1000 to trigger the demo bug.\n * ══════════════════════════════════════════════════════════════════════════════\n */\nclass TheShowcase :\n  FunSpec({\n\n    test(\"The Complete Order Flow - Every Feature in One Test\") {\n      stove {\n        val userId = \"user-${UUID.randomUUID()}\"\n        val productId = \"macbook-pro-16\"\n        val amount = 2499.99\n        var orderId: String? = null\n\n        // ══════════════════════════════════════════════════════════════\n        // SECTION 1: gRPC Mock - Mock External gRPC Service\n        // \"First, we mock the Fraud Detection gRPC service\"\n        // ══════════════════════════════════════════════════════════════\n\n        grpcMock {\n          mockUnary(\n            serviceName = \"frauddetection.FraudDetectionService\",\n            methodName = \"CheckFraud\",\n            response = CheckFraudResponse\n              .newBuilder()\n              .setIsFraudulent(false)\n              .setRiskScore(0.15)\n              .setReason(\"low_risk_user\")\n              .build()\n          )\n        }\n\n        // ══════════════════════════════════════════════════════════════\n        // SECTION 2: WireMock - Mock External REST APIs\n        // \"Our order service also calls inventory and payment APIs\"\n        // ══════════════════════════════════════════════════════════════\n\n        wiremock {\n          mockGet(\n            url = \"/inventory/$productId\",\n            statusCode = 200,\n            responseBody = InventoryResponse(\n              productId = productId,\n              available = true,\n              quantity = 10\n            ).some()\n          )\n\n          mockPost(\n            url = \"/payments/charge\",\n            statusCode = 200,\n            responseBody = PaymentResult(\n              success = true,\n              transactionId = \"txn-${UUID.randomUUID()}\",\n              amount = amount\n            ).some()\n          )\n        }\n\n        // ══════════════════════════════════════════════════════════════\n        // SECTION 3: HTTP - Call Our API\n        // \"Now we call our order endpoint\"\n        // ══════════════════════════════════════════════════════════════\n\n        http {\n          postAndExpectBody<OrderResponse>(\n            uri = \"/api/orders\",\n            body = CreateOrderRequest(\n              userId = userId,\n              productId = productId,\n              amount = amount\n            ).some()\n          ) { response ->\n            response.status shouldBe 201\n            response.body().status shouldBe \"CONFIRMED\"\n            response.body().orderId shouldNotBe null\n            orderId = response.body().orderId\n          }\n        }\n\n        // ══════════════════════════════════════════════════════════════\n        // SECTION 4: Database - Verify State\n        // \"Let's check what's actually in the database\"\n        // ══════════════════════════════════════════════════════════════\n\n        postgresql {\n          shouldQuery<OrderRow>(\n            query = \"SELECT id, user_id, product_id, amount, status FROM orders WHERE user_id = '$userId'\",\n            mapper = { row ->\n              OrderRow(\n                id = row.string(\"id\"),\n                userId = row.string(\"user_id\"),\n                productId = row.string(\"product_id\"),\n                amount = row.double(\"amount\"),\n                status = row.string(\"status\")\n              )\n            }\n          ) { orders ->\n            orders.size shouldBe 1\n            orders.first().apply {\n              this.userId shouldBe userId\n              this.productId shouldBe productId\n              this.amount shouldBe amount\n              this.status shouldBe \"CONFIRMED\" // <-- EDIT THIS TO \"WRONG_STATUS\" TO SHOW FAILURE REPORT\n            }\n          }\n        }\n\n        // ══════════════════════════════════════════════════════════════\n        // SECTION 5: Kafka - Verify Events Published\n        // \"And check that the right events were published\"\n        // ══════════════════════════════════════════════════════════════\n\n        kafka {\n          shouldBePublished<OrderCreatedEvent>(10.seconds) {\n            actual.userId == userId && actual.productId == productId\n          }\n\n          shouldBePublished<PaymentProcessedEvent>(10.seconds) {\n            actual.amount == amount && actual.success\n          }\n        }\n\n        // ══════════════════════════════════════════════════════════════\n        // SECTION 5b: Kafka - Verify Events Consumed + Side Effects\n        // \"Now let's verify the consumer processed the event AND\n        //  updated the read model (CQRS pattern)\"\n        // ══════════════════════════════════════════════════════════════\n\n        kafka {\n          shouldBeConsumed<OrderCreatedEvent>(10.seconds) {\n            actual.userId == userId && actual.orderId == orderId\n          }\n        }\n\n        // Verify the side effect: statistics read model was updated\n        postgresql {\n          shouldQuery<UserStatisticsRow>(\n            query = \"SELECT user_id, total_orders, total_amount FROM user_order_statistics WHERE user_id = '$userId'\",\n            mapper = { row ->\n              UserStatisticsRow(\n                userId = row.string(\"user_id\"),\n                totalOrders = row.int(\"total_orders\"),\n                totalAmount = row.double(\"total_amount\")\n              )\n            }\n          ) { stats ->\n            stats.size shouldBe 1\n            stats.first().apply {\n              this.userId shouldBe userId\n              this.totalOrders shouldBe 1\n              this.totalAmount shouldBe amount\n            }\n          }\n        }\n\n        // ══════════════════════════════════════════════════════════════\n        // SECTION 6: gRPC - Test OUR gRPC Server\n        // \"Our app also exposes a gRPC API for querying orders\"\n        // ══════════════════════════════════════════════════════════════\n\n        grpc {\n          channel<OrderQueryServiceGrpcKt.OrderQueryServiceCoroutineStub> {\n            // Query order by ID via gRPC\n            val orderById = getOrder(\n              GetOrderRequest\n                .newBuilder()\n                .setOrderId(orderId!!)\n                .build()\n            )\n            orderById.found shouldBe true\n            orderById.order.userId shouldBe userId\n            orderById.order.status shouldBe \"CONFIRMED\"\n\n            // Query orders by user via gRPC\n            val ordersByUser = getOrdersByUser(\n              GetOrdersByUserRequest\n                .newBuilder()\n                .setUserId(userId)\n                .build()\n            )\n            ordersByUser.ordersCount shouldBe 1\n            ordersByUser.ordersList.first().productId shouldBe productId\n          }\n        }\n\n        // ══════════════════════════════════════════════════════════════\n        // SECTION 7: Bridge - Access Application Beans\n        // \"We can also access our services directly\"\n        // ══════════════════════════════════════════════════════════════\n\n        using<OrderService> {\n          val order = getOrderByUserId(userId)\n          order shouldNotBe null\n          order!!.status shouldBe OrderStatus.CONFIRMED\n        }\n\n        // ══════════════════════════════════════════════════════════════\n        // SECTION 8: db-scheduler - Verify Scheduled Tasks\n        // \"When an order is created, we schedule a confirmation email.\n        //  Let's verify the task was executed with the correct payload.\n        //  This showcases how to write your own Stove System!\"\n        // ══════════════════════════════════════════════════════════════\n\n        tasks {\n          shouldBeExecuted<OrderEmailPayload> {\n            this.orderId == orderId &&\n              this.userId == userId &&\n              this.amount == amount\n          }\n        }\n      }\n    }\n  })\n\n/**\n * Simple data class for mapping database rows.\n */\ndata class OrderRow(\n  val id: String,\n  val userId: String,\n  val productId: String,\n  val amount: Double,\n  val status: String\n)\n\n/**\n * Data class for mapping user statistics rows.\n */\ndata class UserStatisticsRow(\n  val userId: String,\n  val totalOrders: Int,\n  val totalAmount: Double\n)\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/test-e2e/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.examples.kotlin.spring.e2e.setup.StoveConfig\n"
  },
  {
    "path": "recipes/jvm/kotlin-recipes/spring-showcase/src/test-e2e/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"ERROR\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "recipes/jvm/scala-recipes/build.gradle.kts",
    "content": "plugins {\n  java\n  idea\n  alias(libs.plugins.spotless)\n}\n\nsubprojects {\n  apply {\n    plugin(\"java\")\n    plugin(\"idea\")\n    plugin(rootProject.libs.plugins.spotless.get().pluginId)\n  }\n  sourceSets {\n    @Suppress(\"LocalVariableName\", \"ktlint:standard:property-naming\")\n    val `test-e2e` by creating {\n      compileClasspath += sourceSets.main.get().output\n      runtimeClasspath += sourceSets.main.get().output\n    }\n\n    val testE2eImplementation by configurations.getting {\n      extendsFrom(configurations.testImplementation.get())\n    }\n    configurations[\"testE2eRuntimeOnly\"].extendsFrom(configurations.runtimeOnly.get())\n  }\n\n  idea {\n    module {\n      testSources.from(sourceSets[TestFolders.e2e].allSource.sourceDirectories)\n      testResources.from(sourceSets[TestFolders.e2e].resources.sourceDirectories)\n      isDownloadJavadoc = true\n      isDownloadSources = true\n    }\n  }\n\n  dependencies {\n    implementation(rootProject.projects.shared.domain)\n  }\n\n  tasks.register<Test>(\"e2eTest\") {\n    description = \"Runs e2e tests.\"\n    group = \"verification\"\n    testClassesDirs = sourceSets[TestFolders.e2e].output.classesDirs\n    classpath = sourceSets[TestFolders.e2e].runtimeClasspath\n\n    useJUnitPlatform()\n    reports {\n      junitXml.required.set(true)\n      html.required.set(true)\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/scala-recipes/spring-boot-basic-recipe/build.gradle.kts",
    "content": "plugins {\n  alias(libs.plugins.spring.boot)\n  alias(libs.plugins.spring.plugin)\n  alias(libs.plugins.spring.dependencyManagement)\n  scala\n}\n\ndependencies {\n  implementation(libs.scala2.library)\n  implementation(libs.spring.boot.webflux)\n  implementation(libs.spring.boot.autoconfigure)\n  implementation(libs.spring.boot.kafka)\n  annotationProcessor(libs.spring.boot.annotationProcessor)\n}\n\ndependencies {\n  testImplementation(stoveLibs.stove)\n  testImplementation(stoveLibs.stoveCouchbase)\n  testImplementation(stoveLibs.stoveHttp)\n  testImplementation(stoveLibs.stoveWiremock)\n  testImplementation(stoveLibs.stoveKafka)\n  testImplementation(stoveLibs.stoveSpring)\n}\n"
  },
  {
    "path": "recipes/jvm/scala-recipes/spring-boot-basic-recipe/src/main/scala/com/trendyol/stove/recipes/scala/spring/SpringBootRecipeApp.scala",
    "content": "package com.trendyol.stove.recipes.scala.spring\n\nimport org.springframework.boot.SpringApplication\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.context.ConfigurableApplicationContext\nimport org.springframework.stereotype.Component\nimport org.springframework.web.bind.annotation.{\n  GetMapping,\n  RequestMapping,\n  RestController\n}\n\n@SpringBootApplication\nclass SpringBootRecipeApp\n\nobject SpringBootRecipeApp {\n  def main(args: Array[String]): Unit = run(args, _ => ())\n\n  def run(\n      args: Array[String],\n      configure: SpringApplication => _\n  ): ConfigurableApplicationContext = {\n    val app = new SpringApplication(classOf[SpringBootRecipeApp])\n    configure(app)\n    app.run(args: _*)\n  }\n}\n\n@RestController\n@RequestMapping(Array(\"/hello\"))\nclass HelloWorldController(\n    private val currentThreadRetriever: CurrentThreadRetriever\n) {\n  @GetMapping\n  def hello(): String =\n    \"Hello, World! from \" + currentThreadRetriever.getCurrentThreadName\n}\n\n@Component\nclass CurrentThreadRetriever {\n  def getCurrentThreadName: String = Thread.currentThread().getName\n}\n"
  },
  {
    "path": "recipes/jvm/scala-recipes/spring-boot-basic-recipe/src/test-e2e/kotlin/com/trendyol/stove/recipes/scala/spring/e2e/setup/StoveConfig.kt",
    "content": "package com.trendyol.stove.recipes.scala.spring.e2e.setup\n\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.recipes.scala.spring.SpringBootRecipeApp\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.Stove\nimport io.kotest.core.config.AbstractProjectConfig\n\nclass StoveConfig : AbstractProjectConfig() {\n  override suspend fun beforeProject() {\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:8080\"\n          )\n        }\n        bridge()\n        springBoot(\n          runner = { parameters ->\n            SpringBootRecipeApp.run(parameters) {\n            }\n          },\n          withParameters = listOf()\n        )\n      }.run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/scala-recipes/spring-boot-basic-recipe/src/test-e2e/kotlin/com/trendyol/stove/recipes/scala/spring/e2e/tests/IndexTests.kt",
    "content": "package com.trendyol.stove.recipes.scala.spring.e2e.tests\n\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.recipes.scala.spring.CurrentThreadRetriever\nimport com.trendyol.stove.system.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldNotBe\nimport io.kotest.matchers.string.shouldContain\n\nclass IndexTests :\n  FunSpec({\n    test(\"Index page should be accessible\") {\n      stove {\n        http {\n          get<String>(\"/hello\") { actual ->\n            actual shouldContain \"Hello, World! from reactor\"\n          }\n        }\n      }\n    }\n\n    test(\"bridge should work\") {\n      stove {\n        using<CurrentThreadRetriever> {\n          this.currentThreadName shouldNotBe \"\"\n          println(this.currentThreadName)\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/scala-recipes/spring-boot-basic-recipe/src/test-e2e/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.recipes.scala.spring.e2e.setup.StoveConfig\n"
  },
  {
    "path": "recipes/jvm/settings.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nimport dev.aga.gradle.versioncatalogs.Generator.generate\nimport dev.aga.gradle.versioncatalogs.GeneratorConfig\n\nrootProject.name = \"jvm-recipes\"\npluginManagement {\n  repositories {\n    gradlePluginPortal()\n    mavenCentral()\n    maven(\"https://central.sonatype.com/repository/maven-snapshots\")\n  }\n}\ninclude(\n  \"kotlin-recipes\",\n  \"kotlin-recipes:ktor-mongo-recipe\",\n  \"kotlin-recipes:ktor-postgres-recipe\",\n  \"kotlin-recipes:spring-showcase\",\n  \"java-recipes\",\n  \"java-recipes:spring-boot-postgres-recipe\",\n  \"java-recipes:quarkus-basic-recipe\",\n  \"scala-recipes\",\n  \"scala-recipes:spring-boot-basic-recipe\",\n  \"shared\",\n  \"shared:domain\",\n  \"shared:application\",\n)\nplugins {\n  id(\"dev.aga.gradle.version-catalog-generator\") version (\"4.2.0\")\n}\n\nenableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\ndependencyResolutionManagement {\n  repositories {\n    mavenCentral()\n    maven(\"https://central.sonatype.com/repository/maven-snapshots\") {\n      content {\n        includeGroup(\"com.trendyol\")\n      }\n    }\n  }\n\n  versionCatalogs {\n    generate(\"stoveLibs\") {\n      fromToml(\"stove-bom\") {\n        aliasPrefixGenerator = GeneratorConfig.NO_PREFIX // (8)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/application/build.gradle.kts",
    "content": "plugins {\n  kotlin(\"jvm\") version libs.versions.kotlin\n  java\n  idea\n}\n\ndependencies {\n  compileOnly(libs.lombok)\n  annotationProcessor(libs.lombok)\n}\n\ndependencies {\n  testCompileOnly(libs.lombok)\n  testAnnotationProcessor(libs.lombok)\n  testImplementation(libs.arrow.core)\n}\n"
  },
  {
    "path": "recipes/jvm/shared/application/src/main/java/com/trendyol/stove/recipes/shared/application/BusinessException.java",
    "content": "package com.trendyol.stove.recipes.shared.application;\n\npublic class BusinessException extends Exception {\n  public BusinessException(String message) {\n    super(message);\n  }\n\n  public BusinessException(String message, Throwable cause) {\n    super(message, cause);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/application/src/main/java/com/trendyol/stove/recipes/shared/application/ErrorResponse.java",
    "content": "package com.trendyol.stove.recipes.shared.application;\n\npublic record ErrorResponse(String message, String code) {}\n"
  },
  {
    "path": "recipes/jvm/shared/application/src/main/java/com/trendyol/stove/recipes/shared/application/ExternalApiConfiguration.java",
    "content": "package com.trendyol.stove.recipes.shared.application;\n\nimport lombok.Data;\n\n@Data\npublic abstract class ExternalApiConfiguration {\n  private String url;\n  private int timeout;\n\n  public ExternalApiConfiguration() {\n    this(\"\", 0);\n  }\n\n  public ExternalApiConfiguration(String url, int timeout) {\n    this.url = url;\n    this.timeout = timeout;\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/application/src/main/java/com/trendyol/stove/recipes/shared/application/category/CategoryApiConfiguration.java",
    "content": "package com.trendyol.stove.recipes.shared.application.category;\n\nimport com.trendyol.stove.recipes.shared.application.ExternalApiConfiguration;\n\npublic class CategoryApiConfiguration extends ExternalApiConfiguration {}\n"
  },
  {
    "path": "recipes/jvm/shared/application/src/main/java/com/trendyol/stove/recipes/shared/application/category/CategoryApiResponse.java",
    "content": "package com.trendyol.stove.recipes.shared.application.category;\n\npublic record CategoryApiResponse(int id, String name, boolean isActive) {\n  public CategoryApiResponse {\n    if (name == null) {\n      throw new IllegalArgumentException(\"Name cannot be null\");\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/build.gradle.kts",
    "content": "plugins {\n  kotlin(\"jvm\") version libs.versions.kotlin\n  java\n  idea\n}\n\ndependencies {\n  implementation(libs.jackson.annotations)\n  compileOnly(libs.lombok)\n  annotationProcessor(libs.lombok)\n}\n\ndependencies {\n  testCompileOnly(libs.lombok)\n  testAnnotationProcessor(libs.lombok)\n  testImplementation(libs.arrow.core)\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/ddd/AggregateRoot.java",
    "content": "package com.trendyol.stove.examples.domain.ddd;\n\nimport com.fasterxml.jackson.annotation.JsonIgnore;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.function.Consumer;\nimport lombok.Getter;\n\n@SuppressWarnings(\"unchecked\")\npublic abstract class AggregateRoot<TId> {\n  private final EventRouter router;\n  private final EventRecorder recorder;\n\n  @Getter\n  protected long version;\n\n  @Getter\n  private final TId id;\n\n  protected AggregateRoot(TId id) {\n    this.id = id;\n    this.version = 0;\n    this.router = new EventRouter();\n    this.recorder = new EventRecorder();\n  }\n\n  protected <TEvent extends DomainEvent> void register(\n      Class<TEvent> event, Consumer<TEvent> eventAction) {\n    router.register(event, eventAction);\n  }\n\n  protected <TEvent extends DomainEvent> void applyEvent(TEvent event) {\n    version++;\n    event.setVersion(version);\n    play(event);\n    recorder.record(event);\n  }\n\n  protected <TEvent> void play(TEvent event) {\n    router.route(event);\n  }\n\n  @JsonIgnore\n  public String getIdAsString() {\n    return id.toString();\n  }\n\n  @JsonIgnore\n  public void clearDomainEvents() {\n    recorder.removeAll();\n  }\n\n  @JsonIgnore\n  public List<DomainEvent> domainEvents() {\n    return recorder.getRecords();\n  }\n\n  @JsonIgnore\n  public boolean hasChanges() {\n    return !domainEvents().isEmpty();\n  }\n\n  @JsonIgnore\n  public boolean isNew() {\n    return version - domainEvents().size() == 0;\n  }\n\n  @JsonIgnore\n  public String getAggregateName() {\n    return this.getClass().getSimpleName().toLowerCase(Locale.ROOT);\n  }\n\n  @Override\n  public boolean equals(Object other) {\n    if (this == other) return true;\n    if (getClass() != other.getClass()) return false;\n    AggregateRoot<TId> otherAggregate = (AggregateRoot<TId>) other;\n    return id.equals(otherAggregate.id);\n  }\n\n  @Override\n  public int hashCode() {\n    return id.hashCode();\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/ddd/DomainEvent.java",
    "content": "package com.trendyol.stove.examples.domain.ddd;\n\nimport lombok.AccessLevel;\nimport lombok.Setter;\n\npublic abstract class DomainEvent {\n  public final String type = this.getClass().getSimpleName();\n\n  @Setter(AccessLevel.PROTECTED)\n  private long version;\n\n  public DomainEvent() {\n    this.version = 0;\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/ddd/Entity.java",
    "content": "package com.trendyol.stove.examples.domain.ddd;\n\nimport java.util.function.Consumer;\n\npublic class Entity<TId, TAggregate extends AggregateRoot<?>> {\n  private final TId id;\n  private final EventRouter router;\n\n  public Entity(TId id) {\n    this.id = id;\n    this.router = new EventRouter();\n  }\n\n  protected <TEvent extends DomainEvent> void register(\n      Class<TEvent> event, Consumer<TEvent> eventAction) {\n    router.register(event, eventAction);\n  }\n\n  public <TEvent> void route(TEvent event) {\n    router.route(event);\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/ddd/EventPublisher.java",
    "content": "package com.trendyol.stove.examples.domain.ddd;\n\npublic interface EventPublisher {\n  <TId> void publishFor(AggregateRoot<TId> aggregateRoot);\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/ddd/EventRecorder.java",
    "content": "package com.trendyol.stove.examples.domain.ddd;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\npublic class EventRecorder {\n  private final List<DomainEvent> events;\n\n  public EventRecorder() {\n    this.events = new ArrayList<>();\n  }\n\n  public void record(DomainEvent event) {\n    events.add(event);\n  }\n\n  public List<DomainEvent> getRecords() {\n    return events;\n  }\n\n  public void removeAll() {\n    events.clear();\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/ddd/EventRouter.java",
    "content": "package com.trendyol.stove.examples.domain.ddd;\n\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.NoSuchElementException;\nimport java.util.function.Consumer;\n\npublic class EventRouter {\n  private final Map<Class<?>, Consumer<?>> eventActions = new HashMap<>();\n\n  public <TEvent extends DomainEvent> void register(\n      Class<TEvent> eventClass, Consumer<TEvent> eventAction) {\n    eventActions.put(eventClass, eventAction);\n  }\n\n  public <TEvent> void route(TEvent event) {\n    if (!eventActions.containsKey(event.getClass())) {\n      throw new NoSuchElementException(\n          \"Handler not found for: \" + event.getClass().getName());\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/product/Product.java",
    "content": "package com.trendyol.stove.examples.domain.product;\n\nimport com.trendyol.stove.examples.domain.ddd.AggregateRoot;\nimport com.trendyol.stove.examples.domain.product.events.ProductCreatedEvent;\nimport com.trendyol.stove.examples.domain.product.events.ProductNameChangedEvent;\nimport com.trendyol.stove.examples.domain.product.events.ProductPriceChangedEvent;\nimport java.nio.charset.StandardCharsets;\nimport java.util.UUID;\nimport lombok.Getter;\n\n@Getter\npublic class Product extends AggregateRoot<String> {\n\n  @SuppressWarnings(\"unused\")\n  private Product() {\n    super(null);\n  }\n\n  private Product(String id, String name, double price, int categoryId) {\n    super(id);\n    this.name = name;\n    this.price = price;\n    this.categoryId = categoryId;\n    register(ProductCreatedEvent.class, this::handle);\n    register(ProductNameChangedEvent.class, this::handle);\n    register(ProductPriceChangedEvent.class, this::handle);\n  }\n\n  private String name;\n  private double price;\n  private int categoryId;\n\n  public void changePrice(double newPrice) {\n    applyEvent(new ProductPriceChangedEvent(newPrice));\n  }\n\n  public void changeName(String newName) {\n    applyEvent(new ProductNameChangedEvent(newName));\n  }\n\n  private void handle(ProductCreatedEvent event) {\n    this.name = event.name;\n    this.price = event.price;\n  }\n\n  private void handle(ProductPriceChangedEvent event) {\n    this.price = event.newPrice;\n  }\n\n  private void handle(ProductNameChangedEvent event) {\n    this.name = event.newName;\n  }\n\n  public static Product create(String name, double price, int categoryId) {\n    var aggregate = new Product(\n        UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)).toString(),\n        name,\n        price,\n        categoryId);\n    aggregate.applyEvent(new ProductCreatedEvent(name, price, categoryId));\n    return aggregate;\n  }\n\n  public static Product fromPersistency(\n      String id, String name, double price, int categoryId, long version) {\n    var aggregate = new Product(id, name, price, categoryId);\n    aggregate.version = version;\n    return aggregate;\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/product/events/ProductCreatedEvent.java",
    "content": "package com.trendyol.stove.examples.domain.product.events;\n\nimport com.trendyol.stove.examples.domain.ddd.DomainEvent;\nimport lombok.NoArgsConstructor;\n\n@NoArgsConstructor(force = true)\npublic class ProductCreatedEvent extends DomainEvent {\n  public final String name;\n  public final double price;\n  public final int categoryId;\n\n  public ProductCreatedEvent(String name, double price, int categoryId) {\n    this.name = name;\n    this.price = price;\n    this.categoryId = categoryId;\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/product/events/ProductNameChangedEvent.java",
    "content": "package com.trendyol.stove.examples.domain.product.events;\n\nimport com.trendyol.stove.examples.domain.ddd.DomainEvent;\nimport lombok.NoArgsConstructor;\n\n@NoArgsConstructor(force = true)\npublic class ProductNameChangedEvent extends DomainEvent {\n  public final String newName;\n\n  public ProductNameChangedEvent(String newName) {\n    this.newName = newName;\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/main/java/com/trendyol/stove/examples/domain/product/events/ProductPriceChangedEvent.java",
    "content": "package com.trendyol.stove.examples.domain.product.events;\n\nimport com.trendyol.stove.examples.domain.ddd.DomainEvent;\nimport lombok.NoArgsConstructor;\n\n@NoArgsConstructor(force = true)\npublic class ProductPriceChangedEvent extends DomainEvent {\n  public final double newPrice;\n\n  public ProductPriceChangedEvent(double newPrice) {\n    this.newPrice = newPrice;\n  }\n}\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/test/kotlin/com/trendyol/stove/examples/domain/ProductTests.kt",
    "content": "package com.trendyol.stove.examples.domain\n\nimport com.trendyol.stove.examples.domain.product.Product\nimport com.trendyol.stove.examples.domain.product.events.*\nimport com.trendyol.stove.examples.domain.testing.aggregateroot.AggregateRootAssertion.Companion.assertEvents\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass ProductTests :\n  FunSpec({\n    test(\"should create product\") {\n      // Given\n      val product = Product.create(\"Product 1\", 100.0, 1)\n\n      // Then\n      product.name shouldBe \"Product 1\"\n      product.price shouldBe 100.0\n    }\n\n    test(\"when price change\") {\n      // Given\n      val product = Product.create(\"Product 1\", 100.0, 1)\n\n      // When\n      product.changePrice(200.0)\n\n      // Then\n      product.version shouldBe 2\n      assertEvents(product) {\n        shouldContain<ProductPriceChangedEvent> {\n          newPrice shouldBe 200.0\n        }\n        shouldContain<ProductCreatedEvent> {\n          name shouldBe \"Product 1\"\n          price shouldBe 100.0\n        }\n\n        shouldNotContain<ProductNameChangedEvent>()\n      }\n    }\n\n    test(\"change name\") {\n      // Given\n      val product = Product.create(\"Product 1\", 100.0, 1)\n\n      // When\n      product.changeName(\"Product 2\")\n\n      // Then\n      product.version shouldBe 2\n      assertEvents(product) {\n        shouldContain<ProductNameChangedEvent> {\n          newName shouldBe \"Product 2\"\n        }\n        shouldContain<ProductCreatedEvent> {\n          name shouldBe \"Product 1\"\n          price shouldBe 100.0\n        }\n\n        shouldNotContain<ProductPriceChangedEvent>()\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/jvm/shared/domain/src/test/kotlin/com/trendyol/stove/examples/domain/testing/aggregateroot/AggregateRootAssertion.kt",
    "content": "package com.trendyol.stove.examples.domain.testing.aggregateroot\n\nimport arrow.core.firstOrNone\nimport com.trendyol.stove.examples.domain.ddd.*\nimport io.kotest.assertions.*\nimport io.kotest.assertions.print.Printed\nimport io.kotest.engine.mapError\nimport io.kotest.matchers.collections.shouldHaveAtLeastSize\nimport io.kotest.matchers.shouldBe\n\nclass AggregateRootAssertion<TId, TAggregateRoot : AggregateRoot<TId>>(\n  val root: AggregateRoot<TId>\n) {\n  fun shouldHaveCount(expectedCount: Int) = runCatching { root.domainEvents().count().shouldBe(expectedCount) }\n    .mapError {\n      throw createAssertionError(\n        expected = Expected(Printed(expectedCount.toString())),\n        actual = Actual(Printed(root.domainEvents().count().toString())),\n        message = \"Expected Count but found:\",\n        cause = it\n      )\n    }\n\n  inline fun <reified T : DomainEvent> shouldContain(act: T.() -> Unit) {\n    shouldContain<T>()\n    root\n      .domainEvents()\n      .filter { it::class == T::class }\n      .map { it as T }\n      .shouldHaveAtLeastSize(1)\n      .forEach { act(it) }\n  }\n\n  inline fun <reified T : DomainEvent> shouldContain() = root\n    .domainEvents()\n    .map { it.javaClass }\n    .firstOrNone { it == T::class.java }\n    .onNone {\n      throw createAssertionError(\n        expected = Expected(Printed(T::class.java.simpleName)),\n        actual = Actual(Printed(domainEventsPrinted())),\n        message = \"Expected Domain Event Contain, but not found:\",\n        cause = null\n      )\n    }\n\n  inline fun <reified T : DomainEvent> shouldNotContain() =\n    root\n      .domainEvents()\n      .map { it.javaClass }\n      .firstOrNone { it == T::class.java }\n      .onSome {\n        throw createAssertionError(\n          expected = Expected(Printed(\"[]\")),\n          actual = Actual(Printed(domainEventsPrinted())),\n          message = \"Expected Domain Event Not Contain, but found:\",\n          cause = null\n        )\n      }\n\n  @PublishedApi\n  internal fun domainEventsPrinted(): String {\n    val eventNames = root.domainEvents().joinToString(\", \") { event -> event.javaClass.simpleName }\n    return \"[$eventNames]\"\n  }\n\n  companion object {\n    inline fun <TId, TAggregateRoot : AggregateRoot<TId>> assertEvents(\n      root: TAggregateRoot,\n      block: (AggregateRootAssertion<TId, TAggregateRoot>).() -> Unit\n    ) = block(AggregateRootAssertion(root))\n  }\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/.dockerignore",
    "content": ".gradle/\nbuild/\ngo-coverage/\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/.editorconfig",
    "content": "root = true\n\n[*]\ninsert_final_newline = true\n\n[{*.kt,*.kts}]\nindent_style = space\nmax_line_length = 140\nindent_size = 2\nij_kotlin_code_style_defaults = KOTLIN_OFFICIAL\nij_continuation_indent_size = 2\nij_kotlin_allow_trailing_comma = false\nij_kotlin_allow_trailing_comma_on_call_site = false\nij_kotlin_name_count_to_use_star_import = 2\nij_kotlin_name_count_to_use_star_import_for_members = 2\n\n[{**/stovetests/**.kt}]\nmax_line_length = 240\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/.gitignore",
    "content": "# Go binary (produced by `go build .` in this directory)\nstove-go-showcase\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/Dockerfile.container",
    "content": "FROM golang:1.26.3 AS build\n\nWORKDIR /workspace\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY *.go ./\n\nARG GO_BUILD_FLAGS=\"\"\nRUN CGO_ENABLED=0 GOOS=linux go build ${GO_BUILD_FLAGS} -o /out/go-showcase .\n\nFROM alpine:3.23\nWORKDIR /app\nCOPY --from=build /out/go-showcase /app/go-showcase\n\nEXPOSE 8090\nENTRYPOINT [\"/app/go-showcase\"]\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/build.gradle.kts",
    "content": "plugins {\n  kotlin(\"jvm\") version \"2.3.21\"\n  idea\n}\n\n// -- Go build ----------------------------------------------------------------\nval goBinary = layout.buildDirectory.file(\"go-app\").get().asFile\nval goExecutable = providers.environmentVariable(\"GO_EXECUTABLE\").getOrElse(\"go\")\nval dockerExecutable = providers.environmentVariable(\"DOCKER_EXECUTABLE\").getOrElse(\"docker\")\nval coverageEnabled = providers.gradleProperty(\"go.coverage\").map { it.toBoolean() }.getOrElse(false)\nval goCoverDirPath = layout.buildDirectory.dir(\"go-coverage\").get().asFile.absolutePath\nval goCoverOutPath = layout.buildDirectory.dir(\"go-coverage\").get().asFile.resolve(\"coverage.out\").absolutePath\nval goShowcaseContainerImage = \"stove-go-showcase-container:local\"\n\ntasks.register<Exec>(\"goModTidy\") {\n  description = \"Runs go mod tidy to sync dependencies.\"\n  group = \"build\"\n  commandLine(goExecutable, \"mod\", \"tidy\")\n  inputs.files(\"go.mod\", \"go.sum\")\n  outputs.files(\"go.mod\", \"go.sum\")\n}\n\ntasks.register<Exec>(\"buildGoApp\") {\n  description = \"Compiles the Go application.\"\n  group = \"build\"\n  dependsOn(\"goModTidy\")\n  val args = mutableListOf(goExecutable, \"build\")\n  if (coverageEnabled) args.add(\"-cover\")\n  args.addAll(listOf(\"-o\", goBinary.absolutePath, \".\"))\n  commandLine(args)\n  inputs.files(fileTree(\".\") { include(\"*.go\", \"go.mod\", \"go.sum\") })\n  inputs.property(\"goExecutable\", goExecutable)\n  outputs.file(goBinary)\n}\n\ntasks.register<Exec>(\"buildContainerImage\") {\n  description = \"Builds the Go showcase Docker image.\"\n  group = \"build\"\n  dependsOn(\"goModTidy\")\n  val buildFlags = if (coverageEnabled) \"-cover\" else \"\"\n  commandLine(\n    dockerExecutable,\n    \"build\",\n    \"--file\",\n    projectDir.resolve(\"Dockerfile.container\").absolutePath,\n    \"--tag\",\n    goShowcaseContainerImage,\n    \"--build-arg\",\n    \"GO_BUILD_FLAGS=$buildFlags\",\n    projectDir.absolutePath\n  )\n  inputs.file(project.file(\"Dockerfile.container\"))\n  inputs.file(project.file(\".dockerignore\"))\n  inputs.files(fileTree(\".\") { include(\"*.go\", \"go.mod\", \"go.sum\") })\n  inputs.property(\"coverageEnabled\", coverageEnabled)\n  outputs.upToDateWhen { false }\n}\n\nval removeContainerImageTask = tasks.register<Exec>(\"removeContainerImage\") {\n  description = \"Removes the local Go showcase Docker image.\"\n  group = \"build\"\n  commandLine(dockerExecutable, \"image\", \"rm\", goShowcaseContainerImage)\n  isIgnoreExitValue = true\n}\n\n// -- Test source set ----------------------------------------------------------\nval stoveTests = \"stovetests\"\n\nsourceSets {\n  create(stoveTests) {\n    kotlin {\n      compileClasspath += sourceSets.main.get().output\n      runtimeClasspath += sourceSets.main.get().output\n      srcDirs(\"stovetests/kotlin\")\n    }\n    resources.srcDirs(\"stovetests/resources\")\n  }\n}\n\nval stovetestsImplementation by configurations.getting {\n  extendsFrom(configurations.testImplementation.get())\n}\nconfigurations[\"stovetestsRuntimeOnly\"].extendsFrom(configurations.runtimeOnly.get())\n\nidea {\n  module {\n    testSources.from(sourceSets[stoveTests].allSource.sourceDirectories)\n    testResources.from(sourceSets[stoveTests].resources.sourceDirectories)\n  }\n}\n\n// -- E2E test tasks -----------------------------------------------------------\nval kafkaLibraries = listOf(\"sarama\", \"franz\", \"segmentio\")\n\nval kafkaE2eTasks = kafkaLibraries.mapIndexed { index, lib ->\n  tasks.register<Test>(\"e2eTest_$lib\") {\n    description = \"Runs e2e tests with the $lib Kafka library.\"\n    group = \"verification\"\n    dependsOn(\"buildGoApp\")\n    testClassesDirs = sourceSets[stoveTests].output.classesDirs\n    classpath = sourceSets[stoveTests].runtimeClasspath\n    useJUnitPlatform()\n    systemProperty(\"go.aut.mode\", \"process\")\n    systemProperty(\"go.app.binary\", goBinary.absolutePath)\n    systemProperty(\"kafka.library\", lib)\n    if (coverageEnabled) {\n      systemProperty(\"go.cover.dir\", goCoverDirPath)\n      outputs.cacheIf { false } // Coverage data is a side effect, not a tracked output\n    }\n    if (index > 0) mustRunAfter(\"e2eTest_${kafkaLibraries[index - 1]}\")\n  }\n}\n\ntasks.register<Test>(\"e2eTest\") {\n  description = \"Runs e2e tests for all Kafka libraries.\"\n  group = \"verification\"\n  dependsOn(kafkaE2eTasks)\n  enabled = false\n}\n\nval containerE2eTask = tasks.register<Test>(\"e2eTest-container\") {\n  description = \"Runs container-based e2e tests with sarama Kafka library.\"\n  group = \"verification\"\n  dependsOn(\"buildContainerImage\")\n  testClassesDirs = sourceSets[stoveTests].output.classesDirs\n  classpath = sourceSets[stoveTests].runtimeClasspath\n  useJUnitPlatform()\n  systemProperty(\"go.aut.mode\", \"container\")\n  systemProperty(\"go.app.container.image\", goShowcaseContainerImage)\n  systemProperty(\"kafka.library\", \"sarama\")\n  if (coverageEnabled) {\n    systemProperty(\"go.cover.dir\", goCoverDirPath)\n    outputs.cacheIf { false } // Coverage data is a side effect, not a tracked output\n  }\n}\n\n// -- Go coverage reports ------------------------------------------------------\nif (coverageEnabled) {\n  val goCoverHtmlPath = layout.buildDirectory.dir(\"go-coverage\").get().asFile.resolve(\"coverage.html\").absolutePath\n\n  tasks.register<Exec>(\"goCoverageReport\") {\n    description = \"Converts Go coverage data to standard format.\"\n    group = \"verification\"\n    mustRunAfter(kafkaE2eTasks)\n    mustRunAfter(containerE2eTask)\n    commandLine(goExecutable, \"tool\", \"covdata\", \"textfmt\", \"-i=$goCoverDirPath\", \"-o=$goCoverOutPath\")\n  }\n\n  tasks.register<Exec>(\"goCoverageSummary\") {\n    description = \"Prints Go coverage summary.\"\n    group = \"verification\"\n    dependsOn(\"goCoverageReport\")\n    commandLine(goExecutable, \"tool\", \"cover\", \"-func=$goCoverOutPath\")\n  }\n\n  tasks.register<Exec>(\"goCoverageHtml\") {\n    description = \"Generates HTML coverage report.\"\n    group = \"verification\"\n    dependsOn(\"goCoverageReport\")\n    commandLine(goExecutable, \"tool\", \"cover\", \"-html=$goCoverOutPath\", \"-o=$goCoverHtmlPath\")\n    doLast { logger.lifecycle(\"Go coverage HTML: $goCoverHtmlPath\") }\n  }\n\n  tasks.register(\"e2eTestWithCoverage\") {\n    description = \"Runs e2e tests and generates Go coverage report.\"\n    group = \"verification\"\n    dependsOn(kafkaE2eTasks)\n    finalizedBy(\"goCoverageSummary\", \"goCoverageHtml\")\n  }\n\n  tasks.register(\"e2eTest-containerWithCoverage\") {\n    description = \"Runs container e2e tests and generates Go coverage report.\"\n    group = \"verification\"\n    dependsOn(containerE2eTask)\n    finalizedBy(\"goCoverageSummary\", \"goCoverageHtml\")\n  }\n}\n\n// -- Dependencies -------------------------------------------------------------\ndependencies {\n  testImplementation(stoveLibs.stove)\n  testImplementation(\"com.trendyol:stove-container:${libs.versions.stove.get()}\")\n  testImplementation(stoveLibs.stoveProcess)\n  testImplementation(stoveLibs.stovePostgres)\n  testImplementation(stoveLibs.stoveHttp)\n  testImplementation(stoveLibs.stoveTracing)\n  testImplementation(stoveLibs.stoveDashboard)\n  testImplementation(stoveLibs.stoveKafka)\n  testImplementation(stoveLibs.stoveExtensionsKotest)\n\n  testImplementation(libs.kotest.runner.junit5)\n  testImplementation(libs.kotest.framework.engine)\n  testImplementation(libs.kotest.assertions.core)\n}\n\n// -- Kotlin / Java settings ---------------------------------------------------\nkotlin { jvmToolchain(21) }\n\ntasks.withType<Test> {\n  useJUnitPlatform()\n  jvmArgs(\"--add-opens\", \"java.base/java.util=ALL-UNNAMED\")\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/db.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"fmt\"\n\n\t\"github.com/XSAM/otelsql\"\n\t_ \"github.com/lib/pq\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.26.0\"\n)\n\nfunc initDB(connStr string) (*sql.DB, error) {\n\t// otelsql wraps database/sql — all queries are automatically traced\n\tdb, err := otelsql.Open(\"postgres\", connStr,\n\t\totelsql.WithAttributes(semconv.DBSystemPostgreSQL),\n\t)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open: %w\", err)\n\t}\n\tif err := db.Ping(); err != nil {\n\t\treturn nil, fmt.Errorf(\"ping: %w\", err)\n\t}\n\treturn db, nil\n}\n\nfunc insertProduct(ctx context.Context, db *sql.DB, p Product) error {\n\t_, err := db.ExecContext(ctx,\n\t\t\"INSERT INTO products (id, name, price) VALUES ($1, $2, $3)\",\n\t\tp.ID, p.Name, p.Price,\n\t)\n\treturn err\n}\n\nfunc getProduct(ctx context.Context, db *sql.DB, id string) (*Product, error) {\n\trow := db.QueryRowContext(ctx, \"SELECT id, name, price FROM products WHERE id = $1\", id)\n\tvar p Product\n\tif err := row.Scan(&p.ID, &p.Name, &p.Price); err != nil {\n\t\tif err == sql.ErrNoRows {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn &p, nil\n}\n\nfunc updateProduct(ctx context.Context, db *sql.DB, id string, name string, price float64) error {\n\t_, err := db.ExecContext(ctx,\n\t\t\"UPDATE products SET name = $1, price = $2 WHERE id = $3\",\n\t\tname, price, id,\n\t)\n\treturn err\n}\n\nfunc listProducts(ctx context.Context, db *sql.DB) ([]Product, error) {\n\trows, err := db.QueryContext(ctx, \"SELECT id, name, price FROM products\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar products []Product\n\tfor rows.Next() {\n\t\tvar p Product\n\t\tif err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tproducts = append(products, p)\n\t}\n\treturn products, rows.Err()\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/go.mod",
    "content": "module github.com/trendyol/stove-go-showcase\n\ngo 1.26.2\n\nrequire (\n\tgithub.com/IBM/sarama v1.48.1\n\tgithub.com/XSAM/otelsql v0.42.0\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/lib/pq v1.12.3\n\tgithub.com/segmentio/kafka-go v0.4.51\n\tgithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260511094143-5fae367ca053\n\tgithub.com/twmb/franz-go v1.21.1\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0\n\tgo.opentelemetry.io/otel v1.43.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0\n\tgo.opentelemetry.io/otel/sdk v1.43.0\n\tgoogle.golang.org/grpc v1.81.0\n)\n\nrequire (\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/eapache/go-resiliency v1.7.0 // indirect\n\tgithub.com/eapache/queue v1.1.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/jcmturner/aescts/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/dnsutils/v2 v2.0.0 // indirect\n\tgithub.com/jcmturner/gofork v1.7.6 // indirect\n\tgithub.com/jcmturner/gokrb5/v8 v8.4.4 // indirect\n\tgithub.com/jcmturner/rpc/v2 v2.0.3 // indirect\n\tgithub.com/klauspost/compress v1.18.6 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.26 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect\n\tgithub.com/twmb/franz-go/pkg/kmsg v1.13.1 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.43.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.43.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.10.0 // indirect\n\tgolang.org/x/crypto v0.51.0 // indirect\n\tgolang.org/x/net v0.54.0 // indirect\n\tgolang.org/x/sys v0.44.0 // indirect\n\tgolang.org/x/text v0.37.0 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect\n\tgoogle.golang.org/protobuf v1.36.11 // indirect\n)\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/go.sum",
    "content": "github.com/IBM/sarama v1.48.0 h1:9LJS0VNeg/boXxT/GLAMDKX6uSQ1mr/5F/j4v9gSeBQ=\ngithub.com/IBM/sarama v1.48.0/go.mod h1:UhvwPF8zilmLOSd6O+ENzdycCJYwMww1U9DJOZpoCro=\ngithub.com/IBM/sarama v1.48.1 h1:x1dSWebprjjE7Wr7n8RVAxwa4mt4O9JejRxnZrGIXk0=\ngithub.com/IBM/sarama v1.48.1/go.mod h1:m/Q1aFezH82/AglfTpJbw/fO0ZybYXhPgTmvajiZX50=\ngithub.com/XSAM/otelsql v0.42.0 h1:Li0xF4eJUxG2e0x3D4rvRlys1f27yJKvjTh7ljkUP5o=\ngithub.com/XSAM/otelsql v0.42.0/go.mod h1:4mOrEv+cS1KmKzrvTktvJnstr5GtKSAK+QHvFR9OcpI=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/eapache/go-resiliency v1.7.0 h1:n3NRTnBn5N0Cbi/IeOHuQn9s2UwVUH7Ga0ZWcP+9JTA=\ngithub.com/eapache/go-resiliency v1.7.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=\ngithub.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=\ngithub.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\ngithub.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=\ngithub.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=\ngithub.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=\ngithub.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=\ngithub.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=\ngithub.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=\ngithub.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=\ngithub.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=\ngithub.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=\ngithub.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=\ngithub.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=\ngithub.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=\ngithub.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=\ngithub.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=\ngithub.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/segmentio/kafka-go v0.4.51 h1:JgDPPG75tC1rWIS2Me6MwcvXJ6f49UQ4HjAOef71Hno=\ngithub.com/segmentio/kafka-go v0.4.51/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260503095836-333309ee621f h1:9LnGCifF9X/Udrjo4BFBf86LVMTgBnTbCMMw1KLokvI=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260503095836-333309ee621f/go.mod h1:iLVd0USfKOR+f5CQkdsoeBhE1hN4V6XG5/6+kyGfT00=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260503102840-e1066b3be7fe h1:pYVQ83PqcHCBG1rhe4OM+vFMLaxtQ+NHqphiB1hYoLU=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260503102840-e1066b3be7fe/go.mod h1:iLVd0USfKOR+f5CQkdsoeBhE1hN4V6XG5/6+kyGfT00=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260503221219-fcfb87c85a78 h1:vZFpp8fLTkkXjuJFPJBRCEJmwW48S0+m1gfN8sZUPag=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260503221219-fcfb87c85a78/go.mod h1:iLVd0USfKOR+f5CQkdsoeBhE1hN4V6XG5/6+kyGfT00=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260506073743-d4b7ee1d0368 h1:bzdIcvw7f1O9/uAsOlPSFwugYZ/W+U+Nmp6jF+aAnSE=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260506073743-d4b7ee1d0368/go.mod h1:/FSas5cvybs+2bAM1mlfP193vPoI5RJOXWQuu7q9ncE=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260511094143-5fae367ca053 h1:zCLRdLXmYhVJIw5lR5oGuPM0IMyZ41sGepMEcq7e0n8=\ngithub.com/trendyol/stove/go/stove-kafka v0.0.0-20260511094143-5fae367ca053/go.mod h1:LlJxOgdy8NlPub93cxvFNBOev22rlXna/nM3t0N2TxM=\ngithub.com/twmb/franz-go v1.21.0 h1:J3uB/poWgHD6VIilER2uCPFAZHDRXVFT+11pBgRKod4=\ngithub.com/twmb/franz-go v1.21.0/go.mod h1:1o+jj5oRbItsIMoE+DGpfJIcPcPtDdtkcNFPj4bWNwU=\ngithub.com/twmb/franz-go v1.21.1 h1:sp17bMRLz6OB/w+7vHtBadHGIQVymzQHwvRbEKe5c4I=\ngithub.com/twmb/franz-go v1.21.1/go.mod h1:1o+jj5oRbItsIMoE+DGpfJIcPcPtDdtkcNFPj4bWNwU=\ngithub.com/twmb/franz-go/pkg/kmsg v1.13.1 h1:fG5kItwysTk5UXqVwb64EpQEy3TydF3vYYK21nUQ+bI=\ngithub.com/twmb/franz-go/pkg/kmsg v1.13.1/go.mod h1:+DPt4NC8RmI6hqb8G09+3giKObE6uD2Eya6CfqBpeJY=\ngithub.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=\ngithub.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=\ngithub.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=\ngithub.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=\ngo.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=\ngo.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=\ngo.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=\ngo.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=\ngo.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=\ngo.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=\ngo.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=\ngo.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=\ngo.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=\ngo.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=\ngo.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=\ngolang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=\ngolang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=\ngolang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=\ngolang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=\ngolang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=\ngolang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=\ngolang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=\ngolang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=\ngolang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=\ngolang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=\ngolang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=\ngolang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=\ngonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=\ngoogle.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=\ngoogle.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=\ngoogle.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/gradle/libs.versions.toml",
    "content": "[versions]\nstove = \"1.0.0.529-SNAPSHOT\"\nkotest = \"6.1.11\"\n\n[libraries]\nstove-bom = { module = \"com.trendyol:stove-bom\", version.ref = \"stove\" }\nkotest-runner-junit5 = { module = \"io.kotest:kotest-runner-junit5-jvm\", version.ref = \"kotest\" }\nkotest-framework-engine = { module = \"io.kotest:kotest-framework-engine\", version.ref = \"kotest\" }\nkotest-assertions-core = { module = \"io.kotest:kotest-assertions-core\", version.ref = \"kotest\" }\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.5.0-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/gradle.properties",
    "content": "org.gradle.parallel=false\norg.gradle.caching=true\norg.gradle.configuration-cache=true\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/handlers.go",
    "content": "package main\n\nimport (\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/google/uuid\"\n)\n\n// Product represents a product entity.\ntype Product struct {\n\tID    string  `json:\"id\"`\n\tName  string  `json:\"name\"`\n\tPrice float64 `json:\"price\"`\n}\n\ntype createProductRequest struct {\n\tName  string  `json:\"name\"`\n\tPrice float64 `json:\"price\"`\n}\n\nfunc registerRoutes(mux *http.ServeMux, db *sql.DB, producer KafkaProducer) {\n\tmux.HandleFunc(\"GET /health\", handleHealth)\n\tmux.HandleFunc(\"POST /api/products\", handleCreateProduct(db, producer))\n\tmux.HandleFunc(\"GET /api/products/{id}\", handleGetProduct(db))\n\tmux.HandleFunc(\"GET /api/products\", handleListProducts(db))\n}\n\nfunc handleHealth(w http.ResponseWriter, _ *http.Request) {\n\twriteJSON(w, http.StatusOK, map[string]string{\"status\": \"UP\"})\n}\n\nfunc handleCreateProduct(db *sql.DB, producer KafkaProducer) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tvar req createProductRequest\n\t\tif err := json.NewDecoder(r.Body).Decode(&req); err != nil {\n\t\t\thttp.Error(w, `{\"error\":\"invalid request body\"}`, http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tproduct := Product{\n\t\t\tID:    uuid.New().String(),\n\t\t\tName:  req.Name,\n\t\t\tPrice: req.Price,\n\t\t}\n\n\t\tif err := insertProduct(r.Context(), db, product); err != nil {\n\t\t\thttp.Error(w, `{\"error\":\"failed to create product\"}`, http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t// Publish ProductCreatedEvent to Kafka\n\t\tif producer != nil {\n\t\t\tevent := ProductCreatedEvent{ID: product.ID, Name: product.Name, Price: product.Price}\n\t\t\teventBytes, err := json.Marshal(event)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"failed to marshal ProductCreatedEvent: %v\", err)\n\t\t\t} else if err := producer.SendMessage(topicProductCreated, product.ID, eventBytes); err != nil {\n\t\t\t\tlog.Printf(\"failed to publish ProductCreatedEvent: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\twriteJSON(w, http.StatusCreated, product)\n\t}\n}\n\nfunc handleGetProduct(db *sql.DB) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tid := r.PathValue(\"id\")\n\n\t\tproduct, err := getProduct(r.Context(), db, id)\n\t\tif err != nil {\n\t\t\thttp.Error(w, `{\"error\":\"internal error\"}`, http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tif product == nil {\n\t\t\thttp.Error(w, `{\"error\":\"not found\"}`, http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\twriteJSON(w, http.StatusOK, product)\n\t}\n}\n\nfunc handleListProducts(db *sql.DB) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tproducts, err := listProducts(r.Context(), db)\n\t\tif err != nil {\n\t\t\thttp.Error(w, `{\"error\":\"internal error\"}`, http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tif products == nil {\n\t\t\tproducts = []Product{}\n\t\t}\n\n\t\twriteJSON(w, http.StatusOK, products)\n\t}\n}\n\nfunc writeJSON(w http.ResponseWriter, status int, v any) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(status)\n\tjson.NewEncoder(w).Encode(v)\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/kafka.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"encoding/json\"\n\t\"log\"\n\n\tstovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n)\n\nconst (\n\ttopicProductCreated = \"product.created\"\n\ttopicProductUpdate  = \"product.update\"\n)\n\n// ProductCreatedEvent is published to the product.created topic when a product is created.\ntype ProductCreatedEvent struct {\n\tID    string  `json:\"id\"`\n\tName  string  `json:\"name\"`\n\tPrice float64 `json:\"price\"`\n}\n\n// ProductUpdateEvent is consumed from the product.update topic to update existing products.\ntype ProductUpdateEvent struct {\n\tID    string  `json:\"id\"`\n\tName  string  `json:\"name\"`\n\tPrice float64 `json:\"price\"`\n}\n\n// KafkaProducer abstracts message production across different Kafka client libraries.\ntype KafkaProducer interface {\n\tSendMessage(topic, key string, value []byte) error\n\tClose() error\n}\n\n// initKafka creates a producer and starts a consumer using the specified library.\n// Supported libraries: \"sarama\" (default), \"franz\", \"segmentio\".\nfunc initKafka(library, brokers string, db *sql.DB, bridge *stovekafka.Bridge) (KafkaProducer, func(), error) {\n\tif brokers == \"\" {\n\t\treturn nil, func() {}, nil\n\t}\n\n\tlog.Printf(\"Kafka initializing with library=%s brokers=%s\", library, brokers)\n\n\tgroupID := \"go-showcase-\" + library\n\n\tswitch library {\n\tcase \"franz\":\n\t\treturn initFranzKafka(brokers, groupID, db, bridge)\n\tcase \"segmentio\":\n\t\treturn initSegmentioKafka(brokers, groupID, db, bridge)\n\tdefault:\n\t\treturn initSaramaKafka(brokers, groupID, db, bridge)\n\t}\n}\n\n// handleProductUpdate is shared consumer logic for all Kafka libraries.\nfunc handleProductUpdate(db *sql.DB, value []byte) {\n\tvar event ProductUpdateEvent\n\tif err := json.Unmarshal(value, &event); err != nil {\n\t\tlog.Printf(\"failed to unmarshal update event: %v\", err)\n\t\treturn\n\t}\n\n\tif err := updateProduct(context.Background(), db, event.ID, event.Name, event.Price); err != nil {\n\t\tlog.Printf(\"failed to update product %s: %v\", event.ID, err)\n\t}\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/kafka_franz.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\tstovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n\tstovefranz \"github.com/trendyol/stove/go/stove-kafka/franz\"\n\t\"github.com/twmb/franz-go/pkg/kgo\"\n)\n\ntype franzProducer struct {\n\tclient *kgo.Client\n}\n\nfunc (p *franzProducer) SendMessage(topic, key string, value []byte) error {\n\tresults := p.client.ProduceSync(context.Background(), &kgo.Record{\n\t\tTopic: topic,\n\t\tKey:   []byte(key),\n\t\tValue: value,\n\t})\n\treturn results.FirstErr()\n}\n\nfunc (p *franzProducer) Close() error {\n\tp.client.Close()\n\treturn nil\n}\n\nfunc initFranzKafka(brokers, groupID string, db *sql.DB, bridge *stovekafka.Bridge) (KafkaProducer, func(), error) {\n\tbrokerList := strings.Split(brokers, \",\")\n\thook := &stovefranz.Hook{Bridge: bridge}\n\n\t// Separate producer client — no consumer group overhead\n\tproducerClient, err := kgo.NewClient(\n\t\tkgo.SeedBrokers(brokerList...),\n\t\tkgo.AllowAutoTopicCreation(),\n\t\tkgo.WithHooks(hook),\n\t)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Separate consumer client — consumer group coordination won't block produces\n\tconsumerClient, err := kgo.NewClient(\n\t\tkgo.SeedBrokers(brokerList...),\n\t\tkgo.ConsumeTopics(topicProductUpdate),\n\t\tkgo.ConsumerGroup(groupID),\n\t\tkgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),\n\t\tkgo.AutoCommitInterval(100*time.Millisecond),\n\t\tkgo.AllowAutoTopicCreation(),\n\t\tkgo.WithHooks(hook),\n\t)\n\tif err != nil {\n\t\tproducerClient.Close()\n\t\treturn nil, nil, err\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\tfor {\n\t\t\tfetches := consumerClient.PollFetches(ctx)\n\t\t\tif ctx.Err() != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfetches.EachRecord(func(r *kgo.Record) {\n\t\t\t\tif r.Topic == topicProductUpdate {\n\t\t\t\t\thandleProductUpdate(db, r.Value)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}()\n\n\tstop := func() {\n\t\tcancel()\n\t\tconsumerClient.Close()\n\t\tproducerClient.Close()\n\t}\n\n\tlog.Printf(\"Kafka (franz-go) initialized\")\n\treturn &franzProducer{client: producerClient}, stop, nil\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/kafka_sarama.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/IBM/sarama\"\n\tstovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n\tstovesarama \"github.com/trendyol/stove/go/stove-kafka/sarama\"\n)\n\ntype saramaProducer struct {\n\tproducer sarama.SyncProducer\n}\n\nfunc (p *saramaProducer) SendMessage(topic, key string, value []byte) error {\n\t_, _, err := p.producer.SendMessage(&sarama.ProducerMessage{\n\t\tTopic: topic,\n\t\tKey:   sarama.StringEncoder(key),\n\t\tValue: sarama.ByteEncoder(value),\n\t})\n\treturn err\n}\n\nfunc (p *saramaProducer) Close() error {\n\treturn p.producer.Close()\n}\n\nfunc initSaramaKafka(brokers, groupID string, db *sql.DB, bridge *stovekafka.Bridge) (KafkaProducer, func(), error) {\n\tbrokerList := strings.Split(brokers, \",\")\n\n\tconfig := sarama.NewConfig()\n\tconfig.Producer.Return.Successes = true\n\tconfig.Consumer.Offsets.Initial = sarama.OffsetOldest\n\tconfig.Consumer.Offsets.AutoCommit.Interval = 100 * time.Millisecond\n\n\tconfig.Producer.Interceptors = []sarama.ProducerInterceptor{\n\t\t&stovesarama.ProducerInterceptor{Bridge: bridge},\n\t}\n\tconfig.Consumer.Interceptors = []sarama.ConsumerInterceptor{\n\t\t&stovesarama.ConsumerInterceptor{Bridge: bridge},\n\t}\n\n\tproducer, err := sarama.NewSyncProducer(brokerList, config)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tconsumerGroup, err := sarama.NewConsumerGroup(brokerList, groupID, config)\n\tif err != nil {\n\t\tproducer.Close()\n\t\treturn nil, nil, err\n\t}\n\n\tctx, cancel := context.WithCancel(context.Background())\n\thandler := &saramaUpdateHandler{db: db}\n\n\tgo func() {\n\t\tfor {\n\t\t\tif err := consumerGroup.Consume(ctx, []string{topicProductUpdate}, handler); err != nil {\n\t\t\t\tlog.Printf(\"sarama consumer group error: %v\", err)\n\t\t\t}\n\t\t\tif ctx.Err() != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tstop := func() {\n\t\tcancel()\n\t\tconsumerGroup.Close()\n\t\tproducer.Close()\n\t}\n\n\tlog.Printf(\"Kafka (sarama) initialized\")\n\treturn &saramaProducer{producer: producer}, stop, nil\n}\n\ntype saramaUpdateHandler struct {\n\tdb *sql.DB\n}\n\nfunc (h *saramaUpdateHandler) Setup(_ sarama.ConsumerGroupSession) error   { return nil }\nfunc (h *saramaUpdateHandler) Cleanup(_ sarama.ConsumerGroupSession) error { return nil }\n\nfunc (h *saramaUpdateHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {\n\tfor msg := range claim.Messages() {\n\t\thandleProductUpdate(h.db, msg.Value)\n\t\tsession.MarkMessage(msg, \"\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/kafka_segmentio.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"database/sql\"\n\t\"errors\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\tkafka \"github.com/segmentio/kafka-go\"\n\tstovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n\t\"github.com/trendyol/stove/go/stove-kafka/segmentio\"\n)\n\ntype segmentioProducer struct {\n\twriter *kafka.Writer\n\tbridge *stovekafka.Bridge\n}\n\nfunc (p *segmentioProducer) SendMessage(topic, key string, value []byte) error {\n\tctx := context.Background()\n\tmsg := kafka.Message{\n\t\tTopic: topic,\n\t\tKey:   []byte(key),\n\t\tValue: value,\n\t}\n\tif err := p.writer.WriteMessages(ctx, msg); err != nil {\n\t\treturn err\n\t}\n\tsegmentio.ReportWritten(ctx, p.bridge, msg)\n\treturn nil\n}\n\nfunc (p *segmentioProducer) Close() error {\n\treturn p.writer.Close()\n}\n\nfunc initSegmentioKafka(brokers, groupID string, db *sql.DB, bridge *stovekafka.Bridge) (KafkaProducer, func(), error) {\n\tbrokerList := strings.Split(brokers, \",\")\n\n\twriter := &kafka.Writer{\n\t\tAddr:                   kafka.TCP(brokerList...),\n\t\tBatchSize:              1,\n\t\tBatchTimeout:           10 * time.Millisecond,\n\t\tRequiredAcks:           kafka.RequireAll,\n\t\tAllowAutoTopicCreation: true,\n\t}\n\n\treader := kafka.NewReader(kafka.ReaderConfig{\n\t\tBrokers:        brokerList,\n\t\tGroupID:        groupID,\n\t\tTopic:          topicProductUpdate,\n\t\tMinBytes:       1,\n\t\tMaxBytes:       10e6,\n\t\tCommitInterval: 100 * time.Millisecond,\n\t\tMaxWait:        500 * time.Millisecond,\n\t})\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\tfor {\n\t\t\tmsg, err := reader.ReadMessage(ctx)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, context.Canceled) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"segmentio reader error: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tsegmentio.ReportRead(ctx, bridge, msg)\n\t\t\thandleProductUpdate(db, msg.Value)\n\t\t}\n\t}()\n\n\tstop := func() {\n\t\tcancel()\n\t\treader.Close()\n\t\twriter.Close()\n\t}\n\n\tlog.Printf(\"Kafka (segmentio/kafka-go) initialized\")\n\treturn &segmentioProducer{writer: writer, bridge: bridge}, stop, nil\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\tstovekafka \"github.com/trendyol/stove/go/stove-kafka\"\n\t\"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp\"\n)\n\nfunc getEnv(key, fallback string) string {\n\tif v := os.Getenv(key); v != \"\" {\n\t\treturn v\n\t}\n\treturn fallback\n}\n\nfunc main() {\n\t// Ignore SIGPIPE so log writes to a closed stdout pipe don't kill the process.\n\t// This ensures clean shutdown (and coverage flush) when run under a process manager.\n\tsignal.Ignore(syscall.SIGPIPE)\n\n\tctx := context.Background()\n\n\tport := getEnv(\"APP_PORT\", \"8080\")\n\tdbHost := getEnv(\"DB_HOST\", \"localhost\")\n\tdbPort := getEnv(\"DB_PORT\", \"5432\")\n\tdbName := getEnv(\"DB_NAME\", \"stove\")\n\tdbUser := getEnv(\"DB_USER\", \"sa\")\n\tdbPass := getEnv(\"DB_PASS\", \"sa\")\n\n\tshutdownTracing, err := initTracing(ctx, \"go-showcase\")\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to init tracing: %v\", err)\n\t}\n\tdefer shutdownTracing(ctx)\n\n\tconnStr := fmt.Sprintf(\n\t\t\"host=%s port=%s dbname=%s user=%s password=%s sslmode=disable\",\n\t\tdbHost, dbPort, dbName, dbUser, dbPass,\n\t)\n\n\tdb, err := initDB(connStr)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to connect to database: %v\", err)\n\t}\n\tdefer db.Close()\n\n\t// Initialize Stove Kafka bridge (nil in production — zero overhead)\n\tbridge, err := stovekafka.NewBridgeFromEnv()\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to init stove bridge: %v\", err)\n\t}\n\tdefer bridge.Close()\n\n\t// Initialize Kafka producer and consumer\n\tkafkaLibrary := getEnv(\"KAFKA_LIBRARY\", \"sarama\")\n\tbrokers := getEnv(\"KAFKA_BROKERS\", \"\")\n\tproducer, stopKafka, err := initKafka(kafkaLibrary, brokers, db, bridge)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to init kafka: %v\", err)\n\t}\n\tdefer stopKafka()\n\n\tmux := http.NewServeMux()\n\tregisterRoutes(mux, db, producer)\n\n\t// Wrap with OTel HTTP instrumentation for automatic span creation\n\thandler := otelhttp.NewHandler(mux, \"http.request\")\n\n\tserver := &http.Server{\n\t\tAddr:              \":\" + port,\n\t\tHandler:           handler,\n\t\tReadHeaderTimeout: 10 * time.Second,\n\t}\n\n\t// Graceful shutdown on SIGTERM/SIGINT\n\tstop := make(chan os.Signal, 1)\n\tsignal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)\n\n\tgo func() {\n\t\tlog.Printf(\"Go showcase app listening on :%s\", port)\n\t\tif err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {\n\t\t\tlog.Fatalf(\"server error: %v\", err)\n\t\t}\n\t}()\n\n\t<-stop\n\tlog.Println(\"shutting down...\")\n\n\tshutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\tdefer cancel()\n\n\tif err := server.Shutdown(shutdownCtx); err != nil {\n\t\tlog.Fatalf(\"shutdown error: %v\", err)\n\t}\n\tlog.Println(\"server stopped\")\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/settings.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nimport dev.aga.gradle.versioncatalogs.Generator.generate\nimport dev.aga.gradle.versioncatalogs.GeneratorConfig\n\nrootProject.name = \"go-showcase\"\nval useMavenLocal = providers.gradleProperty(\"useMavenLocal\").map(String::toBoolean).getOrElse(false)\n\npluginManagement {\n  repositories {\n    gradlePluginPortal()\n    mavenCentral()\n    maven(\"https://central.sonatype.com/repository/maven-snapshots\")\n  }\n}\n\nplugins {\n  id(\"dev.aga.gradle.version-catalog-generator\") version \"4.2.0\"\n}\n\ndependencyResolutionManagement {\n  repositories {\n    if (useMavenLocal) {\n      mavenLocal()\n    }\n    mavenCentral()\n    maven(\"https://central.sonatype.com/repository/maven-snapshots\") {\n      content {\n        includeGroup(\"com.trendyol\")\n      }\n    }\n  }\n\n  versionCatalogs {\n    generate(\"stoveLibs\") {\n      fromToml(\"stove-bom\") {\n        aliasPrefixGenerator = GeneratorConfig.NO_PREFIX\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/stovetests/kotlin/com/trendyol/stove/examples/go/e2e/setup/ProductMigration.kt",
    "content": "package com.trendyol.stove.examples.go.e2e.setup\n\nimport com.trendyol.stove.database.migrations.DatabaseMigration\nimport com.trendyol.stove.postgres.PostgresSqlMigrationContext\n\nclass ProductMigration : DatabaseMigration<PostgresSqlMigrationContext> {\n  override val order: Int = 1\n\n  override suspend fun execute(connection: PostgresSqlMigrationContext) {\n    connection.operations.execute(\n      \"\"\"\n      CREATE TABLE IF NOT EXISTS products (\n          id VARCHAR(255) PRIMARY KEY,\n          name VARCHAR(255) NOT NULL,\n          price DECIMAL(10, 2) NOT NULL\n      );\n      \"\"\".trimIndent()\n    )\n  }\n}\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/stovetests/kotlin/com/trendyol/stove/examples/go/e2e/setup/StoveConfig.kt",
    "content": "package com.trendyol.stove.examples.go.e2e.setup\n\nimport com.trendyol.stove.container.ContainerTarget\nimport com.trendyol.stove.container.containerApp\nimport com.trendyol.stove.dashboard.DashboardSystemOptions\nimport com.trendyol.stove.dashboard.dashboard\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.http.HttpClientSystemOptions\nimport com.trendyol.stove.http.httpClient\nimport com.trendyol.stove.kafka.KafkaSystemOptions\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.kafka.stoveKafkaBridgePortDefault\nimport com.trendyol.stove.postgres.PostgresqlOptions\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.process.ProcessTarget\nimport com.trendyol.stove.process.envMapper as processEnvMapper\nimport com.trendyol.stove.process.goApp\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.application.envMapper as containerEnvMapper\nimport com.trendyol.stove.tracing.tracing\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport java.io.File\n\nprivate const val APP_PORT = 8090\nprivate const val OTLP_PORT = 4317\nprivate const val COVERAGE_DIR_IN_CONTAINER = \"/tmp/go-coverage\"\n\nprivate enum class GoAutMode {\n  Process,\n  Container\n}\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject() {\n    val autMode = resolveAutMode()\n    val appImage = System.getProperty(\"go.app.container.image\").orEmpty()\n    val kafkaLibrary = System.getProperty(\"kafka.library\") ?: \"sarama\"\n    val hostCoverageDir = System.getProperty(\"go.cover.dir\").orEmpty()\n    val coverageDirInContainer = if (hostCoverageDir.isBlank()) \"\" else COVERAGE_DIR_IN_CONTAINER\n\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(baseUrl = \"http://localhost:$APP_PORT\")\n        }\n\n        dashboard {\n          DashboardSystemOptions(appName = \"go-showcase\")\n        }\n\n        tracing {\n          enableSpanReceiver(port = OTLP_PORT)\n        }\n\n        kafka {\n          KafkaSystemOptions(\n            configureExposedConfiguration = { cfg ->\n              listOf(\"kafka.bootstrapServers=${cfg.bootstrapServers}\")\n            }\n          )\n        }\n\n        postgresql {\n          PostgresqlOptions(\n            databaseName = \"stove\",\n            configureExposedConfiguration = { cfg ->\n              listOf(\n                \"database.host=${cfg.host}\",\n                \"database.port=${cfg.port}\",\n                \"database.name=stove\",\n                \"database.username=${cfg.username}\",\n                \"database.password=${cfg.password}\"\n              )\n            }\n          ).migrations {\n            register<ProductMigration>()\n          }\n        }\n\n        when (autMode) {\n          GoAutMode.Process -> goApp(\n            target = ProcessTarget.Server(port = APP_PORT, portEnvVar = \"APP_PORT\"),\n            envProvider = processEnvMapper {\n              \"database.host\" to \"DB_HOST\"\n              \"database.port\" to \"DB_PORT\"\n              \"database.name\" to \"DB_NAME\"\n              \"database.username\" to \"DB_USER\"\n              \"database.password\" to \"DB_PASS\"\n              \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n              env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:$OTLP_PORT\")\n              env(\"KAFKA_LIBRARY\", kafkaLibrary)\n              env(\"STOVE_KAFKA_BRIDGE_PORT\", stoveKafkaBridgePortDefault)\n              env(\"GOCOVERDIR\") {\n                hostCoverageDir.takeIf { it.isNotBlank() }?.also { File(it).mkdirs() } ?: \"\"\n              }\n            }\n          )\n\n          GoAutMode.Container -> {\n            require(appImage.isNotBlank()) { \"go.app.container.image system property not set\" }\n            containerApp(\n              image = appImage,\n              target = ContainerTarget.Server(\n                hostPort = APP_PORT,\n                internalPort = APP_PORT,\n                portEnvVar = \"APP_PORT\",\n                bindHostPort = false\n              ),\n              envProvider = containerEnvMapper {\n                \"database.host\" to \"DB_HOST\"\n                \"database.port\" to \"DB_PORT\"\n                \"database.name\" to \"DB_NAME\"\n                \"database.username\" to \"DB_USER\"\n                \"database.password\" to \"DB_PASS\"\n                \"kafka.bootstrapServers\" to \"KAFKA_BROKERS\"\n                env(\"OTEL_EXPORTER_OTLP_ENDPOINT\", \"localhost:$OTLP_PORT\")\n                env(\"KAFKA_LIBRARY\", kafkaLibrary)\n                env(\"STOVE_KAFKA_BRIDGE_PORT\", stoveKafkaBridgePortDefault)\n                env(\"GOCOVERDIR\", coverageDirInContainer)\n              },\n              configureContainer = {\n                withNetworkMode(\"host\")\n                if (hostCoverageDir.isNotBlank()) {\n                  withFileSystemBind(hostCoverageDir, COVERAGE_DIR_IN_CONTAINER)\n                }\n              }\n            )\n          }\n        }\n      }.run()\n  }\n\n  override suspend fun afterProject() {\n    Stove.stop()\n  }\n}\n\nprivate fun resolveAutMode(): GoAutMode =\n  when ((System.getProperty(\"go.aut.mode\") ?: System.getenv(\"GO_AUT_MODE\") ?: \"process\").lowercase()) {\n    \"process\" -> GoAutMode.Process\n    \"container\" -> GoAutMode.Container\n    else -> error(\"Unsupported go.aut.mode. Use 'process' or 'container'.\")\n  }\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/stovetests/kotlin/com/trendyol/stove/examples/go/e2e/tests/GoShowcaseTest.kt",
    "content": "package com.trendyol.stove.examples.go.e2e.tests\n\nimport arrow.core.some\nimport com.trendyol.stove.http.http\nimport com.trendyol.stove.kafka.kafka\nimport com.trendyol.stove.postgres.postgresql\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.tracing.tracing\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\nimport kotliquery.Row\nimport kotlin.time.Duration.Companion.seconds\n\ndata class CreateProductRequest(\n  val name: String,\n  val price: Double\n)\n\ndata class ProductResponse(\n  val id: String,\n  val name: String,\n  val price: Double\n)\n\ndata class ProductRow(\n  val id: String,\n  val name: String,\n  val price: Double\n)\n\ndata class ProductCreatedEvent(\n  val id: String,\n  val name: String,\n  val price: Double\n)\n\ndata class ProductUpdateEvent(\n  val id: String,\n  val name: String,\n  val price: Double\n)\n\nprivate val productRowMapper: (Row) -> ProductRow = { row ->\n  ProductRow(\n    id = row.string(\"id\"),\n    name = row.string(\"name\"),\n    price = row.double(\"price\")\n  )\n}\n\nclass GoShowcaseTest :\n  FunSpec({\n\n    test(\"should create a product and verify via HTTP, database, and traces\") {\n      stove {\n        val productName = \"Stove Go Showcase Product\"\n        val productPrice = 42.99\n        var productId: String? = null\n\n        // 1. Create a product via the Go application's REST API\n        http {\n          postAndExpectBody<ProductResponse>(\n            uri = \"/api/products\",\n            body = CreateProductRequest(name = productName, price = productPrice).some()\n          ) { actual ->\n            actual.status shouldBe 201\n            productId = actual.body().id\n            actual.body().name shouldBe productName\n            actual.body().price shouldBe productPrice\n          }\n        }\n\n        // 2. Verify the product was persisted in PostgreSQL\n        postgresql {\n          shouldQuery<ProductRow>(\n            query = \"SELECT id, name, price FROM products WHERE id = '$productId'\",\n            mapper = productRowMapper\n          ) { rows ->\n            rows.size shouldBe 1\n            rows.first().name shouldBe productName\n            rows.first().price shouldBe productPrice\n          }\n        }\n\n        // 3. Read the product back via HTTP\n        http {\n          getResponse<ProductResponse>(\n            uri = \"/api/products/$productId\"\n          ) { actual ->\n            actual.status shouldBe 200\n            actual.body().id shouldBe productId\n            actual.body().name shouldBe productName\n          }\n        }\n\n        // 4. Verify traces — spans are auto-created by otelhttp middleware and otelsql driver\n        tracing {\n          waitForSpans(4, 5000)\n          shouldContainSpan(\"http.request\")\n          shouldNotHaveFailedSpans()\n          spanCountShouldBeAtLeast(4)\n          executionTimeShouldBeLessThan(30.seconds)\n        }\n      }\n    }\n\n    test(\"should list all products\") {\n      stove {\n        http {\n          postAndExpectBody<ProductResponse>(\n            uri = \"/api/products\",\n            body = CreateProductRequest(name = \"Product A\", price = 10.0).some()\n          ) { actual ->\n            actual.body().name shouldBe \"Product A\"\n          }\n        }\n\n        http {\n          postAndExpectBody<ProductResponse>(\n            uri = \"/api/products\",\n            body = CreateProductRequest(name = \"Product B\", price = 20.0).some()\n          ) { actual ->\n            actual.body().name shouldBe \"Product B\"\n          }\n        }\n\n        http {\n          getMany<ProductResponse>(\n            uri = \"/api/products\"\n          ) { actual ->\n            actual.size shouldNotBe 0\n          }\n        }\n\n        tracing {\n          waitForSpans(4, 5000)\n          shouldContainSpan(\"http.request\")\n          shouldNotHaveFailedSpans()\n        }\n      }\n    }\n\n    test(\"should return 404 for non-existent product\") {\n      stove {\n        http {\n          getBodilessResponse(\"/api/products/non-existent-id\") { actual ->\n            actual.status shouldBe 404\n          }\n        }\n      }\n    }\n\n    test(\"should publish ProductCreatedEvent when product is created\") {\n      stove {\n        http {\n          postAndExpectBody<ProductResponse>(\n            uri = \"/api/products\",\n            body = CreateProductRequest(name = \"Kafka Product\", price = 29.99).some()\n          ) { actual ->\n            actual.status shouldBe 201\n            actual.body().name shouldBe \"Kafka Product\"\n          }\n        }\n\n        kafka {\n          shouldBePublished<ProductCreatedEvent>(10.seconds) {\n            actual.name == \"Kafka Product\" && actual.price == 29.99\n          }\n        }\n      }\n    }\n\n    test(\"should consume product update events from Kafka\") {\n      stove {\n        var productId: String? = null\n\n        // First create a product via HTTP\n        http {\n          postAndExpectBody<ProductResponse>(\n            uri = \"/api/products\",\n            body = CreateProductRequest(name = \"Original Name\", price = 10.0).some()\n          ) { actual ->\n            actual.status shouldBe 201\n            productId = actual.body().id\n          }\n        }\n\n        // Publish an update event to Kafka — Go consumer picks it up and updates DB\n        kafka {\n          publish(\"product.update\", ProductUpdateEvent(id = productId!!, name = \"Updated Name\", price = 99.99))\n          shouldBeConsumed<ProductUpdateEvent>(10.seconds) {\n            actual.id == productId && actual.name == \"Updated Name\"\n          }\n        }\n\n        // Verify the product was updated in the database\n        postgresql {\n          shouldQuery<ProductRow>(\n            query = \"SELECT id, name, price FROM products WHERE id = '$productId'\",\n            mapper = productRowMapper\n          ) { rows ->\n            rows.size shouldBe 1\n            rows.first().name shouldBe \"Updated Name\"\n            rows.first().price shouldBe 99.99\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/stovetests/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.examples.go.e2e.setup.StoveConfig\n"
  },
  {
    "path": "recipes/process/golang/go-showcase/tracing.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc\"\n\t\"go.opentelemetry.io/otel/propagation\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\tsdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.26.0\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/grpc/credentials/insecure\"\n)\n\nfunc initTracing(ctx context.Context, serviceName string) (func(context.Context), error) {\n\tendpoint := os.Getenv(\"OTEL_EXPORTER_OTLP_ENDPOINT\")\n\tif endpoint == \"\" {\n\t\treturn func(context.Context) {}, nil\n\t}\n\n\tconn, err := grpc.NewClient(endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\texporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres, err := resource.New(ctx,\n\t\tresource.WithAttributes(semconv.ServiceNameKey.String(serviceName)),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttp := sdktrace.NewTracerProvider(\n\t\tsdktrace.WithSyncer(exporter),\n\t\tsdktrace.WithResource(res),\n\t)\n\n\totel.SetTracerProvider(tp)\n\totel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(\n\t\tpropagation.TraceContext{},\n\t\tpropagation.Baggage{},\n\t))\n\n\tlog.Printf(\"Tracing enabled, exporting to %s\", endpoint)\n\n\treturn func(ctx context.Context) {\n\t\tif err := tp.Shutdown(ctx); err != nil {\n\t\t\tlog.Printf(\"tracing shutdown error: %v\", err)\n\t\t}\n\t}, nil\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"extends\": [\n    \"config:recommended\",\n    \"group:allNonMajor\",\n    \"group:monorepos\",\n    \"schedule:earlyMondays\"\n  ],\n  \"branchPrefix\": \"renovate/\",\n  \"packageRules\": [\n    {\n      \"allowedVersions\": \"!/M/\",\n      \"matchPackageNames\": [\n        \"/io.kotest:kotest.*/\"\n      ]\n    },\n    {\n      \"matchDepNames\": [\n        \"/org.jetbrains.kotlin.*/\",\n        \"/com.google.devtools.ksp.*/\"\n      ],\n      \"groupName\": \"kotlin\"\n    },\n    {\n      \"matchDepNames\": [\n        \"/.*micronaut.*/\"\n      ],\n      \"groupName\": \"micronaut\"\n    },\n    {\n      \"matchDepNames\": [\n        \"/.*springframework.*/\"\n      ],\n      \"groupName\": \"spring\"\n    },\n    {\n      \"matchDepNames\": [\n        \"/.*quarkus.*/\"\n      ],\n      \"groupName\": \"quarkus\"\n    },\n    {\n      \"matchDepNames\": [\n        \"/io.confluent.*/\"\n      ],\n      \"groupName\": \"confluent\"\n    },\n    {\n      \"matchDepNames\": [\n        \"/org.apache.kafka.*/\"\n      ],\n      \"groupName\": \"kafka\"\n    },\n    {\n      \"matchDepNames\": [\n        \"/.*springframework.*/\"\n      ],\n      \"matchUpdateTypes\": [\"major\"],\n      \"enabled\": false\n    },\n    {\n      \"matchDepNames\": [\n        \"/org.apache.kafka.*/\"\n      ],\n      \"allowedVersions\": \"!/^[78]\\\\./\"\n    },\n    {\n      \"matchDepNames\": [\n        \"org.scala-lang:scala-library\"\n      ],\n      \"matchUpdateTypes\": [\"major\"],\n      \"enabled\": false\n    },\n    {\n      \"matchDepNames\": [\n        \"/org.jetbrains.kotlinx:kotlinx-coroutines.*/\"\n      ],\n      \"allowedVersions\": \"<=1.10.2\"\n    },\n    {\n      \"groupName\": \"com.trendyol\",\n      \"allowedVersions\": \"!/1f1ca59/\",\n      \"matchPackageNames\": [\n        \"/com.trendyol:stove.*/\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\nenableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\nrootProject.name = \"stove\"\n\npluginManagement {\n  repositories {\n    gradlePluginPortal()\n    mavenCentral()\n  }\n}\n\ninclude(\n  \"lib:stove-bom\",\n  \"lib:stove\",\n  \"lib:stove-tracing\",\n  \"lib:stove-wiremock\",\n  \"lib:stove-grpc-mock\",\n  \"lib:stove-http\",\n  \"lib:stove-grpc\",\n  \"lib:stove-kafka\",\n  \"lib:stove-couchbase\",\n  \"lib:stove-rdbms\",\n  \"lib:stove-postgres\",\n  \"lib:stove-mysql\",\n  \"lib:stove-mssql\",\n  \"lib:stove-elasticsearch\",\n  \"lib:stove-redis\",\n  \"lib:stove-mongodb\",\n  \"lib:stove-cassandra\",\n  \"lib:stove-dashboard-api\",\n  \"lib:stove-dashboard\"\n)\ninclude(\n  \"test-extensions:stove-extensions-kotest\",\n  \"test-extensions:stove-extensions-junit\"\n)\ninclude(\n  \"starters:container:stove-container\",\n  \"starters:ktor:stove-ktor\",\n  \"starters:ktor:tests:ktor-test-fixtures\",\n  \"starters:ktor:tests:ktor-koin-tests\",\n  \"starters:ktor:tests:ktor-di-tests\",\n  \"starters:quarkus:stove-quarkus\",\n  \"starters:spring:stove-spring\",\n  \"starters:spring:stove-spring-kafka\",\n  \"starters:spring:tests:spring-test-fixtures\",\n  \"starters:spring:tests:spring-2x-tests\",\n  \"starters:spring:tests:spring-2x-kafka-tests\",\n  \"starters:spring:tests:spring-3x-tests\",\n  \"starters:spring:tests:spring-3x-kafka-tests\",\n  \"starters:spring:tests:spring-4x-tests\",\n  \"starters:spring:tests:spring-4x-kafka-tests\",\n  \"starters:micronaut:stove-micronaut\",\n  \"starters:process:stove-process\"\n)\ninclude(\n  \"examples:spring-example\",\n  \"examples:spring-standalone-example\",\n  \"examples:spring-4x-example\",\n  \"examples:ktor-example\",\n  \"examples:quarkus-example\",\n  \"examples:spring-streams-example\",\n  \"examples:micronaut-example\"\n)\ninclude(\n  \"plugins:stove-tracing-gradle-plugin\"\n)\n\ndependencyResolutionManagement {\n  repositories {\n    mavenCentral()\n    maven {\n      url = uri(\"https://packages.confluent.io/maven/\")\n    }\n  }\n}\nplugins {\n  id(\"org.danilopianini.gradle-pre-commit-git-hooks\").version(\"2.1.16\")\n}\ngitHooks {\n  preCommit {\n    from(rootDir.resolve(\"pre-commit.sh\"))\n  }\n  createHooks(overwriteExisting = true)\n}\n"
  },
  {
    "path": "starters/container/stove-container/api/stove-container.api",
    "content": "public final class com/trendyol/stove/container/ContainerDslKt {\n\tpublic static final fun containerApp-tBQrr_I (Lcom/trendyol/stove/system/Stove;Ljava/lang/String;Lcom/trendyol/stove/container/ContainerTarget;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/system/application/EnvProvider;Lcom/trendyol/stove/system/application/ArgsProvider;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;J)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n\tpublic static synthetic fun containerApp-tBQrr_I$default (Lcom/trendyol/stove/system/Stove;Ljava/lang/String;Lcom/trendyol/stove/container/ContainerTarget;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/system/application/EnvProvider;Lcom/trendyol/stove/system/application/ArgsProvider;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;JILjava/lang/Object;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n}\n\npublic abstract interface class com/trendyol/stove/container/ContainerTarget {\n\tpublic abstract fun getReadiness ()Lcom/trendyol/stove/system/ReadinessStrategy;\n}\n\npublic final class com/trendyol/stove/container/ContainerTarget$Server : com/trendyol/stove/container/ContainerTarget {\n\tpublic fun <init> (IILjava/lang/String;ZLcom/trendyol/stove/system/ReadinessStrategy;)V\n\tpublic synthetic fun <init> (IILjava/lang/String;ZLcom/trendyol/stove/system/ReadinessStrategy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()I\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Z\n\tpublic final fun component5 ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic final fun copy (IILjava/lang/String;ZLcom/trendyol/stove/system/ReadinessStrategy;)Lcom/trendyol/stove/container/ContainerTarget$Server;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/container/ContainerTarget$Server;IILjava/lang/String;ZLcom/trendyol/stove/system/ReadinessStrategy;ILjava/lang/Object;)Lcom/trendyol/stove/container/ContainerTarget$Server;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBindHostPort ()Z\n\tpublic final fun getHostPort ()I\n\tpublic final fun getInternalPort ()I\n\tpublic final fun getPortEnvVar ()Ljava/lang/String;\n\tpublic fun getReadiness ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/container/ContainerTarget$Worker : com/trendyol/stove/container/ContainerTarget {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lcom/trendyol/stove/system/ReadinessStrategy;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/ReadinessStrategy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic final fun copy (Lcom/trendyol/stove/system/ReadinessStrategy;)Lcom/trendyol/stove/container/ContainerTarget$Worker;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/container/ContainerTarget$Worker;Lcom/trendyol/stove/system/ReadinessStrategy;ILjava/lang/Object;)Lcom/trendyol/stove/container/ContainerTarget$Worker;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getReadiness ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\n"
  },
  {
    "path": "starters/container/stove-container/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n}\n"
  },
  {
    "path": "starters/container/stove-container/src/main/kotlin/com/trendyol/stove/container/ContainerApplicationUnderTest.kt",
    "content": "package com.trendyol.stove.container\n\nimport arrow.core.None\nimport arrow.core.Option\nimport arrow.core.Some\nimport com.github.dockerjava.api.model.ExposedPort\nimport com.github.dockerjava.api.model.HostConfig\nimport com.github.dockerjava.api.model.PortBinding\nimport com.github.dockerjava.api.model.Ports\nimport com.trendyol.stove.containers.DEFAULT_REGISTRY\nimport com.trendyol.stove.containers.withProvidedRegistry\nimport com.trendyol.stove.system.ReadinessChecker\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.application.ArgsProvider\nimport com.trendyol.stove.system.application.EnvProvider\nimport com.trendyol.stove.system.application.toConfigurationMap\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.withTimeoutOrNull\nimport org.slf4j.LoggerFactory\nimport org.testcontainers.containers.GenericContainer\nimport org.testcontainers.containers.output.Slf4jLogConsumer\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\ninternal typealias ContainerFactory = () -> GenericContainer<*>\ninternal typealias LaunchConfigurationObserver = (List<String>, Map<String, String>) -> Unit\n\ninternal class ContainerApplicationUnderTest(\n  private val image: String,\n  private val target: ContainerTarget,\n  private val command: List<String> = emptyList(),\n  private val envProvider: EnvProvider = EnvProvider.empty(),\n  private val argsProvider: ArgsProvider = ArgsProvider.empty(),\n  private val beforeStarted: suspend (configurations: Map<String, String>) -> Unit = {},\n  private val configureContainer: GenericContainer<*>.() -> Unit = {},\n  private val gracefulShutdownTimeout: Duration = 5.seconds,\n  private val containerFactory: ContainerFactory,\n  private val launchConfigurationObserver: LaunchConfigurationObserver\n) : ApplicationUnderTest<ContainerApplicationContext> {\n  constructor(\n    image: String,\n    target: ContainerTarget,\n    registry: String = DEFAULT_REGISTRY,\n    compatibleSubstitute: String? = null,\n    command: List<String> = emptyList(),\n    envProvider: EnvProvider = EnvProvider.empty(),\n    argsProvider: ArgsProvider = ArgsProvider.empty(),\n    beforeStarted: suspend (configurations: Map<String, String>) -> Unit = {},\n    configureContainer: GenericContainer<*>.() -> Unit = {},\n    gracefulShutdownTimeout: Duration = 5.seconds\n  ) : this(\n    image = image,\n    target = target,\n    command = command,\n    envProvider = envProvider,\n    argsProvider = argsProvider,\n    beforeStarted = beforeStarted,\n    configureContainer = configureContainer,\n    gracefulShutdownTimeout = gracefulShutdownTimeout,\n    containerFactory = {\n      defaultContainerFactory(\n        image = image,\n        registry = registry,\n        compatibleSubstitute = compatibleSubstitute\n      )\n    },\n    launchConfigurationObserver = { _, _ -> }\n  )\n\n  private val logger = LoggerFactory.getLogger(javaClass)\n  private var runningContainer: Option<GenericContainer<*>> = None\n\n  override suspend fun start(configurations: List<String>): ContainerApplicationContext {\n    val configurationMap = configurations.toConfigurationMap()\n    val commandArgs = argsProvider.provide(configurationMap)\n    val fullCommand = command + commandArgs\n    val envVars = resolveEnv(configurationMap)\n    launchConfigurationObserver(fullCommand, envVars)\n\n    beforeStarted(configurationMap)\n\n    val container = containerFactory()\n    applyContainerConfiguration(container = container, fullCommand = fullCommand, envVars = envVars)\n    logger.info(\"Starting container image {} with {} env vars\", image, envVars.size)\n\n    runCatching {\n      withContext(Dispatchers.IO) {\n        container.start()\n      }\n    }.fold(\n      onSuccess = {},\n      onFailure = { throwable ->\n        val containerLogs = runCatching { container.logs }.getOrElse { \"<container logs unavailable>\" }\n        throw IllegalStateException(\n          \"Failed to start container application `$image`. Logs:\\n$containerLogs\",\n          throwable\n        )\n      }\n    )\n    withContext(Dispatchers.IO) {\n      runCatching {\n        container.followOutput(Slf4jLogConsumer(logger).withPrefix(image))\n      }.onFailure {\n        logger.debug(\"Container log streaming could not be attached: {}\", it.message)\n      }\n    }\n\n    runningContainer = Some(container)\n    try {\n      ReadinessChecker.check(target.readiness)\n      logger.info(\"Container application is ready\")\n    } catch (t: IllegalStateException) {\n      stop()\n      throw t\n    }\n\n    return ContainerApplicationContext(container)\n  }\n\n  override suspend fun stop() {\n    when (val activeContainer = runningContainer) {\n      is Some -> {\n        val container = activeContainer.value\n        val gracefullyStopped = withContext(Dispatchers.IO) {\n          withTimeoutOrNull(gracefulShutdownTimeout) {\n            container.stop()\n          } != null\n        }\n\n        if (!gracefullyStopped) {\n          logger.warn(\"Container did not stop in time, force-closing\")\n          withContext(Dispatchers.IO) { container.close() }\n        }\n      }\n\n      None -> Unit\n    }\n    runningContainer = None\n  }\n\n  private fun resolveEnv(configurationMap: Map<String, String>): Map<String, String> {\n    val mappedEnv = envProvider.provide(configurationMap)\n    return when (val target = target) {\n      is ContainerTarget.Server -> mappedEnv + (target.portEnvVar to target.internalPort.toString())\n      is ContainerTarget.Worker -> mappedEnv\n    }\n  }\n\n  private fun applyContainerConfiguration(\n    container: GenericContainer<*>,\n    fullCommand: List<String>,\n    envVars: Map<String, String>\n  ) {\n    container\n      .withEnv(envVars)\n    configureTarget(container)\n    if (fullCommand.isNotEmpty()) {\n      container.withCommand(*fullCommand.toTypedArray())\n    }\n    configureContainer(container)\n  }\n\n  private fun configureTarget(container: GenericContainer<*>) {\n    when (val target = target) {\n      is ContainerTarget.Server -> {\n        if (target.bindHostPort) {\n          container.withExposedPorts(target.internalPort)\n          container.withCreateContainerCmdModifier { command ->\n            val hostConfig = command.hostConfig ?: HostConfig.newHostConfig()\n            command.withHostConfig(\n              hostConfig.withPortBindings(\n                PortBinding(\n                  Ports.Binding.bindPort(target.hostPort),\n                  ExposedPort(target.internalPort)\n                )\n              )\n            )\n          }\n        }\n      }\n\n      is ContainerTarget.Worker -> Unit\n    }\n  }\n\n  companion object {\n    private fun defaultContainerFactory(\n      image: String,\n      registry: String,\n      compatibleSubstitute: String?\n    ): GenericContainer<*> =\n      withProvidedRegistry(\n        imageName = image,\n        registry = registry,\n        compatibleSubstitute = compatibleSubstitute\n      ) { imageName ->\n        GenericContainer(imageName)\n      }\n  }\n}\n"
  },
  {
    "path": "starters/container/stove-container/src/main/kotlin/com/trendyol/stove/container/ContainerDsl.kt",
    "content": "package com.trendyol.stove.container\n\nimport com.trendyol.stove.containers.DEFAULT_REGISTRY\nimport com.trendyol.stove.system.WithDsl\nimport com.trendyol.stove.system.abstractions.ReadyStove\nimport com.trendyol.stove.system.application.ArgsProvider\nimport com.trendyol.stove.system.application.EnvProvider\nimport org.testcontainers.containers.GenericContainer\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\nfun WithDsl.containerApp(\n  image: String,\n  target: ContainerTarget,\n  registry: String = DEFAULT_REGISTRY,\n  compatibleSubstitute: String? = null,\n  command: List<String> = emptyList(),\n  envProvider: EnvProvider = EnvProvider.empty(),\n  argsProvider: ArgsProvider = ArgsProvider.empty(),\n  beforeStarted: suspend (configurations: Map<String, String>) -> Unit = {},\n  configureContainer: GenericContainer<*>.() -> Unit = {},\n  gracefulShutdownTimeout: Duration = 5.seconds\n): ReadyStove {\n  stove.applicationUnderTest(\n    ContainerApplicationUnderTest(\n      image = image,\n      target = target,\n      registry = registry,\n      compatibleSubstitute = compatibleSubstitute,\n      command = command,\n      envProvider = envProvider,\n      argsProvider = argsProvider,\n      beforeStarted = beforeStarted,\n      configureContainer = configureContainer,\n      gracefulShutdownTimeout = gracefulShutdownTimeout\n    )\n  )\n  return stove\n}\n"
  },
  {
    "path": "starters/container/stove-container/src/main/kotlin/com/trendyol/stove/container/ContainerTarget.kt",
    "content": "package com.trendyol.stove.container\n\nimport com.trendyol.stove.system.ReadinessStrategy\nimport org.testcontainers.containers.GenericContainer\n\nsealed interface ContainerTarget {\n  val readiness: ReadinessStrategy\n\n  data class Server(\n    val hostPort: Int,\n    val internalPort: Int = hostPort,\n    val portEnvVar: String = \"PORT\",\n    val bindHostPort: Boolean = true,\n    override val readiness: ReadinessStrategy =\n      ReadinessStrategy.HttpGet(url = \"http://localhost:$hostPort/health\")\n  ) : ContainerTarget\n\n  data class Worker(\n    override val readiness: ReadinessStrategy = ReadinessStrategy.FixedDelay()\n  ) : ContainerTarget\n}\n\ninternal data class ContainerApplicationContext(\n  val container: GenericContainer<*>\n)\n"
  },
  {
    "path": "starters/container/stove-container/src/test/kotlin/com/trendyol/stove/container/ContainerApplicationUnderTestTest.kt",
    "content": "package com.trendyol.stove.container\n\nimport com.trendyol.stove.system.ReadinessStrategy\nimport com.trendyol.stove.system.application.argsMapper\nimport com.trendyol.stove.system.application.envMapper\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.runBlocking\nimport org.testcontainers.containers.GenericContainer\nimport org.testcontainers.utility.DockerImageName\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass ContainerApplicationUnderTestTest :\n  FunSpec({\n    test(\"builds command and env map including server port\") {\n      val fakeContainer = FakeContainer()\n      var capturedCommand: List<String> = emptyList()\n      var capturedEnv: Map<String, String> = emptyMap()\n\n      val aut = ContainerApplicationUnderTest(\n        image = \"busybox:latest\",\n        command = listOf(\"worker\"),\n        target = ContainerTarget.Server(\n          hostPort = 18090,\n          internalPort = 8090,\n          portEnvVar = \"APP_PORT\",\n          readiness = ReadinessStrategy.FixedDelay(1.milliseconds)\n        ),\n        envProvider = envMapper {\n          \"database.host\" to \"DB_HOST\"\n        },\n        argsProvider = argsMapper(prefix = \"--\", separator = \"=\") {\n          \"database.port\" to \"db-port\"\n        },\n        containerFactory = { fakeContainer },\n        launchConfigurationObserver = { fullCommand, envVars ->\n          capturedCommand = fullCommand\n          capturedEnv = envVars\n        }\n      )\n\n      runBlocking {\n        aut.start(listOf(\"database.host=localhost\", \"database.port=5432\"))\n        aut.stop()\n      }\n\n      capturedCommand shouldBe listOf(\"worker\", \"--db-port=5432\")\n      capturedEnv shouldBe mapOf(\n        \"DB_HOST\" to \"localhost\",\n        \"APP_PORT\" to \"8090\"\n      )\n    }\n\n    test(\"invokes container customizer before start\") {\n      val fakeContainer = FakeContainer()\n      var customizerCalled = false\n\n      val aut = ContainerApplicationUnderTest(\n        image = \"busybox:latest\",\n        target = ContainerTarget.Worker(\n          readiness = ReadinessStrategy.FixedDelay(1.milliseconds)\n        ),\n        configureContainer = {\n          customizerCalled = true\n        },\n        containerFactory = { fakeContainer },\n        launchConfigurationObserver = { _, _ -> }\n      )\n\n      runBlocking {\n        aut.start(emptyList())\n        aut.stop()\n      }\n\n      customizerCalled shouldBe true\n      fakeContainer.started shouldBe true\n      fakeContainer.stopped shouldBe true\n    }\n\n    test(\"stop is a no-op when container was never started\") {\n      val aut = ContainerApplicationUnderTest(\n        image = \"busybox:latest\",\n        target = ContainerTarget.Worker(),\n        containerFactory = { FakeContainer() },\n        launchConfigurationObserver = { _, _ -> }\n      )\n\n      runBlocking {\n        aut.stop()\n      }\n    }\n  })\n\nprivate class FakeContainer : GenericContainer<FakeContainer>(DockerImageName.parse(\"busybox:latest\")) {\n  var started: Boolean = false\n  var stopped: Boolean = false\n\n  override fun start() {\n    started = true\n  }\n\n  override fun stop() {\n    stopped = true\n  }\n}\n"
  },
  {
    "path": "starters/container/stove-container/src/test/kotlin/com/trendyol/stove/container/ContainerDslTest.kt",
    "content": "package com.trendyol.stove.container\n\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.WithDsl\nimport com.trendyol.stove.system.abstractions.ReadyStove\nimport com.trendyol.stove.system.application.ArgsProvider\nimport com.trendyol.stove.system.application.EnvProvider\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport kotlin.time.Duration.Companion.seconds\n\nclass ContainerDslTest :\n  FunSpec({\n    test(\"containerApp accepts option elements as parameters\") {\n      val stove = Stove()\n\n      val readyStove: ReadyStove = WithDsl(stove).containerApp(\n        image = \"busybox:latest\",\n        target = ContainerTarget.Worker(),\n        command = listOf(\"echo\", \"ready\"),\n        envProvider = EnvProvider.empty(),\n        argsProvider = ArgsProvider.empty(),\n        gracefulShutdownTimeout = 1.seconds\n      )\n\n      readyStove shouldBe stove\n    }\n  })\n"
  },
  {
    "path": "starters/container/stove-container/src/test/kotlin/com/trendyol/stove/container/ContainerTargetTest.kt",
    "content": "package com.trendyol.stove.container\n\nimport com.trendyol.stove.system.ReadinessStrategy\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass ContainerTargetTest :\n  FunSpec({\n    test(\"server target defaults use host port and health readiness\") {\n      val target = ContainerTarget.Server(hostPort = 8080)\n\n      target.internalPort shouldBe 8080\n      target.portEnvVar shouldBe \"PORT\"\n      target.bindHostPort shouldBe true\n      (target.readiness is ReadinessStrategy.HttpGet) shouldBe true\n      (target.readiness as ReadinessStrategy.HttpGet).url shouldBe \"http://localhost:8080/health\"\n    }\n\n    test(\"worker target defaults to fixed delay readiness\") {\n      val target = ContainerTarget.Worker()\n\n      (target.readiness is ReadinessStrategy.FixedDelay) shouldBe true\n    }\n  })\n"
  },
  {
    "path": "starters/ktor/stove-ktor/api/stove-ktor.api",
    "content": "public final class com/trendyol/stove/ktor/DependencyResolvers {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/ktor/DependencyResolvers;\n\tpublic final fun autoDetect ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getKoin ()Lkotlin/jvm/functions/Function2;\n\tpublic final fun getKtorDi ()Lkotlin/jvm/functions/Function2;\n}\n\npublic final class com/trendyol/stove/ktor/KtorApplicationUnderTest : com/trendyol/stove/system/abstractions/ApplicationUnderTest {\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;)V\n\tpublic fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/ktor/KtorApplicationUnderTestKt {\n\tpublic static final fun ktor-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n\tpublic static synthetic fun ktor-SscbJ7Y$default (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n}\n\npublic final class com/trendyol/stove/ktor/KtorBridgeSystem : com/trendyol/stove/system/BridgeSystem, com/trendyol/stove/system/abstractions/AfterRunAwareWithContext, com/trendyol/stove/system/abstractions/PluggedSystem {\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;)V\n\tpublic fun get (Lkotlin/reflect/KClass;)Ljava/lang/Object;\n\tpublic fun getByType (Lkotlin/reflect/KType;)Ljava/lang/Object;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/ktor/KtorBridgeSystemKt {\n\tpublic static final fun bridge-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;)Lcom/trendyol/stove/system/Stove;\n\tpublic static synthetic fun bridge-ypJx7X8$default (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/ktor/KtorDiCheck {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/ktor/KtorDiCheck;\n\tpublic final fun isKoinAvailable ()Z\n\tpublic final fun isKtorDiAvailable ()Z\n}\n\n"
  },
  {
    "path": "starters/ktor/stove-ktor/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  implementation(libs.ktor.server.host.common)\n\n  // Both DI systems as compileOnly - users bring their preferred DI at runtime\n  compileOnly(libs.koin.ktor)\n  compileOnly(libs.ktor.server.di)\n}\n"
  },
  {
    "path": "starters/ktor/stove-ktor/src/main/kotlin/com/trendyol/stove/ktor/DependencyResolvers.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport io.ktor.server.application.*\nimport io.ktor.server.plugins.di.*\nimport io.ktor.util.reflect.*\nimport org.koin.ktor.ext.getKoin\nimport kotlin.reflect.*\n\n/**\n * Type alias for a dependency resolver function.\n * Takes an Application and a KType, returns the resolved dependency.\n * KType preserves generic type information (e.g., List<PaymentService>).\n */\ntypealias DependencyResolver = (Application, KType) -> Any\n\n/**\n * Default resolver implementations for supported DI frameworks.\n */\nobject DependencyResolvers {\n  /**\n   * Resolver for Koin DI framework.\n   */\n  val koin: DependencyResolver = { application, type ->\n    val klass = type.classifier as? KClass<*>\n      ?: error(\"Cannot resolve type: $type\")\n    application.getKoin().get(klass)\n  }\n\n  /**\n   * Resolver for Ktor-DI framework.\n   * Uses full KType to preserve generic type information.\n   */\n  val ktorDi: DependencyResolver = { application, type ->\n    require(application.attributes.contains(DependencyRegistryKey)) {\n      \"Ktor-DI not installed in application. Make sure to install(DI) { ... } in your application.\"\n    }\n    val klass = type.classifier as? KClass<*>\n      ?: error(\"Cannot resolve type: $type\")\n    val typeInfo = TypeInfo(klass, type)\n    application.dependencies.getBlocking(DependencyKey(type = typeInfo))\n  }\n\n  /**\n   * Auto-detects and returns the appropriate resolver based on available DI frameworks.\n   * Prefers Ktor-DI over Koin if both are active in runtime.\n   * Detection is deferred to runtime to ensure Ktor application plugins are fully initialized.\n   */\n  fun autoDetect(): DependencyResolver = { application, type ->\n    val resolver = when {\n      isKtorDiActive(application) -> ktorDi\n      isKoinActive(application) -> koin\n      else -> error(buildNoActiveDiFrameworkMessage())\n    }\n    resolver(application, type)\n  }\n\n  /**\n   * Uses reflection-based availability check first, then typed runtime check.\n   * This avoids hard-loading optional Ktor-DI classes in Koin-only apps.\n   */\n  private fun isKtorDiActive(application: Application): Boolean {\n    if (!KtorDiCheck.isKtorDiAvailable()) return false\n    return runCatching { application.attributes.contains(DependencyRegistryKey) }.getOrDefault(false)\n  }\n\n  /**\n   * Uses reflection-based availability check first, then typed runtime check.\n   * This avoids hard-loading optional Koin classes when Koin is not present.\n   */\n  private fun isKoinActive(application: Application): Boolean {\n    if (!KtorDiCheck.isKoinAvailable()) return false\n    return runCatching {\n      application.getKoin()\n      true\n    }.getOrDefault(false)\n  }\n\n  private fun buildNoActiveDiFrameworkMessage(): String {\n    val koinOnClasspath = KtorDiCheck.isKoinAvailable()\n    val ktorDiOnClasspath = KtorDiCheck.isKtorDiAvailable()\n\n    if (!koinOnClasspath && !ktorDiOnClasspath) {\n      return \"No supported DI framework found. \" +\n        \"Add either Koin (io.insert-koin:koin-ktor) or Ktor-DI (io.ktor:ktor-server-di) to your classpath, \" +\n        \"or provide a custom resolver via bridge(resolver = { app, type -> ... })\"\n    }\n\n    return \"No active DI framework detected in the Ktor application runtime. \" +\n      \"Classpath availability: Koin=$koinOnClasspath, Ktor-DI=$ktorDiOnClasspath. \" +\n      \"Install Koin via install(Koin) { ... } and/or install Ktor-DI via dependencies { ... }, \" +\n      \"or provide a custom resolver via bridge(resolver = { app, type -> ... })\"\n  }\n}\n"
  },
  {
    "path": "starters/ktor/stove-ktor/src/main/kotlin/com/trendyol/stove/ktor/KtorApplicationUnderTest.kt",
    "content": "@file:Suppress(\"UNCHECKED_CAST\")\n\npackage com.trendyol.stove.ktor\n\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport io.ktor.server.application.*\nimport io.ktor.utils.io.InternalAPI\nimport kotlinx.coroutines.*\n\n/**\n *  Definition for Application Under Test for Ktor enabled application\n */\ninternal fun Stove.systemUnderTest(\n  runner: Runner<Application>,\n  withParameters: List<String> = listOf()\n): ReadyStove = applicationUnderTest(KtorApplicationUnderTest(this, runner, withParameters))\n\nfun WithDsl.ktor(\n  runner: Runner<Application>,\n  withParameters: List<String> = listOf()\n): ReadyStove = this.stove.systemUnderTest(runner, withParameters)\n\n@StoveDsl\nclass KtorApplicationUnderTest(\n  private val stove: Stove,\n  private val runner: Runner<Application>,\n  private val parameters: List<String>\n) : ApplicationUnderTest<Application> {\n  private lateinit var application: Application\n\n  override suspend fun start(configurations: List<String>): Application = coroutineScope {\n    val allConfigurations = (configurations + defaultConfigurations() + parameters)\n      .map { \"--$it\" }\n      .distinct()\n      .toTypedArray()\n    application = runner(allConfigurations)\n    stove.systemsOf<AfterRunAwareWithContext<Application>>()\n      .map { async { it.afterRun(application) } }\n      .awaitAll()\n\n    application\n  }\n\n  @OptIn(InternalAPI::class)\n  override suspend fun stop(): Unit = application.disposeAndJoin()\n\n  private fun defaultConfigurations(): Array<String> = arrayOf(\"test-system=true\")\n}\n"
  },
  {
    "path": "starters/ktor/stove-ktor/src/main/kotlin/com/trendyol/stove/ktor/KtorBridgeSystem.kt",
    "content": "@file:Suppress(\"UNCHECKED_CAST\")\n\npackage com.trendyol.stove.ktor\n\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport io.ktor.server.application.*\nimport kotlin.reflect.*\nimport kotlin.reflect.full.starProjectedType\n\n/**\n * A system that provides a bridge between the test system and the application context.\n * Supports Koin, Ktor-DI, or a custom resolver for dependency resolution.\n *\n * @property stove the test system to bridge.\n * @property resolver the dependency resolver function to use.\n */\n@StoveDsl\nclass KtorBridgeSystem(\n  override val stove: Stove,\n  private val resolver: DependencyResolver\n) : BridgeSystem<Application>(stove),\n  PluggedSystem,\n  AfterRunAwareWithContext<Application> {\n  /**\n   * Resolves a dependency by KClass (fallback, loses generic info).\n   */\n  override fun <D : Any> get(klass: KClass<D>): D = resolver(ctx, klass.starProjectedType) as D\n\n  /**\n   * Resolves a dependency by KType, preserving generic type information.\n   * This allows resolving types like List<PaymentService> correctly.\n   */\n  override fun <D : Any> getByType(type: KType): D = resolver(ctx, type) as D\n}\n\n/**\n * Registers the Ktor bridge system with automatic DI detection or a custom resolver.\n * Supports Koin and Ktor-DI out of the box.\n *\n * Example usage with auto-detect:\n * ```kotlin\n * bridge() // Auto-detects Koin or Ktor-DI\n * ```\n *\n * Example usage with custom resolver:\n * ```kotlin\n * bridge { application, klass ->\n *   application.myCustomDi.resolve(klass)\n * }\n * ```\n *\n * @param resolver a function that takes an Application and KClass and returns the resolved dependency.\n *                 Defaults to auto-detecting Koin or Ktor-DI.\n * @throws IllegalStateException if no DI framework is available and no custom resolver is provided.\n */\nfun WithDsl.bridge(\n  resolver: DependencyResolver = DependencyResolvers.autoDetect()\n): Stove = this.stove.withBridgeSystem(KtorBridgeSystem(this.stove, resolver))\n"
  },
  {
    "path": "starters/ktor/stove-ktor/src/main/kotlin/com/trendyol/stove/ktor/KtorDiCheck.kt",
    "content": "@file:Suppress(\"TooGenericExceptionCaught\", \"SwallowedException\")\n\npackage com.trendyol.stove.ktor\n\n/**\n * Checks which DI system is available on the classpath.\n */\nobject KtorDiCheck {\n  /**\n   * Returns true if Koin is available on the classpath.\n   */\n  fun isKoinAvailable(): Boolean = try {\n    Class.forName(\"org.koin.ktor.ext.ApplicationExtKt\")\n    true\n  } catch (_: ClassNotFoundException) {\n    false\n  }\n\n  /**\n   * Returns true if Ktor-DI is available on the classpath.\n   */\n  fun isKtorDiAvailable(): Boolean = try {\n    Class.forName(\"io.ktor.server.plugins.di.DependencyInjectionConfig\")\n    true\n  } catch (_: ClassNotFoundException) {\n    false\n  }\n}\n"
  },
  {
    "path": "starters/ktor/stove-ktor/src/test/kotlin/com/trendyol/stove/ktor/DependencyResolversLinkageTest.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport io.kotest.assertions.throwables.shouldNotThrowAny\nimport io.kotest.core.spec.style.FunSpec\n\nclass DependencyResolversLinkageTest :\n  FunSpec({\n    test(\"autoDetect should load without optional DI libraries on classpath\") {\n      shouldNotThrowAny {\n        DependencyResolvers.autoDetect()\n      }\n    }\n  })\n"
  },
  {
    "path": "starters/ktor/stove-ktor/src/test/kotlin/com/trendyol/stove/ktor/KtorDiCheckTest.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass KtorDiCheckTest :\n  FunSpec({\n\n    test(\"isKoinAvailable should return false when Koin is not on classpath\") {\n      // Koin is compileOnly, so it should not be on the test classpath\n      KtorDiCheck.isKoinAvailable() shouldBe false\n    }\n\n    test(\"isKtorDiAvailable should return false when Ktor-DI is not on classpath\") {\n      // Ktor-DI is compileOnly, so it should not be on the test classpath\n      KtorDiCheck.isKtorDiAvailable() shouldBe false\n    }\n\n    test(\"neither DI framework should be available in test classpath\") {\n      // Since both Koin and Ktor-DI are compileOnly dependencies,\n      // neither should be detected in the test runtime classpath\n      val koinAvailable = KtorDiCheck.isKoinAvailable()\n      val ktorDiAvailable = KtorDiCheck.isKtorDiAvailable()\n\n      koinAvailable shouldBe false\n      ktorDiAvailable shouldBe false\n    }\n  })\n"
  },
  {
    "path": "starters/ktor/tests/ktor-di-tests/api/ktor-di-tests.api",
    "content": ""
  },
  {
    "path": "starters/ktor/tests/ktor-di-tests/build.gradle.kts",
    "content": "dependencies {\n  api(projects.starters.ktor.stoveKtor)\n  implementation(libs.ktor.server.netty)\n  implementation(libs.ktor.server.di)\n  implementation(libs.koin.ktor)\n  testImplementation(testFixtures(projects.starters.ktor.tests.ktorTestFixtures))\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.slf4j.simple)\n}\n\ntasks.test.configure {\n  systemProperty(\"kotest.framework.config.fqn\", \"com.trendyol.stove.ktor.KtorDiStove\")\n}\n"
  },
  {
    "path": "starters/ktor/tests/ktor-di-tests/src/test/kotlin/com/trendyol/stove/ktor/AutoDetectRuntimeStateTest.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldContainExactlyInAnyOrder\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport io.ktor.server.plugins.di.*\nimport org.koin.core.context.stopKoin\nimport kotlin.reflect.typeOf\n\nclass AutoDetectRuntimeStateTest :\n  FunSpec({\n    test(\"autoDetect prefers Ktor-DI when both Koin and Ktor-DI are active\") {\n      val application = BothActiveDiTestApp.run(emptyArray())\n      application.attributes.contains(DependencyRegistryKey) shouldBe true\n\n      val resolver = DependencyResolvers.autoDetect()\n\n      val resolvedService = resolver(application, typeOf<ExampleService>()) as ExampleService\n      resolvedService.whatIsTheTime() shouldBe GetUtcNow.frozenTime\n\n      val resolvedConfig = resolver(application, typeOf<TestConfig>()) as TestConfig\n      resolvedConfig.message shouldBe \"Hello from Stove!\"\n\n      val resolvedPaymentServices = resolver(application, typeOf<List<PaymentService>>())\n      (resolvedPaymentServices as List<*>)\n        .filterIsInstance<PaymentService>()\n        .map { it.providerName } shouldContainExactlyInAnyOrder listOf(\"Stripe\", \"PayPal\", \"Square\")\n    }\n\n    test(\"autoDetect throws clear error when no runtime DI is active\") {\n      runCatching { stopKoin() }\n      val application = NoDiTestApp.run(emptyArray())\n      application.attributes.contains(DependencyRegistryKey) shouldBe false\n\n      val resolver = DependencyResolvers.autoDetect()\n      val error = shouldThrow<IllegalStateException> {\n        resolver(application, typeOf<ExampleService>())\n      }\n\n      error.message shouldContain \"No active DI framework detected\"\n      error.message shouldContain \"install(Koin)\"\n      error.message shouldContain \"dependencies { ... }\"\n    }\n  })\n"
  },
  {
    "path": "starters/ktor/tests/ktor-di-tests/src/test/kotlin/com/trendyol/stove/ktor/StoveConfig.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\n\nclass KtorDiStove : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        bridge() // Auto-detects Ktor-DI\n        ktor(\n          runner = { params ->\n            KtorDiTestApp.run(params)\n          }\n        )\n      }.run()\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n\nclass KtorDiBridgeSystemTests : BridgeSystemTests(KtorDiStove())\n"
  },
  {
    "path": "starters/ktor/tests/ktor-di-tests/src/test/kotlin/com/trendyol/stove/ktor/app.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport io.ktor.server.application.*\nimport io.ktor.server.engine.*\nimport io.ktor.server.netty.*\nimport io.ktor.server.plugins.di.*\nimport org.junit.platform.commons.logging.LoggerFactory\nimport org.koin.dsl.module\nimport org.koin.ktor.plugin.Koin\nimport java.net.ServerSocket\nimport java.time.Instant\n\n/**\n * Test Ktor application using Ktor-DI for dependency injection.\n */\nobject KtorDiTestApp {\n  private val logger = LoggerFactory.getLogger(KtorDiTestApp::class.java)\n\n  fun run(args: Array<String>): Application {\n    logger.info { \"Starting Ktor-DI test application with args: ${args.joinToString(\" \")}\" }\n    val port = findAvailablePort()\n    val applicationEngine = embeddedServer(Netty, port = port, host = \"localhost\") {\n      dependencies {\n        provide<GetUtcNow> { SystemTimeGetUtcNow() }\n        provide<ExampleService> { ExampleService(resolve()) }\n        provide<TestConfig> { TestConfig() }\n\n        // Multiple payment service implementations as List<PaymentService>\n        provide<List<PaymentService>> {\n          listOf(\n            StripePaymentService(),\n            PayPalPaymentService(),\n            SquarePaymentService()\n          )\n        }\n      }\n    }\n    applicationEngine.start(wait = false)\n    return applicationEngine.application\n  }\n\n  private fun findAvailablePort(): Int = ServerSocket(0).use { it.localPort }\n}\n\n/**\n * Test app with both Ktor-DI and Koin active.\n * Koin intentionally provides conflicting values to verify Ktor-DI precedence.\n */\nobject BothActiveDiTestApp {\n  private val logger = LoggerFactory.getLogger(BothActiveDiTestApp::class.java)\n\n  fun run(args: Array<String>): Application {\n    logger.info { \"Starting both-active DI test application with args: ${args.joinToString(\" \")}\" }\n    val port = findAvailablePort()\n    val applicationEngine = embeddedServer(Netty, port = port, host = \"localhost\") {\n      install(Koin) {\n        modules(\n          module {\n            single<GetUtcNow> { GetUtcNow { Instant.parse(\"2030-01-01T00:00:00Z\") } }\n            single { ExampleService(get()) }\n            single { TestConfig(message = \"from-koin\") }\n            single<List<PaymentService>> { listOf(StripePaymentService()) }\n          }\n        )\n      }\n      dependencies {\n        provide<GetUtcNow> { SystemTimeGetUtcNow() }\n        provide<ExampleService> { ExampleService(resolve()) }\n        provide<TestConfig> { TestConfig() }\n\n        // Multiple payment service implementations as List<PaymentService>\n        provide<List<PaymentService>> {\n          listOf(\n            StripePaymentService(),\n            PayPalPaymentService(),\n            SquarePaymentService()\n          )\n        }\n      }\n    }\n    applicationEngine.start(wait = false)\n    return applicationEngine.application\n  }\n\n  private fun findAvailablePort(): Int = ServerSocket(0).use { it.localPort }\n}\n\n/**\n * Test app with no DI framework installed in runtime.\n */\nobject NoDiTestApp {\n  private val logger = LoggerFactory.getLogger(NoDiTestApp::class.java)\n\n  fun run(args: Array<String>): Application {\n    logger.info { \"Starting no-DI test application with args: ${args.joinToString(\" \")}\" }\n    val port = findAvailablePort()\n    val applicationEngine = embeddedServer(Netty, port = port, host = \"localhost\") {}\n    applicationEngine.start(wait = false)\n    return applicationEngine.application\n  }\n\n  private fun findAvailablePort(): Int = ServerSocket(0).use { it.localPort }\n}\n"
  },
  {
    "path": "starters/ktor/tests/ktor-di-tests/src/test/resources/simplelogger.properties",
    "content": "org.slf4j.simpleLogger.defaultLogLevel=info\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss.SSS\norg.slf4j.simpleLogger.showShortLogName=true\n\n"
  },
  {
    "path": "starters/ktor/tests/ktor-koin-tests/api/ktor-koin-tests.api",
    "content": ""
  },
  {
    "path": "starters/ktor/tests/ktor-koin-tests/build.gradle.kts",
    "content": "dependencies {\n  api(projects.starters.ktor.stoveKtor)\n  implementation(libs.ktor.server.netty)\n  implementation(libs.koin.ktor)\n  testImplementation(testFixtures(projects.starters.ktor.tests.ktorTestFixtures))\n}\n\ndependencies {\n  testImplementation(project(\":test-extensions:stove-extensions-kotest\"))\n  testImplementation(libs.slf4j.simple)\n}\n\ntasks.test.configure {\n  systemProperty(\"kotest.framework.config.fqn\", \"com.trendyol.stove.ktor.KoinStove\")\n}\n"
  },
  {
    "path": "starters/ktor/tests/ktor-koin-tests/src/test/kotlin/com/trendyol/stove/ktor/AutoDetectRuntimeSelectionTest.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.ktor.server.application.Application\nimport io.ktor.util.AttributeKey\nimport kotlin.reflect.typeOf\n\nclass AutoDetectRuntimeSelectionTest :\n  FunSpec({\n    test(\"autoDetect uses active Koin for a Koin-only app\") {\n      val application = KoinTestApp.run(emptyArray())\n      KtorDiCheck.isKoinAvailable() shouldBe true\n      KtorDiCheck.isKtorDiAvailable() shouldBe true\n      isKtorDiRegistryInstalled(application) shouldBe false\n\n      val resolver = DependencyResolvers.autoDetect()\n      val resolvedService = resolver(application, typeOf<ExampleService>()) as ExampleService\n\n      resolvedService.whatIsTheTime() shouldBe GetUtcNow.frozenTime\n    }\n  })\n\nprivate fun isKtorDiRegistryInstalled(application: Application): Boolean = runCatching {\n  val dependencyInjectionKt = Class.forName(\"io.ktor.server.plugins.di.DependencyInjectionKt\")\n  val key = dependencyInjectionKt.getMethod(\"getDependencyRegistryKey\").invoke(null) as AttributeKey<*>\n  application.attributes.contains(key)\n}.getOrDefault(false)\n"
  },
  {
    "path": "starters/ktor/tests/ktor-koin-tests/src/test/kotlin/com/trendyol/stove/ktor/StoveConfig.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\n\nclass KoinStove : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        bridge() // Auto-detects Koin\n        ktor(\n          runner = { params ->\n            KoinTestApp.run(params)\n          }\n        )\n      }.run()\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n\nclass KoinBridgeSystemTests : BridgeSystemTests(KoinStove())\n"
  },
  {
    "path": "starters/ktor/tests/ktor-koin-tests/src/test/kotlin/com/trendyol/stove/ktor/app.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport io.ktor.server.application.*\nimport io.ktor.server.engine.*\nimport io.ktor.server.netty.*\nimport org.junit.platform.commons.logging.LoggerFactory\nimport org.koin.dsl.module\nimport org.koin.ktor.plugin.Koin\nimport java.net.ServerSocket\n\n/**\n * Test Ktor application using Koin for dependency injection.\n */\nobject KoinTestApp {\n  private val logger = LoggerFactory.getLogger(KoinTestApp::class.java)\n\n  fun run(args: Array<String>): Application {\n    logger.info { \"Starting Koin test application with args: ${args.joinToString(\" \")}\" }\n    val port = findAvailablePort()\n    val applicationEngine = embeddedServer(Netty, port = port, host = \"localhost\") {\n      install(Koin) {\n        modules(\n          module {\n            single<GetUtcNow> { SystemTimeGetUtcNow() }\n            single { ExampleService(get()) }\n            single { TestConfig() }\n\n            // Multiple payment service implementations as List<PaymentService>\n            single<List<PaymentService>> {\n              listOf(\n                StripePaymentService(),\n                PayPalPaymentService(),\n                SquarePaymentService()\n              )\n            }\n          }\n        )\n      }\n    }\n    applicationEngine.start(wait = false)\n    return applicationEngine.application\n  }\n\n  private fun findAvailablePort(): Int = ServerSocket(0).use { it.localPort }\n}\n"
  },
  {
    "path": "starters/ktor/tests/ktor-koin-tests/src/test/resources/simplelogger.properties",
    "content": "org.slf4j.simpleLogger.defaultLogLevel=info\norg.slf4j.simpleLogger.showDateTime=true\norg.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss.SSS\norg.slf4j.simpleLogger.showShortLogName=true\n\n"
  },
  {
    "path": "starters/ktor/tests/ktor-test-fixtures/api/ktor-test-fixtures.api",
    "content": ""
  },
  {
    "path": "starters/ktor/tests/ktor-test-fixtures/build.gradle.kts",
    "content": "plugins {\n  `java-test-fixtures`\n}\n\ndependencies {\n  testFixturesApi(projects.starters.ktor.stoveKtor)\n  testFixturesApi(libs.kotest.runner.junit5)\n  testFixturesApi(libs.ktor.server.host.common)\n\n  // DI systems as compileOnly - version provided by consuming module\n  testFixturesCompileOnly(libs.koin.ktor)\n  testFixturesCompileOnly(libs.ktor.server.di)\n}\n"
  },
  {
    "path": "starters/ktor/tests/ktor-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/ktor/BridgeSystemTests.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.system.using\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.spec.style.ShouldSpec\nimport io.kotest.matchers.collections.shouldContainExactlyInAnyOrder\nimport io.kotest.matchers.shouldBe\nimport java.math.BigDecimal\n\n/**\n * Shared bridge system tests that work with both Koin and Ktor-DI.\n * Each DI variant module only needs to provide the Stove setup configuration.\n */\nabstract class BridgeSystemTests(\n  private val stoveSetup: AbstractProjectConfig\n) : ShouldSpec({\n  beforeSpec {\n    stoveSetup.beforeProject()\n  }\n\n  afterSpec {\n    stoveSetup.afterProject()\n  }\n\n  should(\"resolve service from DI container\") {\n    stove {\n      using<ExampleService> {\n        whatIsTheTime() shouldBe GetUtcNow.frozenTime\n      }\n    }\n  }\n\n  should(\"resolve multiple dependencies\") {\n    stove {\n      using<GetUtcNow, ExampleService> { getUtcNow, exampleService ->\n        getUtcNow() shouldBe GetUtcNow.frozenTime\n        exampleService.whatIsTheTime() shouldBe GetUtcNow.frozenTime\n      }\n    }\n  }\n\n  should(\"resolve config from DI container\") {\n    stove {\n      using<TestConfig> {\n        message shouldBe \"Hello from Stove!\"\n      }\n    }\n  }\n\n  should(\"resolve multiple instances of same interface\") {\n    stove {\n      using<List<PaymentService>> {\n        val order = Order(\"order-123\", BigDecimal(\"99.99\"))\n        val results = map { it.pay(order) }\n\n        results.map { it.provider } shouldContainExactlyInAnyOrder listOf(\"Stripe\", \"PayPal\", \"Square\")\n        results.all { it.success } shouldBe true\n      }\n    }\n  }\n})\n"
  },
  {
    "path": "starters/ktor/tests/ktor-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/ktor/TestDomain.kt",
    "content": "package com.trendyol.stove.ktor\n\nimport java.math.BigDecimal\nimport java.time.Instant\n\n/**\n * Common test domain classes for Ktor bridge tests.\n */\n\nfun interface GetUtcNow {\n  companion object {\n    val frozenTime: Instant = Instant.parse(\"2021-01-01T00:00:00Z\")\n  }\n\n  operator fun invoke(): Instant\n}\n\nclass SystemTimeGetUtcNow : GetUtcNow {\n  override fun invoke(): Instant = GetUtcNow.frozenTime\n}\n\nclass ExampleService(\n  private val getUtcNow: GetUtcNow\n) {\n  fun whatIsTheTime(): Instant = getUtcNow()\n}\n\ndata class TestConfig(\n  val message: String = \"Hello from Stove!\"\n)\n\n/**\n * Domain classes for testing multi-instance resolution.\n */\ndata class Order(\n  val id: String,\n  val amount: BigDecimal\n)\n\ndata class PaymentResult(\n  val provider: String,\n  val success: Boolean\n)\n\ninterface PaymentService {\n  val providerName: String\n\n  fun pay(order: Order): PaymentResult\n}\n\nclass StripePaymentService : PaymentService {\n  override val providerName = \"Stripe\"\n\n  override fun pay(order: Order) = PaymentResult(providerName, true)\n}\n\nclass PayPalPaymentService : PaymentService {\n  override val providerName = \"PayPal\"\n\n  override fun pay(order: Order) = PaymentResult(providerName, true)\n}\n\nclass SquarePaymentService : PaymentService {\n  override val providerName = \"Square\"\n\n  override fun pay(order: Order) = PaymentResult(providerName, true)\n}\n"
  },
  {
    "path": "starters/micronaut/stove-micronaut/api/stove-micronaut.api",
    "content": "public final class com/trendyol/stove/micronaut/MicronautApplicationUnderTest : com/trendyol/stove/system/abstractions/ApplicationUnderTest {\n\tpublic static final field Companion Lcom/trendyol/stove/micronaut/MicronautApplicationUnderTest$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;)V\n\tpublic fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/micronaut/MicronautApplicationUnderTest$Companion {\n}\n\npublic final class com/trendyol/stove/micronaut/MicronautApplicationUnderTestKt {\n\tpublic static final fun micronaut-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n\tpublic static synthetic fun micronaut-SscbJ7Y$default (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n}\n\npublic final class com/trendyol/stove/micronaut/MicronautBridgeSystem : com/trendyol/stove/system/BridgeSystem, com/trendyol/stove/system/abstractions/AfterRunAwareWithContext, com/trendyol/stove/system/abstractions/PluggedSystem {\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;)V\n\tpublic fun get (Lkotlin/reflect/KClass;)Ljava/lang/Object;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/micronaut/MicronautBridgeSystemKt {\n\tpublic static final fun bridge-IDauA90 (Lcom/trendyol/stove/system/Stove;)Lcom/trendyol/stove/system/Stove;\n}\n\n"
  },
  {
    "path": "starters/micronaut/stove-micronaut/build.gradle.kts",
    "content": "plugins {\n  alias(libs.plugins.micronaut.library)\n  alias(libs.plugins.google.ksp)\n}\n\ndependencies {\n  api(projects.lib.stove)\n  api(libs.micronaut.core)\n}\n\ndependencies {\n  testImplementation(projects.testExtensions.stoveExtensionsKotest)\n  testImplementation(libs.micronaut.test.kotest)\n  kspTest(platform(libs.micronaut.platform))\n  kspTest(libs.micronaut.inject.kotlin)\n}\n\nmicronaut {\n  version(libs.versions.micronaut.platform.get())\n  processing {\n    incremental(true)\n    annotations(\"com.trendyol.stove.*\")\n  }\n}\n"
  },
  {
    "path": "starters/micronaut/stove-micronaut/src/main/kotlin/com/trendyol/stove/micronaut/MicronautApplicationUnderTest.kt",
    "content": "package com.trendyol.stove.micronaut\n\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport io.micronaut.context.*\nimport kotlinx.coroutines.*\n\ninternal fun Stove.systemUnderTest(\n  runner: Runner<ApplicationContext>,\n  withParameters: List<String> = listOf()\n): ReadyStove {\n  this.applicationUnderTest(MicronautApplicationUnderTest(this, runner, withParameters))\n  return this\n}\n\nfun WithDsl.micronaut(\n  runner: Runner<ApplicationContext>,\n  withParameters: List<String> = listOf()\n): ReadyStove = this.stove.systemUnderTest(runner, withParameters)\n\n@StoveDsl\nclass MicronautApplicationUnderTest(\n  private val stove: Stove,\n  private val runner: Runner<ApplicationContext>,\n  private val parameters: List<String>\n) : ApplicationUnderTest<ApplicationContext> {\n  private lateinit var application: ApplicationContext\n\n  companion object {\n    private const val DELAY = 500L\n  }\n\n  override suspend fun start(configurations: List<String>): ApplicationContext = coroutineScope {\n    val allConfigurations = (configurations + defaultConfigurations() + parameters).map { \"--$it\" }.toTypedArray()\n    application = runner(allConfigurations)\n    while (!application.isRunning) {\n      delay(DELAY)\n      continue\n    }\n    stove.systemsOf<AfterRunAwareWithContext<ApplicationContext>>()\n      .map { async(context = Dispatchers.IO) { it.afterRun(application) } }\n      .awaitAll()\n    application\n  }\n\n  override suspend fun stop() {\n    application.stop()\n  }\n\n  private fun defaultConfigurations(): Array<String> = arrayOf(\"test-system=true\")\n}\n"
  },
  {
    "path": "starters/micronaut/stove-micronaut/src/main/kotlin/com/trendyol/stove/micronaut/MicronautBridgeSystem.kt",
    "content": "package com.trendyol.stove.micronaut\n\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport io.micronaut.context.ApplicationContext\nimport kotlin.reflect.KClass\n\n@StoveDsl\nclass MicronautBridgeSystem(\n  override val stove: Stove\n) : BridgeSystem<ApplicationContext>(stove),\n  PluggedSystem,\n  AfterRunAwareWithContext<ApplicationContext> {\n  override fun <D : Any> get(klass: KClass<D>): D = ctx.getBean(klass.java)\n}\n\nfun WithDsl.bridge(): Stove = this.stove.withBridgeSystem(MicronautBridgeSystem(this.stove))\n"
  },
  {
    "path": "starters/micronaut/stove-micronaut/src/main/resources/application.properties",
    "content": "#Mon Nov 18 19:19:36 UTC 2024\nmicronaut.application.name=stove-micronaut-testing-e2e\n"
  },
  {
    "path": "starters/micronaut/stove-micronaut/src/main/resources/logback.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <!-- encoders are assigned the type\n             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->\n        <encoder>\n            <pattern>%cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"info\">\n        <appender-ref ref=\"STDOUT\" />\n    </root>\n</configuration>\n"
  },
  {
    "path": "starters/micronaut/stove-micronaut/src/test/kotlin/com/trendyol/stove/BridgeSystemTestConfig.kt",
    "content": "package com.trendyol.stove\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.micronaut.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.micronaut.context.ApplicationContext\nimport io.micronaut.context.annotation.Factory\nimport jakarta.inject.Singleton\nimport java.time.Instant\n\n@Factory\nclass TestAppConfig {\n  @Singleton\n  fun objectMapper(): ObjectMapper = ObjectMapper()\n\n  @Singleton\n  fun getUtcNow(): GetUtcNow = SystemTimeGetUtcNow()\n\n  @Singleton\n  fun exampleService(getUtcNow: GetUtcNow): ExampleService = ExampleService(getUtcNow)\n}\n\nfun interface GetUtcNow {\n  companion object {\n    val frozenTime: Instant = Instant.parse(\"2021-01-01T00:00:00Z\")\n  }\n\n  operator fun invoke(): Instant\n}\n\nclass SystemTimeGetUtcNow : GetUtcNow {\n  override fun invoke(): Instant = GetUtcNow.frozenTime\n}\n\nclass ExampleService(\n  private val getUtcNow: GetUtcNow\n) {\n  fun whatIsTheTime(): Instant = getUtcNow()\n}\n\nobject TestAppRunner {\n  fun run(\n    args: Array<String>,\n    init: ApplicationContext.() -> Unit = {}\n  ): ApplicationContext {\n    val context = ApplicationContext\n      .builder()\n      .args(*args)\n      .packages(TestAppConfig::class.java.packageName)\n      .build()\n      .also(init)\n      .start()\n\n    return context\n  }\n}\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject() = com.trendyol.stove.system\n    .Stove()\n    .with {\n      bridge()\n      micronaut(\n        runner = { params ->\n          TestAppRunner.run(params)\n        }\n      )\n    }.run()\n\n  override suspend fun afterProject() = com.trendyol.stove.system.Stove\n    .stop()\n}\n\nclass BridgeSystemTests :\n  FunSpec({\n    test(\"bridge to application\") {\n      stove {\n        using<ExampleService> {\n          whatIsTheTime() shouldBe GetUtcNow.frozenTime\n        }\n\n        using<GetUtcNow> {\n          invoke() shouldBe GetUtcNow.frozenTime\n        }\n      }\n    }\n\n    test(\"resolve multiple\") {\n      stove {\n        using<GetUtcNow, ExampleService> { getUtcNow, exampleService ->\n          getUtcNow() shouldBe GetUtcNow.frozenTime\n          exampleService.whatIsTheTime() shouldBe GetUtcNow.frozenTime\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "starters/micronaut/stove-micronaut/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.StoveConfig\n"
  },
  {
    "path": "starters/process/stove-process/api/stove-process.api",
    "content": "public final class com/trendyol/stove/process/ArgsMapperBuilder {\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun arg (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun arg (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V\n\tpublic static synthetic fun arg$default (Lcom/trendyol/stove/process/ArgsMapperBuilder;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V\n\tpublic final fun to (Ljava/lang/String;Ljava/lang/String;)V\n}\n\npublic abstract interface class com/trendyol/stove/process/ArgsProvider {\n\tpublic static final field Companion Lcom/trendyol/stove/process/ArgsProvider$Companion;\n\tpublic abstract fun provide (Ljava/util/Map;)Ljava/util/List;\n}\n\npublic final class com/trendyol/stove/process/ArgsProvider$Companion {\n\tpublic final fun empty ()Lcom/trendyol/stove/process/ArgsProvider;\n}\n\npublic final class com/trendyol/stove/process/ArgsProviderKt {\n\tpublic static final fun argsMapper (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/process/ArgsProvider;\n\tpublic static synthetic fun argsMapper$default (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/process/ArgsProvider;\n}\n\npublic final class com/trendyol/stove/process/EnvMapperBuilder {\n\tpublic fun <init> ()V\n\tpublic final fun env (Ljava/lang/String;Ljava/lang/String;)V\n\tpublic final fun env (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V\n\tpublic final fun to (Ljava/lang/String;Ljava/lang/String;)V\n}\n\npublic abstract interface class com/trendyol/stove/process/EnvProvider {\n\tpublic static final field Companion Lcom/trendyol/stove/process/EnvProvider$Companion;\n\tpublic abstract fun provide (Ljava/util/Map;)Ljava/util/Map;\n}\n\npublic final class com/trendyol/stove/process/EnvProvider$Companion {\n\tpublic final fun empty ()Lcom/trendyol/stove/process/EnvProvider;\n}\n\npublic final class com/trendyol/stove/process/EnvProviderKt {\n\tpublic static final fun envMapper (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/process/EnvProvider;\n}\n\npublic final class com/trendyol/stove/process/ProcessApplicationOptions {\n\tpublic synthetic fun <init> (Ljava/util/List;Lcom/trendyol/stove/process/ProcessTarget;Lcom/trendyol/stove/process/EnvProvider;Lcom/trendyol/stove/process/ArgsProvider;Lkotlin/jvm/functions/Function3;Ljava/io/File;ZJILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic synthetic fun <init> (Ljava/util/List;Lcom/trendyol/stove/process/ProcessTarget;Lcom/trendyol/stove/process/EnvProvider;Lcom/trendyol/stove/process/ArgsProvider;Lkotlin/jvm/functions/Function3;Ljava/io/File;ZJLkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/util/List;\n\tpublic final fun component2 ()Lcom/trendyol/stove/process/ProcessTarget;\n\tpublic final fun component3 ()Lcom/trendyol/stove/process/EnvProvider;\n\tpublic final fun component4 ()Lcom/trendyol/stove/process/ArgsProvider;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function3;\n\tpublic final fun component6 ()Ljava/io/File;\n\tpublic final fun component7 ()Z\n\tpublic final fun component8-UwyO8pc ()J\n\tpublic final fun copy-Kk497nc (Ljava/util/List;Lcom/trendyol/stove/process/ProcessTarget;Lcom/trendyol/stove/process/EnvProvider;Lcom/trendyol/stove/process/ArgsProvider;Lkotlin/jvm/functions/Function3;Ljava/io/File;ZJ)Lcom/trendyol/stove/process/ProcessApplicationOptions;\n\tpublic static synthetic fun copy-Kk497nc$default (Lcom/trendyol/stove/process/ProcessApplicationOptions;Ljava/util/List;Lcom/trendyol/stove/process/ProcessTarget;Lcom/trendyol/stove/process/EnvProvider;Lcom/trendyol/stove/process/ArgsProvider;Lkotlin/jvm/functions/Function3;Ljava/io/File;ZJILjava/lang/Object;)Lcom/trendyol/stove/process/ProcessApplicationOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getArgsProvider ()Lcom/trendyol/stove/process/ArgsProvider;\n\tpublic final fun getBeforeStarted ()Lkotlin/jvm/functions/Function3;\n\tpublic final fun getCommand ()Ljava/util/List;\n\tpublic final fun getEnvProvider ()Lcom/trendyol/stove/process/EnvProvider;\n\tpublic final fun getGracefulShutdownTimeout-UwyO8pc ()J\n\tpublic final fun getRedirectErrorStream ()Z\n\tpublic final fun getTarget ()Lcom/trendyol/stove/process/ProcessTarget;\n\tpublic final fun getWorkingDirectory ()Ljava/io/File;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/process/ProcessApplicationUnderTest : com/trendyol/stove/system/abstractions/ApplicationUnderTest {\n\tpublic fun <init> (Lcom/trendyol/stove/process/ProcessApplicationOptions;)V\n\tpublic fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/process/ProcessDslKt {\n\tpublic static final fun goApp-k5kRdxM (Lcom/trendyol/stove/system/Stove;Ljava/lang/String;Lcom/trendyol/stove/process/ProcessTarget;Lcom/trendyol/stove/process/EnvProvider;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n\tpublic static synthetic fun goApp-k5kRdxM$default (Lcom/trendyol/stove/system/Stove;Ljava/lang/String;Lcom/trendyol/stove/process/ProcessTarget;Lcom/trendyol/stove/process/EnvProvider;ILjava/lang/Object;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n\tpublic static final fun processApp-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n}\n\npublic abstract interface class com/trendyol/stove/process/ProcessTarget {\n\tpublic abstract fun getReadiness ()Lcom/trendyol/stove/system/ReadinessStrategy;\n}\n\npublic final class com/trendyol/stove/process/ProcessTarget$Server : com/trendyol/stove/process/ProcessTarget {\n\tpublic fun <init> (ILjava/lang/String;Lcom/trendyol/stove/system/ReadinessStrategy;)V\n\tpublic synthetic fun <init> (ILjava/lang/String;Lcom/trendyol/stove/system/ReadinessStrategy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()I\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic final fun copy (ILjava/lang/String;Lcom/trendyol/stove/system/ReadinessStrategy;)Lcom/trendyol/stove/process/ProcessTarget$Server;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/process/ProcessTarget$Server;ILjava/lang/String;Lcom/trendyol/stove/system/ReadinessStrategy;ILjava/lang/Object;)Lcom/trendyol/stove/process/ProcessTarget$Server;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getPort ()I\n\tpublic final fun getPortEnvVar ()Ljava/lang/String;\n\tpublic fun getReadiness ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/process/ProcessTarget$Worker : com/trendyol/stove/process/ProcessTarget {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lcom/trendyol/stove/system/ReadinessStrategy;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/system/ReadinessStrategy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic final fun copy (Lcom/trendyol/stove/system/ReadinessStrategy;)Lcom/trendyol/stove/process/ProcessTarget$Worker;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/process/ProcessTarget$Worker;Lcom/trendyol/stove/system/ReadinessStrategy;ILjava/lang/Object;)Lcom/trendyol/stove/process/ProcessTarget$Worker;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getReadiness ()Lcom/trendyol/stove/system/ReadinessStrategy;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\n"
  },
  {
    "path": "starters/process/stove-process/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n\n  testImplementation(libs.kotest.runner.junit5)\n  testImplementation(libs.kotest.framework.engine)\n  testImplementation(libs.kotest.assertions.core)\n}\n"
  },
  {
    "path": "starters/process/stove-process/src/main/kotlin/com/trendyol/stove/process/ArgsProvider.kt",
    "content": "package com.trendyol.stove.process\n\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport com.trendyol.stove.system.application.ArgsMapperBuilder as CoreArgsMapperBuilder\nimport com.trendyol.stove.system.application.ArgsProvider as CoreArgsProvider\n\nfun interface ArgsProvider {\n  fun provide(configurations: Map<String, String>): List<String>\n\n  companion object {\n    fun empty(): ArgsProvider = ArgsProvider { emptyList() }\n  }\n}\n\nfun argsMapper(\n  prefix: String = \"--\",\n  separator: String = \"=\",\n  block: ArgsMapperBuilder.() -> Unit\n): ArgsProvider =\n  ArgsMapperBuilder(prefix, separator).apply(block).build()\n\n@StoveDsl\nclass ArgsMapperBuilder(\n  private val prefix: String,\n  private val separator: String\n) {\n  private val delegate = CoreArgsMapperBuilder(prefix = prefix, separator = separator)\n\n  infix fun String.to(flagName: String) {\n    delegate.map(this, flagName)\n  }\n\n  fun arg(flag: String, value: String? = null) {\n    delegate.arg(flag, value)\n  }\n\n  fun arg(flag: String, value: () -> String) {\n    delegate.arg(flag, value)\n  }\n\n  internal fun build(): ArgsProvider {\n    val coreProvider: CoreArgsProvider = delegate.build()\n    return ArgsProvider { configurations ->\n      coreProvider.provide(configurations)\n    }\n  }\n}\n"
  },
  {
    "path": "starters/process/stove-process/src/main/kotlin/com/trendyol/stove/process/EnvProvider.kt",
    "content": "package com.trendyol.stove.process\n\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport com.trendyol.stove.system.application.EnvMapperBuilder as CoreEnvMapperBuilder\nimport com.trendyol.stove.system.application.EnvProvider as CoreEnvProvider\n\nfun interface EnvProvider {\n  fun provide(configurations: Map<String, String>): Map<String, String>\n\n  companion object {\n    fun empty(): EnvProvider = EnvProvider { emptyMap() }\n  }\n}\n\nfun envMapper(block: EnvMapperBuilder.() -> Unit): EnvProvider =\n  EnvMapperBuilder().apply(block).build()\n\n@StoveDsl\nclass EnvMapperBuilder {\n  private val delegate = CoreEnvMapperBuilder()\n\n  infix fun String.to(envVarName: String) {\n    delegate.map(this, envVarName)\n  }\n\n  fun env(name: String, value: String) {\n    delegate.env(name, value)\n  }\n\n  fun env(name: String, value: () -> String) {\n    delegate.env(name, value)\n  }\n\n  internal fun build(): EnvProvider {\n    val coreProvider: CoreEnvProvider = delegate.build()\n    return EnvProvider { configurations ->\n      coreProvider.provide(configurations)\n    }\n  }\n}\n"
  },
  {
    "path": "starters/process/stove-process/src/main/kotlin/com/trendyol/stove/process/ProcessApplicationOptions.kt",
    "content": "package com.trendyol.stove.process\n\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport java.io.File\nimport kotlin.time.Duration\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * Describes what kind of process is being tested and how to verify its readiness.\n *\n * ## Server vs Worker\n *\n * - [Server]: Listens on a port (HTTP APIs, gRPC servers, TCP servers).\n *   Default readiness is an HTTP health check.\n * - [Worker]: Does not listen on a port (Kafka consumers, batch processors, CLI tools).\n *   Default readiness is a fixed delay.\n *\n * Both variants accept any [ReadinessStrategy], so a gRPC server can use\n * [ReadinessStrategy.TcpPort] and a worker with a health endpoint can use\n * [ReadinessStrategy.HttpGet].\n *\n * ## Usage\n *\n * ```kotlin\n * // HTTP API — default health check\n * ProcessTarget.Server(port = 8080, portEnvVar = \"APP_PORT\")\n *\n * // gRPC server — TCP readiness\n * ProcessTarget.Server(\n *     port = 50051,\n *     portEnvVar = \"GRPC_PORT\",\n *     readiness = ReadinessStrategy.TcpPort(port = 50051),\n * )\n *\n * // Kafka consumer — fixed delay\n * ProcessTarget.Worker()\n *\n * // Worker with custom probe\n * ProcessTarget.Worker(\n *     readiness = ReadinessStrategy.Probe { File(\"/tmp/ready\").exists() }\n * )\n * ```\n *\n * @see ReadinessStrategy\n * @see ProcessApplicationOptions\n */\nsealed interface ProcessTarget {\n  val readiness: ReadinessStrategy\n\n  /**\n   * A process that listens on a network port (HTTP, gRPC, TCP).\n   *\n   * @param port The port the process listens on.\n   * @param portEnvVar The environment variable name used to pass the port to the process.\n   * @param readiness How to verify the process is ready. Defaults to HTTP health check at `/health`.\n   */\n  data class Server(\n    val port: Int,\n    val portEnvVar: String = \"PORT\",\n    override val readiness: ReadinessStrategy =\n      ReadinessStrategy.HttpGet(url = \"http://localhost:$port/health\")\n  ) : ProcessTarget\n\n  /**\n   * A process without a network port (consumers, workers, CLI tools).\n   *\n   * @param readiness How to verify the process is ready. Defaults to a 2-second fixed delay.\n   */\n  data class Worker(\n    override val readiness: ReadinessStrategy = ReadinessStrategy.FixedDelay()\n  ) : ProcessTarget\n}\n\n/**\n * Options for running an OS process as the application under test.\n *\n * Works with **any language** — Go, Python, Rust, Node.js, Java CLI, etc.\n * The process is started via [ProcessBuilder], configured with environment\n * variables from [envProvider], and verified via the [target]'s readiness strategy.\n *\n * ## Example\n *\n * ```kotlin\n * // Environment variables (Go, Node.js, etc.)\n * processApp {\n *     ProcessApplicationOptions(\n *         command = listOf(\"/path/to/api-server\"),\n *         target = ProcessTarget.Server(port = 8090, portEnvVar = \"APP_PORT\"),\n *         envProvider = envMapper {\n *             \"database.host\" to \"DB_HOST\"\n *             \"database.port\" to \"DB_PORT\"\n *             env(\"LOG_LEVEL\", \"debug\")\n *         }\n *     )\n * }\n *\n * // CLI arguments (Rust, Python argparse, etc.)\n * processApp {\n *     ProcessApplicationOptions(\n *         command = listOf(\"/path/to/server\"),\n *         target = ProcessTarget.Server(port = 8090),\n *         argsProvider = argsMapper(prefix = \"--\", separator = \"=\") {\n *             \"database.host\" to \"db-host\"   // --db-host=localhost\n *             \"database.port\" to \"db-port\"   // --db-port=5432\n *         }\n *     )\n * }\n * ```\n *\n * @param command The executable and its arguments (e.g., `listOf(\"/path/to/app\", \"--verbose\")`).\n * @param target Describes the process type and how to verify readiness.\n * @param envProvider Maps Stove configurations to environment variables for the process.\n * @param argsProvider Maps Stove configurations to CLI arguments appended to the command.\n * @param beforeStarted Called after configurations are resolved but before the process is launched.\n *   Receives the resolved configuration map and the options themselves. Use this to write config files,\n *   seed directories, or perform any setup the process needs at startup.\n * @param workingDirectory Optional working directory for the process. Defaults to the JVM's current directory.\n * @param redirectErrorStream Whether to merge stderr into stdout. Defaults to `true`.\n * @param gracefulShutdownTimeout How long to wait for the process to exit after SIGTERM before force-killing.\n *\n * @see ProcessTarget\n * @see EnvProvider\n * @see ArgsProvider\n * @see envMapper\n * @see argsMapper\n */\n@StoveDsl\ndata class ProcessApplicationOptions(\n  val command: List<String>,\n  val target: ProcessTarget,\n  val envProvider: EnvProvider = EnvProvider.empty(),\n  val argsProvider: ArgsProvider = ArgsProvider.empty(),\n  val beforeStarted: suspend (\n    configurations: Map<String, String>,\n    options: ProcessApplicationOptions\n  ) -> Unit = { _, _ -> },\n  val workingDirectory: File? = null,\n  val redirectErrorStream: Boolean = true,\n  val gracefulShutdownTimeout: Duration = 5.seconds\n)\n"
  },
  {
    "path": "starters/process/stove-process/src/main/kotlin/com/trendyol/stove/process/ProcessApplicationUnderTest.kt",
    "content": "package com.trendyol.stove.process\n\nimport com.trendyol.stove.system.ReadinessChecker\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport com.trendyol.stove.system.application.toConfigurationMap\nimport kotlinx.coroutines.*\nimport org.slf4j.LoggerFactory\nimport java.util.concurrent.TimeUnit\n\n/**\n * An [ApplicationUnderTest] that manages an OS process (any language/runtime).\n *\n * Lifecycle:\n * 1. [start]: Parses Stove configurations, builds environment variables via [EnvProvider]\n *    and CLI arguments via [ArgsProvider], starts the process, reads its output, and waits for readiness.\n * 2. [stop]: Sends SIGTERM, waits for graceful shutdown, force-kills if needed.\n *\n * ## Example\n *\n * ```kotlin\n * processApp {\n *     ProcessApplicationOptions(\n *         command = listOf(\"/path/to/server\"),\n *         target = ProcessTarget.Server(port = 8080),\n *         envProvider = envMapper { \"database.host\" to \"DB_HOST\" }\n *     )\n * }\n * ```\n *\n * @see ProcessApplicationOptions\n * @see ProcessTarget\n * @see EnvProvider\n * @see ArgsProvider\n */\n@StoveDsl\nclass ProcessApplicationUnderTest(\n  private val options: ProcessApplicationOptions\n) : ApplicationUnderTest<Unit> {\n  private val logger = LoggerFactory.getLogger(javaClass)\n  private var process: Process? = null\n\n  override suspend fun start(configurations: List<String>) {\n    val configMap = configurations.toConfigurationMap()\n    val envVars = options.envProvider.provide(configMap)\n    val cliArgs = options.argsProvider.provide(configMap)\n    val fullCommand = options.command + cliArgs\n\n    val processBuilder = ProcessBuilder(fullCommand)\n      .redirectErrorStream(options.redirectErrorStream)\n\n    options.workingDirectory?.let { processBuilder.directory(it) }\n    processBuilder.environment().putAll(envVars)\n\n    // Inject port env var for Server targets\n    val target = options.target\n    if (target is ProcessTarget.Server) {\n      processBuilder.environment()[target.portEnvVar] = target.port.toString()\n    }\n\n    options.beforeStarted(configMap, options)\n\n    logger.info(\"Starting process: {} with {} env vars and {} cli args\", fullCommand, envVars.size, cliArgs.size)\n    process = withContext(Dispatchers.IO) { processBuilder.start() }\n    launchOutputReader(process!!)\n\n    ReadinessChecker.check(options.target.readiness)\n    logger.info(\"Process is ready\")\n  }\n\n  override suspend fun stop() {\n    process?.let { p ->\n      logger.info(\"Stopping process (SIGTERM)\")\n      p.destroy()\n      if (!p.waitFor(options.gracefulShutdownTimeout.inWholeSeconds, TimeUnit.SECONDS)) {\n        logger.warn(\"Process did not stop gracefully, force-killing\")\n        p.destroyForcibly().waitFor()\n      }\n      logger.info(\"Process stopped (exit code: {})\", p.exitValue())\n    }\n  }\n\n  private fun launchOutputReader(process: Process) {\n    val commandName = options.command.firstOrNull()\n      ?.substringAfterLast('/')\n      ?.substringAfterLast('\\\\')\n      ?: \"process\"\n\n    Thread {\n      process.inputStream.bufferedReader().forEachLine { line ->\n        logger.info(\"[{}] {}\", commandName, line)\n      }\n    }.apply {\n      isDaemon = true\n      name = \"$commandName-output-reader\"\n      start()\n    }\n  }\n}\n"
  },
  {
    "path": "starters/process/stove-process/src/main/kotlin/com/trendyol/stove/process/ProcessDsl.kt",
    "content": "package com.trendyol.stove.process\n\nimport com.trendyol.stove.system.WithDsl\nimport com.trendyol.stove.system.abstractions.ReadyStove\n\n/**\n * Registers an OS-process-based application under test.\n *\n * Works with **any language** — Go, Python, Rust, Node.js, Java CLI, etc.\n * The process is started with environment variables derived from Stove's\n * infrastructure configurations, and readiness is verified via the target's\n * [ReadinessStrategy][com.trendyol.stove.system.ReadinessStrategy].\n *\n * ## Example\n *\n * ```kotlin\n * Stove().with {\n *     httpClient { HttpClientSystemOptions(baseUrl = \"http://localhost:8090\") }\n *     postgresql { PostgresqlOptions(...) }\n *\n *     processApp {\n *         ProcessApplicationOptions(\n *             command = listOf(\"/path/to/server\"),\n *             target = ProcessTarget.Server(port = 8090, portEnvVar = \"APP_PORT\"),\n *             envProvider = envMapper {\n *                 \"database.host\" to \"DB_HOST\"\n *                 \"database.port\" to \"DB_PORT\"\n *             }\n *         )\n *     }\n * }.run()\n * ```\n *\n * @param configure Configuration block that returns [ProcessApplicationOptions].\n * @return [ReadyStove] to chain with `.run()`.\n * @see ProcessApplicationOptions\n * @see ProcessTarget\n */\nfun WithDsl.processApp(configure: () -> ProcessApplicationOptions): ReadyStove {\n  this.stove.applicationUnderTest(ProcessApplicationUnderTest(configure()))\n  return this.stove\n}\n\n/**\n * Convenience extension for Go applications.\n *\n * Defaults the binary path from the `go.app.binary` system property, which is\n * typically set by the Gradle build task that compiles the Go binary.\n *\n * ## Example\n *\n * ```kotlin\n * Stove().with {\n *     httpClient { HttpClientSystemOptions(baseUrl = \"http://localhost:8090\") }\n *     postgresql { PostgresqlOptions(...) }\n *\n *     goApp(\n *         target = ProcessTarget.Server(port = 8090, portEnvVar = \"APP_PORT\"),\n *         envProvider = envMapper {\n *             \"database.host\" to \"DB_HOST\"\n *             \"database.port\" to \"DB_PORT\"\n *         }\n *     )\n * }.run()\n * ```\n *\n * @param binaryPath Path to the compiled Go binary. Defaults to `go.app.binary` system property.\n * @param target The process target (Server or Worker) with readiness strategy.\n * @param envProvider Maps Stove configurations to environment variables.\n * @return [ReadyStove] to chain with `.run()`.\n */\nfun WithDsl.goApp(\n  binaryPath: String = System.getProperty(\"go.app.binary\")\n    ?: error(\"go.app.binary system property not set\"),\n  target: ProcessTarget,\n  envProvider: EnvProvider = EnvProvider.empty()\n): ReadyStove = processApp {\n  ProcessApplicationOptions(\n    command = listOf(binaryPath),\n    target = target,\n    envProvider = envProvider\n  )\n}\n"
  },
  {
    "path": "starters/process/stove-process/src/test/kotlin/com/trendyol/stove/process/ArgsProviderTest.kt",
    "content": "package com.trendyol.stove.process\n\nimport com.trendyol.stove.system.ReadinessStrategy\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldBeEmpty\nimport io.kotest.matchers.collections.shouldContainExactly\nimport kotlinx.coroutines.runBlocking\nimport kotlin.time.Duration.Companion.milliseconds\n\nclass ArgsProviderTest :\n  FunSpec({\n    context(\"ArgsProvider.empty\") {\n      test(\"returns empty list\") {\n        val provider = ArgsProvider.empty()\n        provider.provide(mapOf(\"a\" to \"b\")).shouldBeEmpty()\n      }\n    }\n\n    context(\"ArgsProvider fun interface\") {\n      test(\"can be implemented as lambda\") {\n        val provider = ArgsProvider { configs ->\n          listOf(\"--host\", configs.getValue(\"database.host\"))\n        }\n\n        provider.provide(mapOf(\"database.host\" to \"localhost\")) shouldContainExactly\n          listOf(\"--host\", \"localhost\")\n      }\n    }\n\n    context(\"argsMapper with equals separator\") {\n      test(\"produces --flag=value args\") {\n        val provider = argsMapper(prefix = \"--\", separator = \"=\") {\n          \"database.host\" to \"db-host\"\n          \"database.port\" to \"db-port\"\n        }\n\n        val result = provider.provide(\n          mapOf(\"database.host\" to \"localhost\", \"database.port\" to \"5432\")\n        )\n\n        result shouldContainExactly listOf(\"--db-host=localhost\", \"--db-port=5432\")\n      }\n    }\n\n    context(\"argsMapper with space separator\") {\n      test(\"produces two separate args per mapping\") {\n        val provider = argsMapper(prefix = \"--\", separator = \" \") {\n          \"database.host\" to \"db-host\"\n          \"database.port\" to \"db-port\"\n        }\n\n        val result = provider.provide(\n          mapOf(\"database.host\" to \"localhost\", \"database.port\" to \"5432\")\n        )\n\n        result shouldContainExactly listOf(\"--db-host\", \"localhost\", \"--db-port\", \"5432\")\n      }\n    }\n\n    context(\"argsMapper with single-dash prefix\") {\n      test(\"produces -flag value args\") {\n        val provider = argsMapper(prefix = \"-\", separator = \" \") {\n          \"database.host\" to \"h\"\n          \"database.port\" to \"p\"\n        }\n\n        val result = provider.provide(\n          mapOf(\"database.host\" to \"localhost\", \"database.port\" to \"5432\")\n        )\n\n        result shouldContainExactly listOf(\"-h\", \"localhost\", \"-p\", \"5432\")\n      }\n    }\n\n    context(\"argsMapper with no prefix\") {\n      test(\"produces flag=value args\") {\n        val provider = argsMapper(prefix = \"\", separator = \"=\") {\n          \"database.host\" to \"db-host\"\n        }\n\n        val result = provider.provide(mapOf(\"database.host\" to \"localhost\"))\n\n        result shouldContainExactly listOf(\"db-host=localhost\")\n      }\n    }\n\n    context(\"argsMapper skips missing keys\") {\n      test(\"only includes present config keys\") {\n        val provider = argsMapper(prefix = \"--\", separator = \"=\") {\n          \"database.host\" to \"db-host\"\n          \"database.port\" to \"db-port\"\n        }\n\n        val result = provider.provide(mapOf(\"database.host\" to \"localhost\"))\n\n        result shouldContainExactly listOf(\"--db-host=localhost\")\n      }\n    }\n\n    context(\"argsMapper static args\") {\n      test(\"adds boolean flag without value\") {\n        val provider = argsMapper(prefix = \"--\", separator = \"=\") {\n          arg(\"verbose\")\n        }\n\n        val result = provider.provide(emptyMap())\n\n        result shouldContainExactly listOf(\"--verbose\")\n      }\n\n      test(\"adds flag with static value\") {\n        val provider = argsMapper(prefix = \"--\", separator = \"=\") {\n          arg(\"log-level\", \"debug\")\n        }\n\n        val result = provider.provide(emptyMap())\n\n        result shouldContainExactly listOf(\"--log-level=debug\")\n      }\n\n      test(\"adds flag with computed value\") {\n        val provider = argsMapper(prefix = \"--\", separator = \"=\") {\n          arg(\"config-file\") { \"/tmp/test.yaml\" }\n        }\n\n        val result = provider.provide(emptyMap())\n\n        result shouldContainExactly listOf(\"--config-file=/tmp/test.yaml\")\n      }\n\n      test(\"static flag with space separator produces two args\") {\n        val provider = argsMapper(prefix = \"--\", separator = \" \") {\n          arg(\"log-level\", \"debug\")\n        }\n\n        val result = provider.provide(emptyMap())\n\n        result shouldContainExactly listOf(\"--log-level\", \"debug\")\n      }\n    }\n\n    context(\"argsMapper combines mappings and static args\") {\n      test(\"mappings come before static args\") {\n        val provider = argsMapper(prefix = \"--\", separator = \"=\") {\n          \"database.host\" to \"db-host\"\n          arg(\"verbose\")\n          arg(\"log-level\", \"debug\")\n        }\n\n        val result = provider.provide(mapOf(\"database.host\" to \"localhost\"))\n\n        result shouldContainExactly listOf(\"--db-host=localhost\", \"--verbose\", \"--log-level=debug\")\n      }\n    }\n\n    context(\"empty builder\") {\n      test(\"returns empty list\") {\n        val provider = argsMapper { }\n        provider.provide(mapOf(\"a\" to \"b\")).shouldBeEmpty()\n      }\n    }\n\n    context(\"integration with ProcessApplicationUnderTest\") {\n      test(\"CLI args are appended to command\") {\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\"sh\", \"-c\", \"echo received: \\\"$@\\\"\", \"--\"),\n            target = ProcessTarget.Worker(\n              readiness = ReadinessStrategy.FixedDelay(100.milliseconds)\n            ),\n            argsProvider = argsMapper(prefix = \"--\", separator = \"=\") {\n              \"database.host\" to \"db-host\"\n              arg(\"verbose\")\n            }\n          )\n        )\n\n        runBlocking {\n          aut.start(listOf(\"database.host=localhost\"))\n          aut.stop()\n        }\n      }\n\n      test(\"both env vars and CLI args work together\") {\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\n              \"sh\",\n              \"-c\",\n              \"[ \\\"\\$DB_HOST\\\" = 'localhost' ] || exit 1; sleep 1\"\n            ),\n            target = ProcessTarget.Worker(\n              readiness = ReadinessStrategy.FixedDelay(100.milliseconds)\n            ),\n            envProvider = envMapper {\n              \"database.host\" to \"DB_HOST\"\n            },\n            argsProvider = argsMapper(prefix = \"--\", separator = \"=\") {\n              \"database.port\" to \"db-port\"\n            }\n          )\n        )\n\n        runBlocking {\n          aut.start(listOf(\"database.host=localhost\", \"database.port=5432\"))\n          aut.stop()\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "starters/process/stove-process/src/test/kotlin/com/trendyol/stove/process/EnvProviderTest.kt",
    "content": "package com.trendyol.stove.process\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.maps.shouldBeEmpty\nimport io.kotest.matchers.maps.shouldContainExactly\nimport io.kotest.matchers.shouldBe\n\nclass EnvProviderTest :\n  FunSpec({\n    context(\"EnvProvider.empty\") {\n      test(\"returns empty map\") {\n        val provider = EnvProvider.empty()\n        provider.provide(mapOf(\"a\" to \"b\")).shouldBeEmpty()\n      }\n    }\n\n    context(\"EnvProvider fun interface\") {\n      test(\"can be implemented as lambda\") {\n        val provider = EnvProvider { configs ->\n          mapOf(\"DB_HOST\" to configs.getValue(\"database.host\"))\n        }\n\n        provider.provide(mapOf(\"database.host\" to \"localhost\")) shouldContainExactly\n          mapOf(\"DB_HOST\" to \"localhost\")\n      }\n    }\n\n    context(\"envMapper builder\") {\n      test(\"maps config keys to env var names\") {\n        val provider = envMapper {\n          \"database.host\" to \"DB_HOST\"\n          \"database.port\" to \"DB_PORT\"\n        }\n\n        val result = provider.provide(\n          mapOf(\"database.host\" to \"localhost\", \"database.port\" to \"5432\")\n        )\n\n        result shouldContainExactly mapOf(\"DB_HOST\" to \"localhost\", \"DB_PORT\" to \"5432\")\n      }\n\n      test(\"skips missing config keys silently\") {\n        val provider = envMapper {\n          \"database.host\" to \"DB_HOST\"\n          \"database.port\" to \"DB_PORT\"\n        }\n\n        val result = provider.provide(mapOf(\"database.host\" to \"localhost\"))\n\n        result shouldContainExactly mapOf(\"DB_HOST\" to \"localhost\")\n      }\n\n      test(\"adds static env vars\") {\n        val provider = envMapper {\n          env(\"APP_ENV\", \"test\")\n          env(\"LOG_LEVEL\", \"debug\")\n        }\n\n        val result = provider.provide(emptyMap())\n\n        result shouldContainExactly mapOf(\"APP_ENV\" to \"test\", \"LOG_LEVEL\" to \"debug\")\n      }\n\n      test(\"adds computed env vars\") {\n        var counter = 0\n        val provider = envMapper {\n          env(\"COMPUTED\") { \"value-${++counter}\" }\n        }\n\n        val result = provider.provide(emptyMap())\n\n        result[\"COMPUTED\"] shouldBe \"value-1\"\n      }\n\n      test(\"combines mappings and static vars\") {\n        val provider = envMapper {\n          \"database.host\" to \"DB_HOST\"\n          env(\"APP_ENV\", \"test\")\n          env(\"DYNAMIC\") { \"computed\" }\n        }\n\n        val result = provider.provide(mapOf(\"database.host\" to \"localhost\"))\n\n        result shouldContainExactly mapOf(\n          \"DB_HOST\" to \"localhost\",\n          \"APP_ENV\" to \"test\",\n          \"DYNAMIC\" to \"computed\"\n        )\n      }\n\n      test(\"static vars override mappings with same name\") {\n        val provider = envMapper {\n          \"some.key\" to \"MY_VAR\"\n          env(\"MY_VAR\", \"override\")\n        }\n\n        val result = provider.provide(mapOf(\"some.key\" to \"original\"))\n\n        result[\"MY_VAR\"] shouldBe \"override\"\n      }\n\n      test(\"empty builder returns empty map\") {\n        val provider = envMapper { }\n        provider.provide(mapOf(\"a\" to \"b\")).shouldBeEmpty()\n      }\n    }\n  })\n"
  },
  {
    "path": "starters/process/stove-process/src/test/kotlin/com/trendyol/stove/process/ProcessApplicationUnderTestTest.kt",
    "content": "package com.trendyol.stove.process\n\nimport com.trendyol.stove.system.ReadinessStrategy\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.runBlocking\nimport kotlin.time.Duration.Companion.milliseconds\nimport kotlin.time.Duration.Companion.seconds\n\nclass ProcessApplicationUnderTestTest :\n  FunSpec({\n    context(\"Server target\") {\n      test(\"starts process and injects port env var\") {\n        val port = java.net.ServerSocket(0).use { it.localPort }\n\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\"sh\", \"-c\", \"echo PORT=\\$APP_PORT && sleep 5\"),\n            target = ProcessTarget.Server(\n              port = port,\n              portEnvVar = \"APP_PORT\",\n              readiness = ReadinessStrategy.FixedDelay(100.milliseconds)\n            ),\n            envProvider = EnvProvider.empty()\n          )\n        )\n\n        runBlocking {\n          aut.start(emptyList())\n          aut.stop()\n        }\n      }\n\n      test(\"passes Stove configurations through envProvider\") {\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\n              \"sh\",\n              \"-c\",\n              \"[ \\\"\\$DB_HOST\\\" = 'localhost' ] && [ \\\"\\$DB_PORT\\\" = '5432' ] && exit 0 || exit 1\"\n            ),\n            target = ProcessTarget.Worker(\n              readiness = ReadinessStrategy.FixedDelay(100.milliseconds)\n            ),\n            envProvider = envMapper {\n              \"database.host\" to \"DB_HOST\"\n              \"database.port\" to \"DB_PORT\"\n            }\n          )\n        )\n\n        runBlocking {\n          aut.start(listOf(\"database.host=localhost\", \"database.port=5432\"))\n          aut.stop()\n        }\n      }\n\n      test(\"static and computed env vars are injected\") {\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\n              \"sh\",\n              \"-c\",\n              \"[ \\\"\\$APP_ENV\\\" = 'test' ] && [ \\\"\\$COMPUTED\\\" = 'hello' ] || exit 1; sleep 1\"\n            ),\n            target = ProcessTarget.Worker(\n              readiness = ReadinessStrategy.FixedDelay(100.milliseconds)\n            ),\n            envProvider = envMapper {\n              env(\"APP_ENV\", \"test\")\n              env(\"COMPUTED\") { \"hello\" }\n            }\n          )\n        )\n\n        runBlocking {\n          aut.start(emptyList())\n          aut.stop()\n        }\n      }\n    }\n\n    context(\"beforeStarted callback\") {\n      test(\"is called with configurations and options before process starts\") {\n        lateinit var capturedConfigs: Map<String, String>\n        lateinit var capturedOptions: ProcessApplicationOptions\n\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\"sh\", \"-c\", \"sleep 5\"),\n            target = ProcessTarget.Worker(\n              readiness = ReadinessStrategy.FixedDelay(100.milliseconds)\n            ),\n            envProvider = envMapper {\n              \"database.host\" to \"DB_HOST\"\n            },\n            beforeStarted = { configs, opts ->\n              capturedConfigs = configs\n              capturedOptions = opts\n            }\n          )\n        )\n\n        runBlocking {\n          aut.start(listOf(\"database.host=localhost\", \"database.port=5432\"))\n          capturedConfigs shouldBe mapOf(\"database.host\" to \"localhost\", \"database.port\" to \"5432\")\n          capturedOptions.command shouldBe listOf(\"sh\", \"-c\", \"sleep 5\")\n          capturedOptions.workingDirectory shouldBe null\n          aut.stop()\n        }\n      }\n\n      test(\"options include working directory when set\") {\n        val tempDir = kotlin.io.path.createTempDirectory(\"stove-test\").toFile()\n        lateinit var capturedOptions: ProcessApplicationOptions\n\n        try {\n          val aut = ProcessApplicationUnderTest(\n            ProcessApplicationOptions(\n              command = listOf(\"sh\", \"-c\", \"sleep 5\"),\n              target = ProcessTarget.Worker(\n                readiness = ReadinessStrategy.FixedDelay(100.milliseconds)\n              ),\n              beforeStarted = { _, opts -> capturedOptions = opts },\n              workingDirectory = tempDir\n            )\n          )\n\n          runBlocking {\n            aut.start(emptyList())\n            capturedOptions.workingDirectory shouldBe tempDir\n            aut.stop()\n          }\n        } finally {\n          tempDir.deleteRecursively()\n        }\n      }\n    }\n\n    context(\"Worker target\") {\n      test(\"starts without port injection\") {\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\"sh\", \"-c\", \"sleep 5\"),\n            target = ProcessTarget.Worker(\n              readiness = ReadinessStrategy.FixedDelay(100.milliseconds)\n            )\n          )\n        )\n\n        runBlocking {\n          aut.start(emptyList())\n          aut.stop()\n        }\n      }\n    }\n\n    context(\"stop lifecycle\") {\n      test(\"stop terminates a running process\") {\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\"sleep\", \"30\"),\n            target = ProcessTarget.Worker(\n              readiness = ReadinessStrategy.FixedDelay(100.milliseconds)\n            ),\n            gracefulShutdownTimeout = 5.seconds\n          )\n        )\n\n        runBlocking {\n          aut.start(emptyList())\n          aut.stop()\n        }\n        // No exception — process was stopped successfully\n      }\n\n      test(\"stop is no-op when process was never started\") {\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\"sh\", \"-c\", \"sleep 1\"),\n            target = ProcessTarget.Worker()\n          )\n        )\n\n        runBlocking { aut.stop() }\n        // No exception — success\n      }\n    }\n\n    context(\"readiness\") {\n      test(\"fails when process exits before readiness check\") {\n        shouldThrow<Exception> {\n          val aut = ProcessApplicationUnderTest(\n            ProcessApplicationOptions(\n              command = listOf(\"sh\", \"-c\", \"exit 1\"),\n              target = ProcessTarget.Server(\n                port = 19999,\n                readiness = ReadinessStrategy.TcpPort(\n                  port = 19999,\n                  retries = 2,\n                  retryDelay = 50.milliseconds\n                )\n              )\n            )\n          )\n          runBlocking { aut.start(emptyList()) }\n        }\n      }\n\n      test(\"readiness probe succeeds\") {\n        var ready = false\n        val aut = ProcessApplicationUnderTest(\n          ProcessApplicationOptions(\n            command = listOf(\"sh\", \"-c\", \"sleep 30\"),\n            target = ProcessTarget.Worker(\n              readiness = ReadinessStrategy.Probe(retries = 5, retryDelay = 50.milliseconds) {\n                ready = true\n                true\n              }\n            )\n          )\n        )\n\n        runBlocking {\n          aut.start(emptyList())\n          ready shouldBe true\n          aut.stop()\n        }\n      }\n    }\n\n    context(\"ProcessTarget defaults\") {\n      test(\"Server defaults to HTTP health check on /health\") {\n        val target = ProcessTarget.Server(port = 8080)\n        val readiness = target.readiness\n        (readiness is ReadinessStrategy.HttpGet) shouldBe true\n        (readiness as ReadinessStrategy.HttpGet).url shouldContain \"8080\"\n        readiness.url shouldContain \"/health\"\n      }\n\n      test(\"Server accepts custom portEnvVar\") {\n        val target = ProcessTarget.Server(port = 8080, portEnvVar = \"MY_PORT\")\n        target.portEnvVar shouldBe \"MY_PORT\"\n      }\n\n      test(\"Worker defaults to FixedDelay\") {\n        val target = ProcessTarget.Worker()\n        (target.readiness is ReadinessStrategy.FixedDelay) shouldBe true\n      }\n\n      test(\"Worker accepts custom readiness strategy\") {\n        val target = ProcessTarget.Worker(\n          readiness = ReadinessStrategy.TcpPort(port = 9090)\n        )\n        (target.readiness is ReadinessStrategy.TcpPort) shouldBe true\n      }\n    }\n  })\n"
  },
  {
    "path": "starters/quarkus/stove-quarkus/api/stove-quarkus.api",
    "content": "public final class com/trendyol/stove/quarkus/QuarkusApplicationUnderTest : com/trendyol/stove/system/abstractions/ApplicationUnderTest {\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;)V\n\tpublic fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/quarkus/QuarkusApplicationUnderTestKt {\n\tpublic static final fun quarkus-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n\tpublic static synthetic fun quarkus-SscbJ7Y$default (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n}\n\n"
  },
  {
    "path": "starters/quarkus/stove-quarkus/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  compileOnly(libs.quarkus.core)\n}\n"
  },
  {
    "path": "starters/quarkus/stove-quarkus/src/main/kotlin/com/trendyol/stove/quarkus/QuarkusApplicationUnderTest.kt",
    "content": "package com.trendyol.stove.quarkus\n\nimport com.trendyol.stove.system.Runner\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.WithDsl\nimport com.trendyol.stove.system.abstractions.AfterRunAwareWithContext\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.system.abstractions.ReadyStove\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport io.quarkus.runtime.Quarkus\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.delay\nimport java.io.BufferedInputStream\nimport java.io.Closeable\nimport java.io.File\nimport java.lang.reflect.InvocationTargetException\nimport java.net.HttpURLConnection\nimport java.net.URI\nimport java.net.URL\nimport java.net.URLClassLoader\nimport java.nio.file.Path\nimport java.nio.file.Paths\nimport java.util.concurrent.atomic.AtomicReference\n\ninternal fun Stove.systemUnderTest(\n  runner: Runner<Unit>,\n  withParameters: List<String> = listOf()\n): ReadyStove {\n  this.applicationUnderTest(QuarkusApplicationUnderTest(this, runner, withParameters))\n  return this\n}\n\nfun WithDsl.quarkus(\n  runner: Runner<Unit>,\n  withParameters: List<String> = listOf()\n): ReadyStove = this.stove.systemUnderTest(runner, withParameters)\n\n@StoveDsl\nclass QuarkusApplicationUnderTest(\n  private val stove: Stove,\n  private val runner: Runner<Unit>,\n  private val parameters: List<String>\n) : ApplicationUnderTest<Unit> {\n  private var launcher: QuarkusLauncher? = null\n\n  @Suppress(\"TooGenericExceptionCaught\")\n  override suspend fun start(configurations: List<String>): Unit = coroutineScope {\n    val quarkusLauncher = createLauncher(configurations)\n    launcher = quarkusLauncher\n    quarkusLauncher.start()\n\n    try {\n      waitForApplication(quarkusLauncher)\n      notifySystemsAfterRun()\n    } catch (error: Throwable) {\n      stopAfterStartupFailure(quarkusLauncher, error)\n      launcher = null\n      throw error\n    }\n  }\n\n  override suspend fun stop() {\n    launcher?.stop()\n    launcher = null\n  }\n\n  private suspend fun waitForApplication(quarkusLauncher: QuarkusLauncher) {\n    val startTime = System.currentTimeMillis()\n    val startupTimeoutMs = resolveTimeoutMillis(\n      propertyName = STARTUP_TIMEOUT_PROPERTY,\n      defaultValue = DEFAULT_STARTUP_TIMEOUT_MS\n    )\n\n    while (true) {\n      failIfStartupFailed(quarkusLauncher)\n      if (quarkusLauncher.isReady()) return\n      failIfStartupTimedOut(quarkusLauncher, startTime, startupTimeoutMs)\n\n      delay(POLL_INTERVAL_MS)\n    }\n  }\n\n  private suspend fun notifySystemsAfterRun() {\n    coroutineScope {\n      stove.systemsOf<AfterRunAwareWithContext<Unit>>()\n        .map { async { it.afterRun(Unit) } }\n        .awaitAll()\n    }\n  }\n\n  private fun createLauncher(configurations: List<String>) = QuarkusLauncher(\n    runtime = resolveLaunchRuntime(runner),\n    configuration = QuarkusConfiguration.from(configurations + parameters)\n  )\n\n  @Suppress(\"TooGenericExceptionCaught\")\n  private fun stopAfterStartupFailure(quarkusLauncher: QuarkusLauncher, error: Throwable) {\n    try {\n      quarkusLauncher.stop()\n    } catch (stopError: Throwable) {\n      error.addSuppressed(stopError)\n    }\n  }\n\n  private fun failIfStartupFailed(quarkusLauncher: QuarkusLauncher) {\n    quarkusLauncher.failureOrNull()?.let { failure ->\n      throw IllegalStateException(\"Quarkus startup failed\", failure)\n    }\n  }\n\n  private fun failIfStartupTimedOut(\n    quarkusLauncher: QuarkusLauncher,\n    startTime: Long,\n    startupTimeoutMs: Long\n  ) {\n    if (System.currentTimeMillis() - startTime <= startupTimeoutMs) return\n    error(readinessTimeoutMessage(quarkusLauncher, startupTimeoutMs))\n  }\n\n  private fun readinessTimeoutMessage(quarkusLauncher: QuarkusLauncher, startupTimeoutMs: Long) =\n    \"Timeout waiting for Quarkus application readiness after ${startupTimeoutMs}ms. \" +\n      quarkusLauncher.describeReadinessState()\n}\n\nprivate data class LaunchRuntime(\n  val modeName: String,\n  val launch: (Array<String>) -> Unit,\n  val stop: () -> Unit = {},\n  val close: () -> Unit = {}\n)\n\nprivate data class QuarkusConfiguration(\n  val properties: Map<String, String>,\n  val httpPort: Int,\n  val readinessHosts: List<String>\n) {\n  val readinessUrls: List<String> = readinessHosts.map { \"http://$it:$httpPort/\" }\n\n  companion object {\n    fun from(configurations: List<String>): QuarkusConfiguration {\n      val properties = parseConfigurations(configurations)\n      return QuarkusConfiguration(\n        properties = properties,\n        httpPort = properties[\"quarkus.http.port\"]?.toIntOrNull() ?: DEFAULT_HTTP_PORT,\n        readinessHosts = resolveReadinessHosts(properties)\n      )\n    }\n  }\n}\n\nprivate fun resolveLaunchRuntime(runner: Runner<Unit>): LaunchRuntime =\n  resolvePackagedRuntimeArtifacts()\n    ?.let(::packagedRuntime)\n    ?: directMainRuntime(runner)\n\nprivate fun directMainRuntime(runner: Runner<Unit>) = LaunchRuntime(\n  modeName = \"direct-main\",\n  launch = { args -> runner(args) },\n  stop = { Quarkus.asyncExit() }\n)\n\nprivate fun packagedRuntime(packagedRuntimeArtifacts: PackagedRuntimeArtifacts): LaunchRuntime =\n  PackagedRuntimeState(packagedRuntimeArtifacts).asRuntime()\n\nprivate class QuarkusLauncher(\n  private val runtime: LaunchRuntime,\n  private val configuration: QuarkusConfiguration\n) : Closeable {\n  private val runnerArguments = emptyArray<String>()\n  private val startupFailure = AtomicReference<Throwable?>(null)\n  private var previousSystemProperties: Map<String, String?> = emptyMap()\n  private var launcherThread: Thread? = null\n\n  @Suppress(\"TooGenericExceptionCaught\")\n  fun start() {\n    check(launcherThread == null) { \"Quarkus launcher already started\" }\n\n    prepareStart()\n\n    try {\n      val thread = createLauncherThread()\n      launcherThread = thread\n      thread.start()\n    } catch (error: Throwable) {\n      cleanupAfterFailedStart()\n      throw error\n    }\n  }\n\n  fun isAlive(): Boolean = launcherThread?.isAlive == true\n\n  fun failureOrNull(): Throwable? = startupFailure.get()\n\n  fun isReady(): Boolean = hasStartupSignal() || configuration.isHttpReady()\n\n  fun describeReadinessState(): String =\n    \"launcherMode=${runtime.modeName}, \" +\n      \"signalPresent=${hasStartupSignal()}, \" +\n      \"httpReady=${configuration.isHttpReady()}, \" +\n      \"readinessUrls=${configuration.readinessUrls}, \" +\n      \"launcherAlive=${isAlive()}, \" +\n      \"launcherState=${launcherThread?.state ?: \"not-started\"}\"\n\n  @Suppress(\"TooGenericExceptionCaught\")\n  fun stop() {\n    val thread = launcherThread\n    var stopFailure: Throwable? = null\n\n    try {\n      stopFailure = stopRunningThread(thread)\n    } finally {\n      cleanup()\n    }\n\n    stopFailure?.let { throw it }\n  }\n\n  override fun close() {\n    stop()\n  }\n\n  private fun hasStartupSignal(): Boolean =\n    System.getProperty(DEFAULT_READY_SIGNAL_PROPERTY) == READY_SIGNAL_VALUE\n\n  private fun prepareStart() {\n    clearStartupSignal()\n    previousSystemProperties = applySystemProperties(configuration.properties)\n  }\n\n  private fun createLauncherThread() = Thread(\n    { runtime.launchCatching(runnerArguments, startupFailure) },\n    \"quarkus-main-launcher\"\n  ).apply {\n    isDaemon = false\n    setUncaughtExceptionHandler { _, error ->\n      startupFailure.compareAndSet(null, unwrap(error))\n    }\n  }\n\n  private fun cleanupAfterFailedStart() {\n    restoreSystemProperties(previousSystemProperties)\n    previousSystemProperties = emptyMap()\n  }\n\n  @Suppress(\"TooGenericExceptionCaught\")\n  private fun stopRunningThread(thread: Thread?): Throwable? {\n    if (thread == null || !thread.isAlive) return null\n\n    val stopFailure = try {\n      runtime.stop()\n      null\n    } catch (error: Throwable) {\n      error\n    }\n\n    thread.join(SHUTDOWN_TIMEOUT_MS)\n    if (thread.isAlive) {\n      error(\"Timeout waiting for Quarkus to shut down\")\n    }\n    return stopFailure\n  }\n\n  private fun cleanup() {\n    clearStartupSignal()\n    restoreSystemProperties(previousSystemProperties)\n    previousSystemProperties = emptyMap()\n    runtime.close()\n    launcherThread = null\n  }\n}\n\nprivate fun parseConfigurations(configurations: List<String>): Map<String, String> {\n  val properties = linkedMapOf<String, String>()\n  configurations.forEach { configuration ->\n    val separatorIndex = configuration.indexOf('=')\n    require(separatorIndex > 0) {\n      \"Invalid Quarkus configuration '$configuration'. Expected key=value.\"\n    }\n    val key = configuration.substring(0, separatorIndex)\n    val value = configuration.substring(separatorIndex + 1)\n    properties[key] = value\n  }\n  return properties\n}\n\nprivate fun resolveReadinessHosts(configurationProperties: Map<String, String>): List<String> {\n  val configuredHost = configurationProperties[\"quarkus.http.host\"]\n  return listOfNotNull(configuredHost, \"localhost\", \"127.0.0.1\")\n    .filterNot { it == \"0.0.0.0\" || it == \"::\" || it == \"[::]\" }\n    .distinct()\n}\n\nprivate fun QuarkusConfiguration.isHttpReady(): Boolean = readinessUrls.any(::isReadyUrl)\n\nprivate fun isReadyUrl(readinessUrl: String): Boolean {\n  val connection = try {\n    URI.create(readinessUrl).toURL().openConnection() as HttpURLConnection\n  } catch (_: Exception) {\n    return false\n  }\n\n  return try {\n    connection.connectTimeout = CONNECTION_TIMEOUT_MS.toInt()\n    connection.readTimeout = CONNECTION_TIMEOUT_MS.toInt()\n    connection.requestMethod = \"GET\"\n    connection.instanceFollowRedirects = false\n    connection.responseCode\n    true\n  } catch (_: Exception) {\n    false\n  } finally {\n    connection.disconnect()\n  }\n}\n\nprivate fun applySystemProperties(properties: Map<String, String>): Map<String, String?> =\n  buildMap {\n    properties.forEach { (key, value) ->\n      put(key, System.getProperty(key))\n      System.setProperty(key, value)\n    }\n  }\n\nprivate fun restoreSystemProperties(previousSystemProperties: Map<String, String?>) {\n  previousSystemProperties.forEach { (key, previousValue) ->\n    if (previousValue == null) {\n      System.clearProperty(key)\n    } else {\n      System.setProperty(key, previousValue)\n    }\n  }\n}\n\nprivate fun clearStartupSignal() {\n  System.clearProperty(DEFAULT_READY_SIGNAL_PROPERTY)\n}\n\nprivate fun resolvePackagedRuntimeArtifacts(): PackagedRuntimeArtifacts? =\n  resolveClassPathEntries()\n    .mapNotNull { it.findBuildDirectory() }\n    .distinct()\n    .firstNotNullOfOrNull(::resolvePackagedRuntimeArtifacts)\n\nprivate fun Path.findBuildDirectory(): Path? =\n  generateSequence(this) { current -> current.parent }\n    .firstOrNull { current -> current.fileName?.toString() == \"build\" }\n\nprivate fun resolveTimeoutMillis(propertyName: String, defaultValue: Long): Long =\n  System\n    .getProperty(propertyName)\n    ?.toLongOrNull()\n    ?.takeIf { it > 0 }\n    ?: defaultValue\n\nprivate data class PackagedRuntimeArtifacts(\n  val appRoot: Path,\n  val applicationDat: Path,\n  val bootUrls: Array<URL>\n)\n\nprivate data class PackagedApplication(\n  val runnerClassLoader: ClassLoader,\n  val mainClassName: String\n)\n\nprivate class PackagedRuntimeState(\n  private val packagedRuntimeArtifacts: PackagedRuntimeArtifacts\n) {\n  private var runnerClassLoader: Any? = null\n  private var bootClassLoader: URLClassLoader? = null\n\n  fun asRuntime() = LaunchRuntime(\n    modeName = \"packaged-runtime\",\n    launch = ::launch,\n    stop = ::stop,\n    close = ::close\n  )\n\n  fun launch(runnerArguments: Array<String>) {\n    withBootClassLoader { packagedBootClassLoader ->\n      val packagedApplication = loadPackagedApplication(packagedBootClassLoader)\n      launchPackagedApplication(packagedApplication, packagedBootClassLoader, runnerArguments)\n    }\n  }\n\n  fun stop() {\n    (runnerClassLoader as? ClassLoader)?.let(::asyncExit)\n  }\n\n  fun close() {\n    runnerClassLoader?.let(::closeRunnerClassLoader)\n    runnerClassLoader = null\n    bootClassLoader?.close()\n    bootClassLoader = null\n  }\n\n  private fun withBootClassLoader(block: (URLClassLoader) -> Unit) {\n    val originalClassLoader = Thread.currentThread().contextClassLoader\n    val packagedBootClassLoader = createBootClassLoader(originalClassLoader)\n\n    try {\n      block(packagedBootClassLoader)\n    } finally {\n      restoreContextClassLoader(originalClassLoader, packagedBootClassLoader)\n      close()\n    }\n  }\n\n  private fun createBootClassLoader(originalClassLoader: ClassLoader): URLClassLoader =\n    URLClassLoader(packagedRuntimeArtifacts.bootUrls, originalClassLoader).also {\n      bootClassLoader = it\n    }\n\n  private fun loadPackagedApplication(packagedBootClassLoader: URLClassLoader): PackagedApplication {\n    val serializedApplicationClass = loadSerializedApplicationClass(packagedBootClassLoader)\n    val serializedApplication = readSerializedApplication(serializedApplicationClass)\n    val packagedRunnerClassLoader = serializedApplication.runnerClassLoader(serializedApplicationClass)\n    runnerClassLoader = packagedRunnerClassLoader\n\n    return PackagedApplication(\n      runnerClassLoader = packagedRunnerClassLoader as ClassLoader,\n      mainClassName = serializedApplication.mainClassName(serializedApplicationClass)\n    )\n  }\n\n  private fun launchPackagedApplication(\n    packagedApplication: PackagedApplication,\n    packagedBootClassLoader: URLClassLoader,\n    runnerArguments: Array<String>\n  ) {\n    Thread.currentThread().contextClassLoader = packagedApplication.runnerClassLoader\n    setForkJoinApplicationClassLoader(packagedBootClassLoader, packagedApplication.runnerClassLoader)\n    invokeMain(packagedApplication.runnerClassLoader, packagedApplication.mainClassName, runnerArguments)\n  }\n\n  private fun restoreContextClassLoader(\n    originalClassLoader: ClassLoader,\n    packagedBootClassLoader: URLClassLoader\n  ) {\n    clearForkJoinApplicationClassLoader(packagedBootClassLoader)\n    Thread.currentThread().contextClassLoader = originalClassLoader\n  }\n\n  private fun loadSerializedApplicationClass(packagedBootClassLoader: URLClassLoader): Class<*> =\n    Class.forName(SERIALIZED_APPLICATION_CLASS_NAME, true, packagedBootClassLoader)\n\n  private fun readSerializedApplication(serializedApplicationClass: Class<*>): Any =\n    BufferedInputStream(packagedRuntimeArtifacts.applicationDat.toFile().inputStream()).use { input ->\n      serializedApplicationClass\n        .getMethod(\"read\", java.io.InputStream::class.java, Path::class.java)\n        .invoke(null, input, packagedRuntimeArtifacts.appRoot)\n    }\n}\n\n@Suppress(\"TooGenericExceptionCaught\")\nprivate fun LaunchRuntime.launchCatching(\n  runnerArguments: Array<String>,\n  startupFailure: AtomicReference<Throwable?>\n) {\n  try {\n    launch(runnerArguments)\n  } catch (error: Throwable) {\n    startupFailure.compareAndSet(null, unwrap(error))\n  }\n}\n\nprivate fun resolveClassPathEntries(): List<Path> {\n  val pathSeparator = File.pathSeparator\n  return System\n    .getProperty(\"java.class.path\")\n    .split(pathSeparator)\n    .map { Paths.get(it).toAbsolutePath().normalize() }\n}\n\nprivate fun resolvePackagedRuntimeArtifacts(buildDirectory: Path): PackagedRuntimeArtifacts? {\n  val appRoot = buildDirectory.resolve(\"quarkus-app\")\n  val applicationDat = appRoot.resolve(\"quarkus/quarkus-application.dat\")\n  val bootUrls = resolveBootUrls(appRoot.resolve(\"lib/boot\"))\n  if (!applicationDat.toFile().exists() || bootUrls.isEmpty()) return null\n\n  return PackagedRuntimeArtifacts(\n    appRoot = appRoot,\n    applicationDat = applicationDat,\n    bootUrls = bootUrls.toTypedArray()\n  )\n}\n\nprivate fun resolveBootUrls(bootDirectory: Path): List<URL> =\n  bootDirectory\n    .toFile()\n    .listFiles { file -> file.extension == \"jar\" }\n    ?.sortedBy { it.name }\n    ?.map { it.toURI().toURL() }\n    .orEmpty()\n\nprivate fun Any.runnerClassLoader(serializedApplicationClass: Class<*>): Any =\n  serializedApplicationClass.getMethod(\"getRunnerClassLoader\").invoke(this)\n\nprivate fun Any.mainClassName(serializedApplicationClass: Class<*>): String =\n  serializedApplicationClass.getMethod(\"getMainClass\").invoke(this) as String\n\nprivate fun invokeMain(appClassLoader: ClassLoader, mainClassName: String, runnerArguments: Array<String>) {\n  appClassLoader\n    .loadClass(mainClassName)\n    .getMethod(\"main\", Array<String>::class.java)\n    .invoke(null, runnerArguments)\n}\n\nprivate fun asyncExit(appClassLoader: ClassLoader) {\n  appClassLoader.loadClass(QUARKUS_CLASS_NAME).getMethod(\"asyncExit\").invoke(null)\n}\n\nprivate fun setForkJoinApplicationClassLoader(bootClassLoader: URLClassLoader, appClassLoader: ClassLoader) {\n  val forkJoinWorkerThreadClass = Class.forName(FORK_JOIN_WORKER_THREAD_CLASS_NAME, true, bootClassLoader)\n  forkJoinWorkerThreadClass.getMethod(\"setQuarkusAppClassloader\", ClassLoader::class.java).invoke(null, appClassLoader)\n}\n\nprivate fun clearForkJoinApplicationClassLoader(bootClassLoader: URLClassLoader) {\n  val forkJoinWorkerThreadClass = Class.forName(FORK_JOIN_WORKER_THREAD_CLASS_NAME, true, bootClassLoader)\n  forkJoinWorkerThreadClass.getMethod(\"setQuarkusAppClassloader\", ClassLoader::class.java).invoke(null, null)\n}\n\nprivate fun closeRunnerClassLoader(runnerClassLoader: Any) {\n  runnerClassLoader.javaClass.getMethod(\"close\").invoke(runnerClassLoader)\n}\n\nprivate fun unwrap(error: Throwable): Throwable = when (error) {\n  is InvocationTargetException -> error.targetException ?: error\n  else -> error.cause?.takeIf { error is RuntimeException && error.message == null } ?: error\n}\n\nprivate const val DEFAULT_HTTP_PORT = 8080\nprivate const val DEFAULT_STARTUP_TIMEOUT_MS = 120_000L\nprivate const val SHUTDOWN_TIMEOUT_MS = 10_000L\nprivate const val POLL_INTERVAL_MS = 250L\nprivate const val CONNECTION_TIMEOUT_MS = 500L\nprivate const val DEFAULT_READY_SIGNAL_PROPERTY = \"stove.quarkus.ready\"\nprivate const val READY_SIGNAL_VALUE = \"true\"\nprivate const val STARTUP_TIMEOUT_PROPERTY = \"stove.quarkus.startup.timeout.ms\"\nprivate const val FORK_JOIN_WORKER_THREAD_CLASS_NAME = \"io.quarkus.bootstrap.forkjoin.QuarkusForkJoinWorkerThread\"\nprivate const val SERIALIZED_APPLICATION_CLASS_NAME = \"io.quarkus.bootstrap.runner.SerializedApplication\"\nprivate const val QUARKUS_CLASS_NAME = \"io.quarkus.runtime.Quarkus\"\n"
  },
  {
    "path": "starters/spring/stove-spring/api/stove-spring-common.api",
    "content": "public final class com/trendyol/stove/testing/e2e/BridgeSystemKt {\n\tpublic static final fun bridge-hQma78k (Lcom/trendyol/stove/testing/e2e/system/TestSystem;)Lcom/trendyol/stove/testing/e2e/system/TestSystem;\n}\n\npublic final class com/trendyol/stove/testing/e2e/SpringApplicationUnderTest : com/trendyol/stove/testing/e2e/system/abstractions/ApplicationUnderTest {\n\tpublic static final field Companion Lcom/trendyol/stove/testing/e2e/SpringApplicationUnderTest$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function1;Ljava/util/List;)V\n\tpublic fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/testing/e2e/SpringApplicationUnderTest$Companion {\n}\n\npublic final class com/trendyol/stove/testing/e2e/SpringApplicationUnderTestKt {\n\tpublic static final fun springBoot-FMzRXaI (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function1;Ljava/util/List;)Lcom/trendyol/stove/testing/e2e/system/abstractions/ReadyTestSystem;\n\tpublic static synthetic fun springBoot-FMzRXaI$default (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function1;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/system/abstractions/ReadyTestSystem;\n}\n\npublic final class com/trendyol/stove/testing/e2e/SpringBridgeSystem : com/trendyol/stove/testing/e2e/system/BridgeSystem, com/trendyol/stove/testing/e2e/system/abstractions/AfterRunAwareWithContext, com/trendyol/stove/testing/e2e/system/abstractions/PluggedSystem {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;)V\n\tpublic fun get (Lkotlin/reflect/KClass;)Ljava/lang/Object;\n\tpublic fun getTestSystem ()Lcom/trendyol/stove/testing/e2e/system/TestSystem;\n}\n\n"
  },
  {
    "path": "starters/spring/stove-spring/api/stove-spring.api",
    "content": "public final class com/trendyol/stove/spring/BridgeSystemKt {\n\tpublic static final fun bridge-IDauA90 (Lcom/trendyol/stove/system/Stove;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/spring/RegistrarKt {\n\tpublic static final fun addTestDependencies (Lorg/springframework/boot/SpringApplication;Lkotlin/jvm/functions/Function1;)V\n\tpublic static final fun addTestDependencies4x (Lorg/springframework/boot/SpringApplication;Lkotlin/jvm/functions/Function1;)V\n\tpublic static final fun stoveSpring4xRegistrar (Lkotlin/jvm/functions/Function1;)Lorg/springframework/context/ApplicationContextInitializer;\n\tpublic static final fun stoveSpringRegistrar (Lkotlin/jvm/functions/Function1;)Lorg/springframework/context/ApplicationContextInitializer;\n}\n\npublic final class com/trendyol/stove/spring/SpringApplicationUnderTest : com/trendyol/stove/system/abstractions/ApplicationUnderTest {\n\tpublic static final field Companion Lcom/trendyol/stove/spring/SpringApplicationUnderTest$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;)V\n\tpublic fun start (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic final class com/trendyol/stove/spring/SpringApplicationUnderTest$Companion {\n}\n\npublic final class com/trendyol/stove/spring/SpringApplicationUnderTestKt {\n\tpublic static final fun springBoot-SscbJ7Y (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n\tpublic static synthetic fun springBoot-SscbJ7Y$default (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function1;Ljava/util/List;ILjava/lang/Object;)Lcom/trendyol/stove/system/abstractions/ReadyStove;\n}\n\npublic final class com/trendyol/stove/spring/SpringBridgeSystem : com/trendyol/stove/system/BridgeSystem, com/trendyol/stove/system/abstractions/AfterRunAwareWithContext, com/trendyol/stove/system/abstractions/PluggedSystem {\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;)V\n\tpublic fun get (Lkotlin/reflect/KClass;)Ljava/lang/Object;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n}\n\n"
  },
  {
    "path": "starters/spring/stove-spring/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  // Both Spring versions as compileOnly - users bring the actual version at runtime\n  compileOnly(libs.spring.boot)\n  compileOnly(libs.spring.boot.four)\n}\n\ndependencies {\n  testImplementation(libs.kotest.runner.junit5)\n  testImplementation(libs.mockito.kotlin)\n  testImplementation(libs.spring.boot)\n}\n"
  },
  {
    "path": "starters/spring/stove-spring/src/main/kotlin/com/trendyol/stove/spring/BridgeSystem.kt",
    "content": "package com.trendyol.stove.spring\n\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.springframework.context.ApplicationContext\nimport kotlin.reflect.KClass\n\n/**\n * A system that provides a bridge between the test system and the application context.\n *\n * @property stove the test system to bridge.\n */\n@StoveDsl\nclass SpringBridgeSystem(\n  override val stove: Stove\n) : BridgeSystem<ApplicationContext>(stove),\n  PluggedSystem,\n  AfterRunAwareWithContext<ApplicationContext> {\n  override fun <D : Any> get(klass: KClass<D>): D = ctx.getBean(klass.java)\n}\n\n/**\n * Returns the bridge system associated with the test system.\n *\n * @receiver the test system.\n * @return the bridge system.\n * @throws SystemNotRegisteredException if the bridge system is not registered.\n */\nfun WithDsl.bridge(): Stove = this.stove.withBridgeSystem(SpringBridgeSystem(this.stove))\n"
  },
  {
    "path": "starters/spring/stove-spring/src/main/kotlin/com/trendyol/stove/spring/SpringApplicationUnderTest.kt",
    "content": "@file:Suppress(\"UNCHECKED_CAST\")\n\npackage com.trendyol.stove.spring\n\nimport com.trendyol.stove.system.Runner\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.WithDsl\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport kotlinx.coroutines.*\nimport org.springframework.context.ConfigurableApplicationContext\n\ninternal fun Stove.systemUnderTest(\n  runner: Runner<ConfigurableApplicationContext>,\n  withParameters: List<String> = listOf()\n): ReadyStove {\n  this.applicationUnderTest(SpringApplicationUnderTest(this, runner, withParameters))\n  return this\n}\n\nfun WithDsl.springBoot(\n  runner: Runner<ConfigurableApplicationContext>,\n  withParameters: List<String> = listOf()\n): ReadyStove {\n  SpringBootVersionCheck.ensureSpringBootAvailable()\n  return this.stove.systemUnderTest(runner, withParameters)\n}\n\n@StoveDsl\nclass SpringApplicationUnderTest(\n  private val stove: Stove,\n  private val runner: Runner<ConfigurableApplicationContext>,\n  private val parameters: List<String>\n) : ApplicationUnderTest<ConfigurableApplicationContext> {\n  private lateinit var application: ConfigurableApplicationContext\n\n  companion object {\n    private const val DELAY = 500L\n  }\n\n  override suspend fun start(configurations: List<String>): ConfigurableApplicationContext =\n    coroutineScope {\n      val allConfigurations = (configurations + defaultConfigurations() + parameters).map { \"--$it\" }.toTypedArray()\n      application = runner(allConfigurations)\n      while (!application.isRunning || !application.isActive) {\n        delay(DELAY)\n        continue\n      }\n      stove.systemsOf<AfterRunAwareWithContext<ConfigurableApplicationContext>>()\n        .map { async(context = Dispatchers.IO) { it.afterRun(application) } }\n        .awaitAll()\n      application\n    }\n\n  override suspend fun stop(): Unit = application.stop()\n\n  private fun defaultConfigurations(): Array<String> = arrayOf(\"test-system=true\")\n}\n"
  },
  {
    "path": "starters/spring/stove-spring/src/main/kotlin/com/trendyol/stove/spring/SpringBootVersionCheck.kt",
    "content": "@file:Suppress(\"TooGenericExceptionCaught\", \"SwallowedException\")\n\npackage com.trendyol.stove.spring\n\n/**\n * Utility object to check Spring Boot availability and version at runtime.\n * Since Spring Boot is a `compileOnly` dependency, users must bring their own version.\n */\ninternal object SpringBootVersionCheck {\n  private const val SPRING_APPLICATION_CLASS = \"org.springframework.boot.SpringApplication\"\n  private const val SPRING_BOOT_VERSION_CLASS = \"org.springframework.boot.SpringBootVersion\"\n\n  /**\n   * Checks if Spring Boot is available on the classpath.\n   * @throws IllegalStateException if Spring Boot is not found\n   */\n  fun ensureSpringBootAvailable() {\n    try {\n      Class.forName(SPRING_APPLICATION_CLASS)\n    } catch (e: ClassNotFoundException) {\n      throw IllegalStateException(\n        \"\"\"\n        |\n        |═══════════════════════════════════════════════════════════════════════════════\n        |  Spring Boot Not Found on Classpath!\n        |═══════════════════════════════════════════════════════════════════════════════\n        |\n        |  stove-spring-testing-e2e requires Spring Boot to be on your classpath.\n        |  Spring Boot is declared as a 'compileOnly' dependency, so you must add it\n        |  to your project.\n        |\n        |  Add one of the following to your build.gradle.kts:\n        |\n        |  For Spring Boot 2.x:\n        |    testImplementation(\"org.springframework.boot:spring-boot-starter:2.7.x\")\n        |\n        |  For Spring Boot 3.x:\n        |    testImplementation(\"org.springframework.boot:spring-boot-starter:3.x.x\")\n        |\n        |  For Spring Boot 4.x:\n        |    testImplementation(\"org.springframework.boot:spring-boot-starter:4.x.x\")\n        |\n        |═══════════════════════════════════════════════════════════════════════════════\n        \"\"\".trimMargin(),\n        e\n      )\n    }\n  }\n\n  /**\n   * Gets the Spring Boot version if available.\n   * @return the Spring Boot version string, or \"unknown\" if not determinable\n   */\n  fun getSpringBootVersion(): String = try {\n    val versionClass = Class.forName(SPRING_BOOT_VERSION_CLASS)\n    val getVersionMethod = versionClass.getMethod(\"getVersion\")\n    getVersionMethod.invoke(null) as? String ?: \"unknown\"\n  } catch (_: Exception) {\n    \"unknown\"\n  }\n\n  /**\n   * Gets the major version of Spring Boot.\n   * @return the major version (2, 3, 4, etc.) or -1 if not determinable\n   */\n  fun getSpringBootMajorVersion(): Int = try {\n    val version = getSpringBootVersion()\n    version.split(\".\").firstOrNull()?.toIntOrNull() ?: -1\n  } catch (e: Exception) {\n    -1\n  }\n}\n"
  },
  {
    "path": "starters/spring/stove-spring/src/main/kotlin/com/trendyol/stove/spring/registrar.kt",
    "content": "@file:Suppress(\"DEPRECATION\")\n\npackage com.trendyol.stove.spring\n\nimport org.springframework.beans.factory.BeanRegistrarDsl\nimport org.springframework.boot.SpringApplication\nimport org.springframework.context.ApplicationContextInitializer\nimport org.springframework.context.support.*\n\n// =============================================================================\n// Spring Boot 3.x (uses BeanDefinitionDsl - deprecated but still works)\n// =============================================================================\n\n/**\n * Creates an [ApplicationContextInitializer] that registers beans using the [BeanDefinitionDsl].\n *\n * **For Spring Boot 3.x applications.**\n *\n * Example usage:\n * ```kotlin\n * TestAppRunner.run(params) {\n *   addInitializers(\n *     stoveSpringRegistrar {\n *       bean<MyService>()\n *       bean<MyRepository> { MyRepositoryImpl() }\n *     }\n *   )\n * }\n * ```\n *\n * @param registration A lambda with [BeanDefinitionDsl] receiver to define beans.\n * @return An [ApplicationContextInitializer] that can be added to a [SpringApplication].\n */\nfun stoveSpringRegistrar(\n  registration: BeanDefinitionDsl.() -> Unit\n): ApplicationContextInitializer<GenericApplicationContext> = ApplicationContextInitializer { context ->\n  val beansDsl = beans(registration)\n  beansDsl.initialize(context)\n}\n\n/**\n * Extension function to easily add test dependencies to a [SpringApplication].\n *\n * **For Spring Boot 3.x applications.**\n *\n * Example usage:\n * ```kotlin\n * TestAppRunner.run(params) {\n *   addTestDependencies {\n *     bean<MyService>()\n *     bean<MyRepository> { MyRepositoryImpl() }\n *   }\n * }\n * ```\n *\n * @param registration A lambda with [BeanDefinitionDsl] receiver to define beans.\n */\nfun SpringApplication.addTestDependencies(\n  registration: BeanDefinitionDsl.() -> Unit\n): Unit = this.addInitializers(stoveSpringRegistrar(registration))\n\n// =============================================================================\n// Spring Boot 4.x (uses BeanRegistrarDsl - the new recommended approach)\n// =============================================================================\n\n/**\n * Creates an [ApplicationContextInitializer] that registers beans using the [BeanRegistrarDsl].\n *\n * **For Spring Boot 4.x applications.**\n *\n * Example usage:\n * ```kotlin\n * TestAppRunner.run(params) {\n *   addInitializers(\n *     stoveSpring4xRegistrar {\n *       registerBean<MyService>()\n *       registerBean<MyRepository> { MyRepositoryImpl() }\n *     }\n *   )\n * }\n * ```\n *\n * @param registration A lambda with [BeanRegistrarDsl] receiver to define beans.\n * @return An [ApplicationContextInitializer] that can be added to a [SpringApplication].\n */\nfun stoveSpring4xRegistrar(\n  registration: BeanRegistrarDsl.() -> Unit\n): ApplicationContextInitializer<*> = ApplicationContextInitializer<GenericApplicationContext> { context ->\n  context.register(BeanRegistrarDsl(registration))\n}\n\n/**\n * Extension function to easily add test dependencies to a [SpringApplication].\n *\n * **For Spring Boot 4.x applications.**\n *\n * Example usage:\n * ```kotlin\n * TestAppRunner.run(params) {\n *   addTestDependencies4x {\n *     registerBean<MyService>()\n *     registerBean<MyRepository> { MyRepositoryImpl() }\n *   }\n * }\n * ```\n *\n * @param registration A lambda with [BeanRegistrarDsl] receiver to define beans.\n */\nfun SpringApplication.addTestDependencies4x(\n  registration: BeanRegistrarDsl.() -> Unit\n): Unit = this.addInitializers(stoveSpring4xRegistrar(registration))\n"
  },
  {
    "path": "starters/spring/stove-spring/src/test/kotlin/com/trendyol/stove/SpringApplicationUnderTestTests.kt",
    "content": "package com.trendyol.stove\n\nimport com.trendyol.stove.spring.SpringApplicationUnderTest\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.shouldContain\nimport io.kotest.matchers.shouldBe\nimport org.mockito.kotlin.*\nimport org.springframework.context.ConfigurableApplicationContext\n\nclass SpringApplicationUnderTestTests :\n  FunSpec({\n\n    test(\"should include default test-system configuration\") {\n      val testSystem = Stove()\n      var capturedArgs: Array<String> = emptyArray()\n\n      val runner: (Array<String>) -> ConfigurableApplicationContext = { args ->\n        capturedArgs = args\n        mock<ConfigurableApplicationContext> {\n          on { isRunning } doReturn true\n          on { isActive } doReturn true\n        }\n      }\n\n      val applicationUnderTest = SpringApplicationUnderTest(\n        stove = testSystem,\n        runner = runner,\n        parameters = listOf()\n      )\n\n      applicationUnderTest.start(listOf())\n\n      capturedArgs.toList().shouldContain(\"--test-system=true\")\n    }\n\n    test(\"should include custom parameters\") {\n      val testSystem = Stove()\n      var capturedArgs: Array<String> = emptyArray()\n\n      val runner: (Array<String>) -> ConfigurableApplicationContext = { args ->\n        capturedArgs = args\n        mock<ConfigurableApplicationContext> {\n          on { isRunning } doReturn true\n          on { isActive } doReturn true\n        }\n      }\n\n      val applicationUnderTest = SpringApplicationUnderTest(\n        stove = testSystem,\n        runner = runner,\n        parameters = listOf(\"custom.param=value\")\n      )\n\n      applicationUnderTest.start(listOf())\n\n      capturedArgs.toList().shouldContain(\"--custom.param=value\")\n    }\n\n    test(\"should include provided configurations\") {\n      val testSystem = Stove()\n      var capturedArgs: Array<String> = emptyArray()\n\n      val runner: (Array<String>) -> ConfigurableApplicationContext = { args ->\n        capturedArgs = args\n        mock<ConfigurableApplicationContext> {\n          on { isRunning } doReturn true\n          on { isActive } doReturn true\n        }\n      }\n\n      val applicationUnderTest = SpringApplicationUnderTest(\n        stove = testSystem,\n        runner = runner,\n        parameters = listOf()\n      )\n\n      applicationUnderTest.start(listOf(\"server.port=8080\", \"spring.profiles.active=test\"))\n\n      capturedArgs.toList().shouldContain(\"--server.port=8080\")\n      capturedArgs.toList().shouldContain(\"--spring.profiles.active=test\")\n    }\n\n    test(\"should combine all configurations with -- prefix\") {\n      val testSystem = Stove()\n      var capturedArgs: Array<String> = emptyArray()\n\n      val runner: (Array<String>) -> ConfigurableApplicationContext = { args ->\n        capturedArgs = args\n        mock<ConfigurableApplicationContext> {\n          on { isRunning } doReturn true\n          on { isActive } doReturn true\n        }\n      }\n\n      val applicationUnderTest = SpringApplicationUnderTest(\n        stove = testSystem,\n        runner = runner,\n        parameters = listOf(\"param1=val1\")\n      )\n\n      applicationUnderTest.start(listOf(\"config1=val1\"))\n\n      capturedArgs.all { it.startsWith(\"--\") } shouldBe true\n    }\n\n    test(\"should stop application context\") {\n      val mockContext = mock<ConfigurableApplicationContext> {\n        on { isRunning } doReturn true\n        on { isActive } doReturn true\n      }\n      val testSystem = Stove()\n\n      val runner: (Array<String>) -> ConfigurableApplicationContext = { mockContext }\n\n      val applicationUnderTest = SpringApplicationUnderTest(\n        stove = testSystem,\n        runner = runner,\n        parameters = listOf()\n      )\n\n      applicationUnderTest.start(listOf())\n      applicationUnderTest.stop()\n\n      verify(mockContext).stop()\n    }\n  })\n"
  },
  {
    "path": "starters/spring/stove-spring/src/test/kotlin/com/trendyol/stove/SpringBridgeSystemTests.kt",
    "content": "package com.trendyol.stove\n\nimport com.trendyol.stove.spring.SpringBridgeSystem\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.types.shouldBeInstanceOf\nimport org.mockito.kotlin.*\nimport org.springframework.context.ApplicationContext\n\nclass SpringBridgeSystemTests :\n  FunSpec({\n\n    test(\"SpringBridgeSystem should return bean from application context\") {\n      val testSystem = Stove()\n      val bridgeSystem = SpringBridgeSystem(testSystem)\n      val mockContext = mock<ApplicationContext>()\n\n      val testBean = TestBean(\"test-value\")\n      whenever(mockContext.getBean(TestBean::class.java)).thenReturn(testBean)\n\n      // Set the context via reflection since afterRun is protected\n      val ctxField = bridgeSystem.javaClass.superclass.getDeclaredField(\"ctx\")\n      ctxField.isAccessible = true\n      ctxField.set(bridgeSystem, mockContext)\n\n      val result = bridgeSystem.get(TestBean::class)\n\n      result shouldBe testBean\n      verify(mockContext).getBean(TestBean::class.java)\n    }\n\n    test(\"SpringBridgeSystem should be associated with test system\") {\n      val testSystem = Stove()\n      val bridgeSystem = SpringBridgeSystem(testSystem)\n\n      bridgeSystem.stove shouldBe testSystem\n    }\n\n    test(\"SpringBridgeSystem should implement required interfaces\") {\n      val testSystem = Stove()\n      val bridgeSystem = SpringBridgeSystem(testSystem)\n\n      bridgeSystem.shouldBeInstanceOf<SpringBridgeSystem>()\n    }\n  })\n\ndata class TestBean(\n  val value: String\n)\n"
  },
  {
    "path": "starters/spring/stove-spring/src/test/kotlin/com/trendyol/stove/spring/SpringBootVersionCheckTest.kt",
    "content": "package com.trendyol.stove.spring\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.ints.shouldBeGreaterThanOrEqual\nimport io.kotest.matchers.shouldNotBe\n\nclass SpringBootVersionCheckTest :\n  FunSpec({\n    test(\"ensureSpringBootAvailable should not throw when Spring Boot is on classpath\") {\n      SpringBootVersionCheck.ensureSpringBootAvailable()\n    }\n\n    test(\"getSpringBootVersion should return a non-blank value\") {\n      SpringBootVersionCheck.getSpringBootVersion().shouldNotBe(\"unknown\")\n    }\n\n    test(\"getSpringBootMajorVersion should parse major version\") {\n      SpringBootVersionCheck.getSpringBootMajorVersion().shouldBeGreaterThanOrEqual(2)\n    }\n  })\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/api/stove-spring-kafka-common.api",
    "content": "public final class com/trendyol/stove/testing/e2e/kafka/Caching {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/testing/e2e/kafka/Caching;\n\tpublic final fun of ()Lcom/github/benmanes/caffeine/cache/Cache;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lorg/apache/kafka/common/serialization/Serializer;Lorg/apache/kafka/common/serialization/Serializer;)V\n\tpublic synthetic fun <init> (Lorg/apache/kafka/common/serialization/Serializer;Lorg/apache/kafka/common/serialization/Serializer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lorg/apache/kafka/common/serialization/Serializer;\n\tpublic final fun component2 ()Lorg/apache/kafka/common/serialization/Serializer;\n\tpublic final fun copy (Lorg/apache/kafka/common/serialization/Serializer;Lorg/apache/kafka/common/serialization/Serializer;)Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;Lorg/apache/kafka/common/serialization/Serializer;Lorg/apache/kafka/common/serialization/Serializer;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getKeySerializer ()Lorg/apache/kafka/common/serialization/Serializer;\n\tpublic final fun getValueSerializer ()Lorg/apache/kafka/common/serialization/Serializer;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/KafkaContainerOptions : com/trendyol/stove/testing/e2e/containers/ContainerOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/e2e/kafka/KafkaContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/KafkaContext {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/e2e/system/abstractions/SystemRuntime;Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/testing/e2e/system/abstractions/SystemRuntime;\n\tpublic final fun component2 ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;\n\tpublic final fun copy (Lcom/trendyol/stove/testing/e2e/system/abstractions/SystemRuntime;Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/e2e/kafka/KafkaContext;Lcom/trendyol/stove/testing/e2e/system/abstractions/SystemRuntime;Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getOptions ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/testing/e2e/system/abstractions/SystemRuntime;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract interface annotation class com/trendyol/stove/testing/e2e/kafka/KafkaDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/KafkaExposedConfiguration : com/trendyol/stove/testing/e2e/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/e2e/kafka/KafkaExposedConfiguration;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBootstrapServers ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/KafkaMigrationContext {\n\tpublic fun <init> (Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;)V\n\tpublic final fun component1 ()Lorg/apache/kafka/clients/admin/Admin;\n\tpublic final fun component2 ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;\n\tpublic final fun copy (Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaMigrationContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/e2e/kafka/KafkaMigrationContext;Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaMigrationContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAdmin ()Lorg/apache/kafka/clients/admin/Admin;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/KafkaOps {\n\tpublic fun <init> (Lkotlin/jvm/functions/Function3;)V\n\tpublic final fun component1 ()Lkotlin/jvm/functions/Function3;\n\tpublic final fun copy (Lkotlin/jvm/functions/Function3;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getSend ()Lkotlin/jvm/functions/Function3;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/KafkaSystem : com/trendyol/stove/testing/e2e/system/abstractions/ExposesConfiguration, com/trendyol/stove/testing/e2e/system/abstractions/PluggedSystem, com/trendyol/stove/testing/e2e/system/abstractions/RunnableSystemWithContext {\n\tpublic static final field Companion Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystem$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lcom/trendyol/stove/testing/e2e/kafka/KafkaContext;)V\n\tpublic final fun adminOperations (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic synthetic fun afterRun (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun afterRun (Lorg/springframework/context/ApplicationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun beforeRun (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun getGetInterceptor ()Lkotlin/jvm/functions/Function0;\n\tpublic fun getTestSystem ()Lcom/trendyol/stove/testing/e2e/system/TestSystem;\n\tpublic final fun pause ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystem;\n\tpublic final fun publish (Ljava/lang/String;Ljava/lang/Object;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun publish$default (Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystem;Ljava/lang/String;Ljava/lang/Object;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldBeConsumedInternal-dWUq8MI (Lkotlin/reflect/KClass;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldBeFailedInternal-dWUq8MI (Lkotlin/reflect/KClass;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldBePublishedInternal-dWUq8MI (Lkotlin/reflect/KClass;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/testing/e2e/system/TestSystem;\n\tpublic final fun unpause ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystem;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/KafkaSystem$Companion {\n\tpublic final fun kafkaTemplate (Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystem;)Lorg/springframework/kafka/core/KafkaTemplate;\n}\n\npublic class com/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions : com/trendyol/stove/testing/e2e/database/migrations/SupportsMigrations, com/trendyol/stove/testing/e2e/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/testing/e2e/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions$Companion;\n\tpublic fun <init> (Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/testing/e2e/kafka/KafkaContainerOptions;Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/testing/e2e/kafka/KafkaContainerOptions;Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainerOptions ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaContainerOptions;\n\tpublic fun getFallbackSerde ()Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/testing/e2e/database/migrations/MigrationCollection;\n\tpublic fun getOps ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;\n\tpublic fun getPorts ()Ljava/util/List;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/testing/e2e/database/migrations/SupportsMigrations;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions$Companion {\n\tpublic final fun getDEFAULT_KAFKA_PORTS ()Ljava/util/List;\n\tpublic final fun provided (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/testing/e2e/kafka/ProvidedKafkaSystemOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions$Companion;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/testing/e2e/kafka/ProvidedKafkaSystemOptions;\n}\n\npublic abstract interface class com/trendyol/stove/testing/e2e/kafka/MessageProperties {\n\tpublic abstract fun getKey ()Ljava/lang/String;\n\tpublic abstract fun getMetadata ()Lcom/trendyol/stove/testing/e2e/messaging/MessageMetadata;\n\tpublic abstract fun getPartition ()Ljava/lang/Integer;\n\tpublic abstract fun getTimestamp ()Ljava/lang/Long;\n\tpublic abstract fun getTopic ()Ljava/lang/String;\n\tpublic abstract fun getValue ()[B\n\tpublic abstract fun getValueAsString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/OptionsKt {\n\tpublic static final fun kafka-E6EcY7A (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun kafka-PmNtuJU (Lcom/trendyol/stove/testing/e2e/system/TestSystem;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/testing/e2e/system/TestSystem;\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/ProvidedKafkaSystemOptions : com/trendyol/stove/testing/e2e/kafka/KafkaSystemOptions, com/trendyol/stove/testing/e2e/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/e2e/kafka/KafkaExposedConfiguration;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/testing/e2e/kafka/KafkaExposedConfiguration;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/testing/e2e/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/testing/e2e/kafka/KafkaOps;Lkotlin/jvm/functions/Function2;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/testing/e2e/kafka/KafkaExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/testing/e2e/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic class com/trendyol/stove/testing/e2e/kafka/StoveKafkaContainer : org/testcontainers/kafka/ConfluentKafkaContainer, com/trendyol/stove/testing/e2e/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/testing/e2e/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/testing/e2e/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\npublic final class com/trendyol/stove/testing/e2e/kafka/TestSystemKafkaInterceptor : org/springframework/kafka/listener/CompositeRecordInterceptor, org/springframework/kafka/support/ProducerListener {\n\tpublic fun <init> (Lcom/trendyol/stove/testing/e2e/serialization/StoveSerde;)V\n\tpublic fun failure (Lorg/apache/kafka/clients/consumer/ConsumerRecord;Ljava/lang/Exception;Lorg/apache/kafka/clients/consumer/Consumer;)V\n\tpublic fun onError (Lorg/apache/kafka/clients/producer/ProducerRecord;Lorg/apache/kafka/clients/producer/RecordMetadata;Ljava/lang/Exception;)V\n\tpublic fun onSuccess (Lorg/apache/kafka/clients/producer/ProducerRecord;Lorg/apache/kafka/clients/producer/RecordMetadata;)V\n\tpublic fun success (Lorg/apache/kafka/clients/consumer/ConsumerRecord;Lorg/apache/kafka/clients/consumer/Consumer;)V\n}\n\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/api/stove-spring-kafka.api",
    "content": "public final class com/trendyol/stove/kafka/Caching {\n\tpublic static final field INSTANCE Lcom/trendyol/stove/kafka/Caching;\n\tpublic final fun of ()Lcom/github/benmanes/caffeine/cache/Cache;\n}\n\npublic final class com/trendyol/stove/kafka/FallbackTemplateSerde {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Lorg/apache/kafka/common/serialization/Serializer;Lorg/apache/kafka/common/serialization/Serializer;)V\n\tpublic synthetic fun <init> (Lorg/apache/kafka/common/serialization/Serializer;Lorg/apache/kafka/common/serialization/Serializer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Lorg/apache/kafka/common/serialization/Serializer;\n\tpublic final fun component2 ()Lorg/apache/kafka/common/serialization/Serializer;\n\tpublic final fun copy (Lorg/apache/kafka/common/serialization/Serializer;Lorg/apache/kafka/common/serialization/Serializer;)Lcom/trendyol/stove/kafka/FallbackTemplateSerde;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/FallbackTemplateSerde;Lorg/apache/kafka/common/serialization/Serializer;Lorg/apache/kafka/common/serialization/Serializer;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/FallbackTemplateSerde;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getKeySerializer ()Lorg/apache/kafka/common/serialization/Serializer;\n\tpublic final fun getValueSerializer ()Lorg/apache/kafka/common/serialization/Serializer;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaContainerOptions : com/trendyol/stove/containers/ContainerOptions {\n\tpublic fun <init> ()V\n\tpublic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun component2 ()Ljava/lang/String;\n\tpublic final fun component3 ()Ljava/lang/String;\n\tpublic final fun component4 ()Ljava/lang/String;\n\tpublic final fun component5 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun component6 ()Lkotlin/jvm/functions/Function1;\n\tpublic final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/kafka/KafkaContainerOptions;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/KafkaContainerOptions;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/KafkaContainerOptions;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic fun getCompatibleSubstitute ()Ljava/lang/String;\n\tpublic fun getContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getImage ()Ljava/lang/String;\n\tpublic fun getImageWithTag ()Ljava/lang/String;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic fun getTag ()Ljava/lang/String;\n\tpublic fun getUseContainerFn ()Lkotlin/jvm/functions/Function1;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaContext {\n\tpublic fun <init> (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/kafka/KafkaSystemOptions;)V\n\tpublic final fun component1 ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic final fun component2 ()Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n\tpublic final fun copy (Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/kafka/KafkaSystemOptions;)Lcom/trendyol/stove/kafka/KafkaContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/KafkaContext;Lcom/trendyol/stove/system/abstractions/SystemRuntime;Lcom/trendyol/stove/kafka/KafkaSystemOptions;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/KafkaContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getOptions ()Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n\tpublic final fun getRuntime ()Lcom/trendyol/stove/system/abstractions/SystemRuntime;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic abstract interface annotation class com/trendyol/stove/kafka/KafkaDsl : java/lang/annotation/Annotation {\n}\n\npublic final class com/trendyol/stove/kafka/KafkaExposedConfiguration : com/trendyol/stove/system/abstractions/ExposedConfiguration {\n\tpublic fun <init> (Ljava/lang/String;)V\n\tpublic final fun component1 ()Ljava/lang/String;\n\tpublic final fun copy (Ljava/lang/String;)Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;Ljava/lang/String;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getBootstrapServers ()Ljava/lang/String;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaMigrationContext {\n\tpublic fun <init> (Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/kafka/KafkaSystemOptions;)V\n\tpublic final fun component1 ()Lorg/apache/kafka/clients/admin/Admin;\n\tpublic final fun component2 ()Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n\tpublic final fun copy (Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/kafka/KafkaSystemOptions;)Lcom/trendyol/stove/kafka/KafkaMigrationContext;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/KafkaMigrationContext;Lorg/apache/kafka/clients/admin/Admin;Lcom/trendyol/stove/kafka/KafkaSystemOptions;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/KafkaMigrationContext;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getAdmin ()Lorg/apache/kafka/clients/admin/Admin;\n\tpublic final fun getOptions ()Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaOps {\n\tpublic fun <init> (Lkotlin/jvm/functions/Function3;)V\n\tpublic final fun component1 ()Lkotlin/jvm/functions/Function3;\n\tpublic final fun copy (Lkotlin/jvm/functions/Function3;)Lcom/trendyol/stove/kafka/KafkaOps;\n\tpublic static synthetic fun copy$default (Lcom/trendyol/stove/kafka/KafkaOps;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/KafkaOps;\n\tpublic fun equals (Ljava/lang/Object;)Z\n\tpublic final fun getSend ()Lkotlin/jvm/functions/Function3;\n\tpublic fun hashCode ()I\n\tpublic fun toString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaSystem : com/trendyol/stove/reporting/Reports, com/trendyol/stove/system/abstractions/ExposesConfiguration, com/trendyol/stove/system/abstractions/PluggedSystem, com/trendyol/stove/system/abstractions/RunnableSystemWithContext {\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/KafkaSystem$Companion;\n\tpublic fun <init> (Lcom/trendyol/stove/system/Stove;Lcom/trendyol/stove/kafka/KafkaContext;)V\n\tpublic final fun adminOperations (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic synthetic fun afterRun (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun afterRun (Lorg/springframework/context/ApplicationContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun assertKafkaMessage-WPi__2c (Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun beforeRun (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun close ()V\n\tpublic fun configuration ()Ljava/util/List;\n\tpublic fun executeWithReuseCheck (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun getGetInterceptor ()Lkotlin/jvm/functions/Function0;\n\tpublic fun getReportSystemName ()Ljava/lang/String;\n\tpublic fun getReporter ()Lcom/trendyol/stove/reporting/StoveReporter;\n\tpublic fun getStove ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun pause ()Lcom/trendyol/stove/kafka/KafkaSystem;\n\tpublic final fun publish (Ljava/lang/String;Ljava/lang/Object;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static synthetic fun publish$default (Lcom/trendyol/stove/kafka/KafkaSystem;Ljava/lang/String;Ljava/lang/Object;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;\n\tpublic fun report (Ljava/lang/String;Larrow/core/Option;Larrow/core/Option;Ljava/util/Map;Larrow/core/Option;Larrow/core/Option;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun run (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldBeConsumedInternal-dWUq8MI (Lkotlin/reflect/KClass;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldBeFailedInternal-dWUq8MI (Lkotlin/reflect/KClass;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic final fun shouldBePublishedInternal-dWUq8MI (Lkotlin/reflect/KClass;JLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun snapshot ()Lcom/trendyol/stove/reporting/SystemSnapshot;\n\tpublic fun stop (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic fun then ()Lcom/trendyol/stove/system/Stove;\n\tpublic final fun unpause ()Lcom/trendyol/stove/kafka/KafkaSystem;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaSystem$Companion {\n\tpublic final fun kafkaTemplate (Lcom/trendyol/stove/kafka/KafkaSystem;)Lorg/springframework/kafka/core/KafkaTemplate;\n}\n\npublic class com/trendyol/stove/kafka/KafkaSystemOptions : com/trendyol/stove/database/migrations/SupportsMigrations, com/trendyol/stove/system/abstractions/ConfiguresExposedConfiguration, com/trendyol/stove/system/abstractions/SystemOptions {\n\tpublic static final field Companion Lcom/trendyol/stove/kafka/KafkaSystemOptions$Companion;\n\tpublic fun <init> (Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/kafka/KafkaContainerOptions;Lcom/trendyol/stove/kafka/KafkaOps;Lkotlin/jvm/functions/Function2;Ljava/util/Map;Lkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/kafka/KafkaContainerOptions;Lcom/trendyol/stove/kafka/KafkaOps;Lkotlin/jvm/functions/Function2;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic fun getCleanup ()Lkotlin/jvm/functions/Function2;\n\tpublic fun getConfigureExposedConfiguration ()Lkotlin/jvm/functions/Function1;\n\tpublic fun getContainerOptions ()Lcom/trendyol/stove/kafka/KafkaContainerOptions;\n\tpublic fun getFallbackSerde ()Lcom/trendyol/stove/kafka/FallbackTemplateSerde;\n\tpublic fun getMigrationCollection ()Lcom/trendyol/stove/database/migrations/MigrationCollection;\n\tpublic fun getOps ()Lcom/trendyol/stove/kafka/KafkaOps;\n\tpublic fun getPorts ()Ljava/util/List;\n\tpublic fun getProperties ()Ljava/util/Map;\n\tpublic fun getRegistry ()Ljava/lang/String;\n\tpublic synthetic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/database/migrations/SupportsMigrations;\n\tpublic fun migrations (Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/kafka/KafkaSystemOptions;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaSystemOptions$Companion {\n\tpublic final fun getDEFAULT_KAFKA_PORTS ()Ljava/util/List;\n\tpublic final fun provided (Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/kafka/KafkaOps;Ljava/util/Map;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)Lcom/trendyol/stove/kafka/ProvidedKafkaSystemOptions;\n\tpublic static synthetic fun provided$default (Lcom/trendyol/stove/kafka/KafkaSystemOptions$Companion;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/kafka/KafkaOps;Ljava/util/Map;ZLkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/trendyol/stove/kafka/ProvidedKafkaSystemOptions;\n}\n\npublic final class com/trendyol/stove/kafka/KafkaTemplateCompatibilityKt {\n\tpublic static final fun defaultKafkaOps ()Lcom/trendyol/stove/kafka/KafkaOps;\n\tpublic static final fun sendCompatible (Lorg/springframework/kafka/core/KafkaTemplate;Lorg/apache/kafka/clients/producer/ProducerRecord;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\npublic abstract interface class com/trendyol/stove/kafka/MessageProperties {\n\tpublic abstract fun getKey ()Ljava/lang/String;\n\tpublic abstract fun getMetadata ()Lcom/trendyol/stove/messaging/MessageMetadata;\n\tpublic abstract fun getPartition ()Ljava/lang/Integer;\n\tpublic abstract fun getTimestamp ()Ljava/lang/Long;\n\tpublic abstract fun getTopic ()Ljava/lang/String;\n\tpublic abstract fun getValue ()[B\n\tpublic abstract fun getValueAsString ()Ljava/lang/String;\n}\n\npublic final class com/trendyol/stove/kafka/OptionsKt {\n\tpublic static final fun kafka-JSkDyPw (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n\tpublic static final fun kafka-ypJx7X8 (Lcom/trendyol/stove/system/Stove;Lkotlin/jvm/functions/Function0;)Lcom/trendyol/stove/system/Stove;\n}\n\npublic final class com/trendyol/stove/kafka/ProvidedKafkaSystemOptions : com/trendyol/stove/kafka/KafkaSystemOptions, com/trendyol/stove/system/abstractions/ProvidedSystemOptions {\n\tpublic fun <init> (Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/kafka/KafkaOps;Lkotlin/jvm/functions/Function2;Ljava/util/Map;ZLkotlin/jvm/functions/Function1;)V\n\tpublic synthetic fun <init> (Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;Ljava/lang/String;Ljava/util/List;Lcom/trendyol/stove/kafka/FallbackTemplateSerde;Lcom/trendyol/stove/kafka/KafkaOps;Lkotlin/jvm/functions/Function2;Ljava/util/Map;ZLkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V\n\tpublic final fun getConfig ()Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;\n\tpublic fun getProvidedConfig ()Lcom/trendyol/stove/kafka/KafkaExposedConfiguration;\n\tpublic synthetic fun getProvidedConfig ()Lcom/trendyol/stove/system/abstractions/ExposedConfiguration;\n\tpublic final fun getRunMigrations ()Z\n\tpublic fun getRunMigrationsForProvided ()Z\n}\n\npublic class com/trendyol/stove/kafka/StoveKafkaContainer : org/testcontainers/kafka/ConfluentKafkaContainer, com/trendyol/stove/containers/StoveContainer {\n\tpublic fun <init> (Lorg/testcontainers/utility/DockerImageName;)V\n\tpublic fun execCommand ([Ljava/lang/String;J)Lcom/trendyol/stove/containers/ExecResult;\n\tpublic fun getContainerIdAccess ()Ljava/lang/String;\n\tpublic fun getDockerClientAccess ()Lkotlin/Lazy;\n\tpublic fun getImageNameAccess ()Lorg/testcontainers/utility/DockerImageName;\n\tpublic fun inspect ()Lcom/trendyol/stove/containers/StoveContainerInspectInformation;\n\tpublic fun pause ()V\n\tpublic fun unpause ()V\n}\n\npublic final class com/trendyol/stove/kafka/TestSystemKafkaInterceptor : org/springframework/kafka/listener/CompositeRecordInterceptor, org/springframework/kafka/support/ProducerListener {\n\tpublic fun <init> (Lcom/trendyol/stove/serialization/StoveSerde;)V\n\tpublic fun failure (Lorg/apache/kafka/clients/consumer/ConsumerRecord;Ljava/lang/Exception;Lorg/apache/kafka/clients/consumer/Consumer;)V\n\tpublic fun onError (Lorg/apache/kafka/clients/producer/ProducerRecord;Lorg/apache/kafka/clients/producer/RecordMetadata;Ljava/lang/Exception;)V\n\tpublic fun onSuccess (Lorg/apache/kafka/clients/producer/ProducerRecord;Lorg/apache/kafka/clients/producer/RecordMetadata;)V\n\tpublic fun success (Lorg/apache/kafka/clients/consumer/ConsumerRecord;Lorg/apache/kafka/clients/consumer/Consumer;)V\n}\n\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(libs.testcontainers.kafka)\n  compileOnly(libs.spring.boot.kafka)\n  implementation(libs.caffeine)\n  implementation(libs.pprint)\n}\n\ndependencies {\n  testImplementation(libs.kotest.runner.junit5)\n}\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/Caching.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.github.benmanes.caffeine.cache.*\n\nobject Caching {\n  fun <K : Any, V : Any> of(): Cache<K, V> = Caffeine.newBuilder().build()\n}\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/Extensions.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport arrow.core.Option\nimport com.trendyol.stove.messaging.MessageMetadata\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.tracing.TraceContext\nimport org.apache.kafka.clients.consumer.ConsumerRecord\nimport org.apache.kafka.clients.producer.ProducerRecord\n\ninternal fun <K, V> ProducerRecord<K, V>.toMetadata(): MessageMetadata = MessageMetadata(\n  this.topic(),\n  this.key().toString(),\n  this.headers().associate { h -> Pair(h.key(), String(h.value())) }\n)\n\ninternal fun <K, V> ConsumerRecord<K, V>.toMetadata(): MessageMetadata = MessageMetadata(\n  this.topic(),\n  this.key().toString(),\n  this.headers().associate { h -> Pair(h.key(), String(h.value())) }\n)\n\ninternal fun <K, V> ConsumerRecord<K, V>.toStoveMessage(\n  serde: StoveSerde<Any, ByteArray>\n): StoveMessage.Consumed = StoveMessage.consumed(\n  this.topic(),\n  serializeIfNotYet(this.value(), serde),\n  this.toMetadata(),\n  this.partition(),\n  this.key()?.toString() ?: \"\",\n  this.timestamp(),\n  this.offset()\n)\n\ninternal fun <K, V> ConsumerRecord<K, V>.toFailedStoveMessage(\n  serde: StoveSerde<Any, ByteArray>,\n  exception: Exception\n): StoveMessage.Failed = StoveMessage.failed(\n  this.topic(),\n  serializeIfNotYet(this.value(), serde),\n  this.toMetadata(),\n  exception,\n  this.partition(),\n  this.key()?.toString() ?: \"\",\n  this.timestamp()\n)\n\ninternal fun <K, V> ProducerRecord<K, V>.toStoveMessage(\n  serde: StoveSerde<Any, ByteArray>\n): StoveMessage.Published = StoveMessage.published(\n  this.topic(),\n  serializeIfNotYet(this.value(), serde),\n  this.toMetadata(),\n  this.partition(),\n  this.key()?.toString() ?: \"\",\n  this.timestamp()\n)\n\ninternal fun <K, V> ProducerRecord<K, V>.toFailedStoveMessage(\n  serde: StoveSerde<Any, ByteArray>,\n  exception: Exception\n): StoveMessage.Failed = StoveMessage.failed(\n  this.topic(),\n  serializeIfNotYet(this.value(), serde),\n  this.toMetadata(),\n  exception,\n  this.partition(),\n  this.key()?.toString() ?: \"\",\n  this.timestamp()\n)\n\nprivate fun <V> serializeIfNotYet(\n  value: V,\n  serde: StoveSerde<Any, ByteArray>\n): ByteArray = when (value) {\n  is ByteArray -> value\n  else -> serde.serialize(value as Any)\n}\n\ninternal fun (MutableMap<String, String>).addTestCase(testCase: Option<String>): MutableMap<String, String> =\n  if (this.containsKey(\"testCase\")) this else testCase.map { this[\"testCase\"] = it }.let { this }\n\ninternal fun (MutableMap<String, String>).addTraceContext(\n  traceContext: TraceContext?\n): MutableMap<String, String> =\n  traceContext?.let {\n    this[TraceContext.TRACEPARENT_HEADER] = it.toTraceparent()\n    this[TraceContext.STOVE_TEST_ID_HEADER] = it.testId\n    this\n  } ?: this\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/KafkaDsl.kt",
    "content": "package com.trendyol.stove.kafka\n\n@DslMarker\n@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPEALIAS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)\nannotation class KafkaDsl\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/KafkaSystem.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport arrow.core.*\nimport com.trendyol.stove.functional.*\nimport com.trendyol.stove.messaging.*\nimport com.trendyol.stove.reporting.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.tracing.TraceContext\nimport kotlinx.coroutines.*\nimport org.apache.kafka.clients.admin.*\nimport org.apache.kafka.clients.producer.*\nimport org.apache.kafka.common.header.internals.RecordHeader\nimport org.slf4j.*\nimport org.springframework.beans.factory.*\nimport org.springframework.context.ApplicationContext\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.RecordInterceptor\nimport kotlin.reflect.KClass\nimport kotlin.time.*\nimport kotlin.time.Duration.Companion.seconds\n\n@KafkaDsl\n@Suppress(\"TooManyFunctions\", \"unused\", \"TooGenericExceptionCaught\")\nclass KafkaSystem(\n  override val stove: Stove,\n  private val context: KafkaContext\n) : PluggedSystem,\n  RunnableSystemWithContext<ApplicationContext>,\n  ExposesConfiguration,\n  Reports {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private lateinit var applicationContext: ApplicationContext\n  private lateinit var kafkaTemplate: KafkaTemplate<Any, Any>\n  private lateinit var exposedConfiguration: KafkaExposedConfiguration\n  private lateinit var admin: Admin\n  val getInterceptor: () -> TestSystemKafkaInterceptor<Any, Any> = { applicationContext.getBean() }\n\n  override fun snapshot(): SystemSnapshot {\n    val currentTestId = reporter.currentTestId()\n    val store = getInterceptor().getStore()\n    val belongsToTest: (Map<String, Any>) -> Boolean = { headers ->\n      val testId = headers[TraceContext.STOVE_TEST_ID_HEADER].toOption()\n      testId.isNone() || testId.isSome { it.toString() == currentTestId }\n    }\n\n    val consumed = store.consumedRecords().filter { belongsToTest(it.metadata.headers) }\n    val produced = store.producedRecords().filter { belongsToTest(it.metadata.headers) }\n    val failed = store.failedRecords().filter { belongsToTest(it.metadata.headers) }\n\n    return SystemSnapshot(\n      system = reportSystemName,\n      state = mapOf(\n        \"consumed\" to consumed.map { it.toReportMap() },\n        \"produced\" to produced.map { it.toReportMap() },\n        \"failed\" to failed.map { it.toReportMap() }\n      ),\n      summary = listOf(\n        \"Consumed (this test)\" to consumed.size,\n        \"Produced (this test)\" to produced.size,\n        \"Failed (this test)\" to failed.size\n      ).joinToString(\"\\n\") { (label, count) -> \"$label: $count\" }\n    )\n  }\n\n  private fun StoveMessage.Consumed.toReportMap(): Map<String, Any> = buildMap {\n    put(\"topic\", topic)\n    put(\"key\", metadata.key)\n    put(\"offset\", offset ?: 0L)\n    put(\"headers\", metadata.headers)\n    put(\"value\", String(value))\n  }\n\n  private fun StoveMessage.Published.toReportMap(): Map<String, Any> = buildMap {\n    put(\"topic\", topic)\n    put(\"key\", metadata.key)\n    put(\"headers\", metadata.headers)\n    put(\"value\", String(value))\n  }\n\n  private fun StoveMessage.Failed.toReportMap(): Map<String, Any> = buildMap {\n    put(\"topic\", topic)\n    put(\"key\", metadata.key)\n    put(\"headers\", metadata.headers)\n    put(\"reason\", reason.message ?: \"Unknown error\")\n    put(\"value\", String(value))\n  }\n\n  private val state: StateStorage<KafkaExposedConfiguration> =\n    stove.createStateStorage<KafkaExposedConfiguration, KafkaSystem>()\n\n  /**\n   * Publishes a message to the given topic.\n   * The message will be serialized using the provided serde.\n   *\n   * If the KafkaTemplate of the application is desired to be used, then [BridgeSystem] functionality can be used.\n   * For example:\n   * ```kotlin\n   * stove {\n   *   using<KafkaTemplate<Any, Any>> {\n   *      this.send(ProducerRecord(\"topic\", \"message\"))\n   *   }\n   * }\n   * ```\n   * [BridgeSystem] should be enabled while configuring the [TestSystem].\n   * @param topic The topic to publish the message to.\n   * @param message The message to publish.\n   * @param key The key of the message.\n   * @param partition The partition to publish the message to.\n   * @param headers The headers of the message.\n   * @param serde The serde to serialize the message.\n   * @param testCase The test case of the message.\n   * @return KafkaSystem\n   */\n  suspend fun publish(\n    topic: String,\n    message: Any,\n    key: Option<String> = None,\n    partition: Option<Int> = None,\n    headers: Map<String, String> = mapOf(),\n    serde: Option<StoveSerde<Any, *>> = None,\n    testCase: Option<String> = None\n  ): KafkaSystem {\n    report(\n      action = \"Publish to '$topic'\",\n      input = arrow.core.Some(message),\n      metadata = mapOf(\n        \"key\" to (key.getOrNull() ?: \"\"),\n        \"headers\" to headers,\n        \"partition\" to (partition.getOrNull()?.toString() ?: \"\")\n      )\n    ) {\n      val record = ProducerRecord<String, Any>(\n        topic,\n        partition.getOrNull(),\n        key.getOrNull(),\n        message,\n        headers\n          .toMutableMap()\n          .addTestCase(testCase)\n          .addTraceContext(TraceContext.current())\n          .map { RecordHeader(it.key, it.value.toByteArray()) }\n      )\n      context.options.ops.send(kafkaTemplate, record)\n    }\n    return this\n  }\n\n  /**\n   * Admin operations for Kafka.\n   */\n  suspend fun adminOperations(block: suspend Admin.() -> Unit) = block(admin)\n\n  /**\n   * Asserts that a message is consumed.\n   */\n  suspend inline fun <reified T : Any> shouldBeConsumed(\n    atLeastIn: Duration = 5.seconds,\n    crossinline condition: ObservedMessage<T>.() -> Boolean\n  ): KafkaSystem = assertKafkaMessage(\n    assertionName = \"shouldBeConsumed\",\n    typeName = T::class.simpleName ?: \"Unknown\",\n    timeout = atLeastIn,\n    expected = \"Message matching condition within $atLeastIn\"\n  ) { onMatch ->\n    shouldBeConsumedInternal(T::class, atLeastIn) { parsed ->\n      parsed.message.isSome { o ->\n        val result = condition(ObservedMessage(o, parsed.metadata))\n        if (result) onMatch(o)\n        result\n      }\n    }\n  }\n\n  /**\n   * Asserts that a message is failed.\n   */\n  suspend inline fun <reified T : Any> shouldBeFailed(\n    atLeastIn: Duration = 5.seconds,\n    crossinline condition: FailedObservedMessage<T>.() -> Boolean\n  ): KafkaSystem = assertKafkaMessage(\n    assertionName = \"shouldBeFailed\",\n    typeName = T::class.simpleName ?: \"Unknown\",\n    timeout = atLeastIn,\n    expected = \"Failed message matching condition within $atLeastIn\"\n  ) { onMatch ->\n    shouldBeFailedInternal(T::class, atLeastIn) { parsed ->\n      parsed as FailedParsedMessage<T>\n      parsed.message.isSome { o ->\n        val result = condition(FailedObservedMessage(o, parsed.metadata, parsed.reason))\n        if (result) onMatch(o)\n        result\n      }\n    }\n  }\n\n  /**\n   * Asserts that a message is published.\n   */\n  suspend inline fun <reified T : Any> shouldBePublished(\n    atLeastIn: Duration = 5.seconds,\n    crossinline condition: ObservedMessage<T>.() -> Boolean\n  ): KafkaSystem = assertKafkaMessage(\n    assertionName = \"shouldBePublished\",\n    typeName = T::class.simpleName ?: \"Unknown\",\n    timeout = atLeastIn,\n    expected = \"Message matching condition within $atLeastIn\"\n  ) { onMatch ->\n    shouldBePublishedInternal(T::class, atLeastIn) { parsed ->\n      parsed.message.isSome { o ->\n        val result = condition(ObservedMessage(o, parsed.metadata))\n        if (result) onMatch(o)\n        result\n      }\n    }\n  }\n\n  /**\n   * Helper to reduce boilerplate in Kafka assertion methods.\n   * Handles try-catch, recording, and re-throwing.\n   */\n  @PublishedApi\n  internal suspend inline fun <T : Any> assertKafkaMessage(\n    assertionName: String,\n    typeName: String,\n    timeout: Duration,\n    expected: String,\n    crossinline block: suspend ((T) -> Unit) -> Unit\n  ): KafkaSystem {\n    var matchedMessage: T? = null\n\n    val result = runCatching {\n      coroutineScope {\n        block { matchedMessage = it }\n      }\n    }\n\n    val failure = result.exceptionOrNull()?.let { e ->\n      e as? AssertionError ?: AssertionError(\n        \"Expected $assertionName<$typeName> matching condition within $timeout, but none was found\",\n        e\n      )\n    }\n\n    if (result.isSuccess) {\n      reporter.record(\n        ReportEntry.success(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = \"$assertionName<$typeName>\",\n          output = matchedMessage.toOption(),\n          metadata = mapOf(\"timeout\" to timeout.toString())\n        )\n      )\n    } else {\n      reporter.record(\n        ReportEntry.failure(\n          system = reportSystemName,\n          testId = reporter.currentTestId(),\n          action = \"$assertionName<$typeName>\",\n          error = failure?.message ?: \"No matching message found\",\n          expected = expected.some(),\n          actual = (matchedMessage ?: \"No matching message found\").some()\n        )\n      )\n    }\n\n    failure?.let { throw it }\n    return this\n  }\n\n  @PublishedApi\n  internal suspend fun <T : Any> shouldBeConsumedInternal(\n    clazz: KClass<T>,\n    atLeastIn: Duration,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ): Unit = coroutineScope { getInterceptor().waitUntilConsumed(atLeastIn, clazz, condition) }\n\n  @PublishedApi\n  internal suspend fun <T : Any> shouldBeFailedInternal(\n    clazz: KClass<T>,\n    atLeastIn: Duration,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ): Unit = coroutineScope { getInterceptor().waitUntilFailed(atLeastIn, clazz, condition) }\n\n  @PublishedApi\n  internal suspend fun <T : Any> shouldBePublishedInternal(\n    clazz: KClass<T>,\n    atLeastIn: Duration,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ): Unit = coroutineScope { getInterceptor().waitUntilPublished(atLeastIn, clazz, condition) }\n\n  override fun configuration(): List<String> = context.options.configureExposedConfiguration(exposedConfiguration)\n\n  /**\n   * Pauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return KafkaSystem\n   */\n  fun pause(): KafkaSystem = withContainerOrWarn(\"pause\") { it.pause() }\n\n  /**\n   * Unpauses the container. Use with care, as it will pause the container which might affect other tests.\n   * This operation is not supported when using a provided instance.\n   * @return KafkaSystem\n   */\n  fun unpause(): KafkaSystem = withContainerOrWarn(\"unpause\") { it.unpause() }\n\n  override suspend fun stop(): Unit = whenContainer { it.stop() }\n\n  override fun close(): Unit = runBlocking {\n    Try {\n      context.options.cleanup(admin)\n      kafkaTemplate.destroy()\n      executeWithReuseCheck { stop() }\n    }.recover {\n      logger.warn(\"got an error while closing KafkaSystem\", it)\n    }\n  }\n\n  override suspend fun beforeRun() = Unit\n\n  override suspend fun run() {\n    exposedConfiguration = obtainExposedConfiguration()\n  }\n\n  override suspend fun afterRun(context: ApplicationContext) {\n    applicationContext = context\n    checkIfInterceptorConfiguredProperly(context)\n    kafkaTemplate = createKafkaTemplate(context, exposedConfiguration)\n    admin = createAdminClient(exposedConfiguration)\n    runMigrationsIfNeeded()\n  }\n\n  private suspend fun obtainExposedConfiguration(): KafkaExposedConfiguration =\n    when {\n      context.options is ProvidedKafkaSystemOptions -> context.options.config\n      context.runtime is StoveKafkaContainer -> startKafkaContainer(context.runtime)\n      else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n    }\n\n  private suspend fun startKafkaContainer(container: StoveKafkaContainer): KafkaExposedConfiguration =\n    state.capture {\n      container.start()\n      KafkaExposedConfiguration(container.bootstrapServers)\n    }\n\n  private suspend fun runMigrationsIfNeeded() {\n    if (shouldRunMigrations()) {\n      context.options.migrationCollection.run(KafkaMigrationContext(admin, context.options))\n    }\n  }\n\n  private fun shouldRunMigrations(): Boolean = when {\n    context.options is ProvidedKafkaSystemOptions -> context.options.runMigrations\n    context.runtime is StoveKafkaContainer -> !state.isSubsequentRun() || stove.runMigrationsAlways\n    else -> throw UnsupportedOperationException(\"Unsupported runtime type: ${context.runtime::class}\")\n  }\n\n  private fun createAdminClient(\n    exposedConfiguration: KafkaExposedConfiguration\n  ): Admin = Admin.create(\n    buildMap {\n      putAll(context.options.properties)\n      put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, exposedConfiguration.bootstrapServers)\n      put(AdminClientConfig.CLIENT_ID_CONFIG, \"stove-kafka-admin-client\")\n    }\n  )\n\n  private fun createKafkaTemplate(\n    context: ApplicationContext,\n    exposedConfiguration: KafkaExposedConfiguration\n  ): KafkaTemplate<Any, Any> {\n    val kafkaTemplates: Map<String, KafkaTemplate<Any, Any>> = context.getBeansOfType()\n    return kafkaTemplates\n      .values\n      .onEach {\n        it.setProducerListener(getInterceptor())\n        it.setCloseTimeout(1.seconds.toJavaDuration())\n      }.firstOrNone { safeContains(it, exposedConfiguration) }\n      .getOrElse {\n        logger.warn(\"No KafkaTemplate found for the configured bootstrap servers, using a fallback KafkaTemplate\")\n        createFallbackTemplate(exposedConfiguration)\n      }\n  }\n\n  @Suppress(\"UNCHECKED_CAST\")\n  private fun safeContains(\n    kafkaTemplate: KafkaTemplate<Any, Any>,\n    exposedConfiguration: KafkaExposedConfiguration\n  ): Boolean = kafkaTemplate.producerFactory.configurationProperties[ProducerConfig.BOOTSTRAP_SERVERS_CONFIG]\n    .toOption()\n    .map {\n      when (it) {\n        is String -> it\n        is Iterable<*> -> (it as Iterable<String>).joinToString(\",\")\n        else -> it.toString()\n      }\n    }.isSome { it.contains(exposedConfiguration.bootstrapServers) }\n\n  private fun createFallbackTemplate(exposedConfiguration: KafkaExposedConfiguration): KafkaTemplate<Any, Any> {\n    val producerFactory = DefaultKafkaProducerFactory<Any, Any>(\n      buildMap {\n        putAll(context.options.properties)\n        put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, exposedConfiguration.bootstrapServers)\n        put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, context.options.fallbackSerde.keySerializer::class.java)\n        put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, context.options.fallbackSerde.valueSerializer::class.java)\n      }\n    )\n    val fallbackTemplate = KafkaTemplate(producerFactory).also {\n      it.setProducerListener(getInterceptor())\n      it.setCloseTimeout(1.seconds.toJavaDuration())\n    }\n    return fallbackTemplate\n  }\n\n  private fun checkIfInterceptorConfiguredProperly(context: ApplicationContext) {\n    val interceptors: Map<String, RecordInterceptor<*, *>> = context.getBeansOfType()\n\n    fun stoveInterceptionPresent(): Boolean = interceptors.values.any { it is TestSystemKafkaInterceptor<*, *> }\n    if (!stoveInterceptionPresent()) {\n      throw AssertionError(\n        \"Kafka interceptor is not an instance of TestSystemKafkaInterceptor, \" +\n          \"please make sure that you have configured the Stove Kafka interceptor in your Spring Application properly.\" +\n          \"You can use stoveSpringRegistrar to add the interceptor to your Spring Application: \" +\n          \"\"\"\n              TestAppRunner.run(params) {\n                addInitializers(\n                  stoveSpringRegistrar {\n                    bean<TestSystemKafkaInterceptor<*, *>>(isPrimary = true)\n                  }\n                )\n              }          \n          \"\"\".trimIndent()\n      )\n    }\n  }\n\n  private inline fun withContainerOrWarn(\n    operation: String,\n    action: (StoveKafkaContainer) -> Unit\n  ): KafkaSystem = when (val runtime = context.runtime) {\n    is StoveKafkaContainer -> {\n      action(runtime)\n      this\n    }\n\n    is ProvidedRuntime -> {\n      logger.warn(\"$operation() is not supported when using a provided instance\")\n      this\n    }\n\n    else -> {\n      throw UnsupportedOperationException(\"Unsupported runtime type: ${runtime::class}\")\n    }\n  }\n\n  private inline fun whenContainer(action: (StoveKafkaContainer) -> Unit) {\n    if (context.runtime is StoveKafkaContainer) {\n      action(context.runtime)\n    }\n  }\n\n  companion object {\n    /**\n     * Exposes the [KafkaTemplate] to the [KafkaSystem].\n     * Use this for advanced Kafka operations not covered by the DSL.\n     */\n    fun KafkaSystem.kafkaTemplate(): KafkaTemplate<Any, Any> = kafkaTemplate\n  }\n}\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/KafkaTemplateCompatibility.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport kotlinx.coroutines.future.await\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.springframework.kafka.core.KafkaTemplate\nimport java.util.concurrent.CompletableFuture\n\n/**\n * Sends a [ProducerRecord] using the [KafkaTemplate] and waits for the result.\n * This method is used to send a record to Kafka in a compatible way with different versions of Spring Kafka.\n *\n * Supports:\n * - Spring Kafka 2.x (ListenableFuture)\n * - Spring Kafka 3.x (CompletableFuture with ListenableFuture backward compatibility)\n * - Spring Kafka 4.x (CompletableFuture only)\n *\n * Uses reflection to avoid compile-time dependency on ListenableFuture which doesn't exist in Spring 4.x.\n */\nsuspend fun KafkaTemplate<*, *>.sendCompatible(record: ProducerRecord<*, *>) {\n  val method = this::class.java.getDeclaredMethod(\"send\", ProducerRecord::class.java).apply { isAccessible = true }\n  val returnType = method.returnType\n  val result = method.invoke(this, record)\n\n  when {\n    CompletableFuture::class.java.isAssignableFrom(returnType) -> {\n      (result as CompletableFuture<*>).await()\n    }\n\n    returnType.name == \"org.springframework.util.concurrent.ListenableFuture\" -> {\n      // Use reflection to call completable() method for Spring Kafka 2.x/3.x ListenableFuture\n      val completableMethod = result.javaClass.getMethod(\"completable\")\n      (completableMethod.invoke(result) as CompletableFuture<*>).await()\n    }\n\n    else -> {\n      error(\"Unsupported return type for KafkaTemplate.send method: $returnType\")\n    }\n  }\n}\n\n/**\n * Default KafkaOps that uses the compatible send method.\n * Works with Spring Kafka 2.x, 3.x, and 4.x.\n */\nfun defaultKafkaOps(): KafkaOps = KafkaOps(\n  send = { kafkaTemplate, record -> kafkaTemplate.sendCompatible(record) }\n)\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/MessageStore.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.trendyol.stove.messaging.Failure\nimport io.exoquery.pprint\nimport java.util.*\n\ninternal class MessageStore {\n  private val consumed = Caching.of<UUID, StoveMessage.Consumed>()\n  private val produced = Caching.of<UUID, StoveMessage.Published>()\n  private val failures = Caching.of<UUID, Failure<StoveMessage.Failed>>()\n\n  fun record(record: StoveMessage.Consumed) {\n    consumed.put(UUID.randomUUID(), record)\n  }\n\n  fun record(record: StoveMessage.Published) {\n    produced.put(UUID.randomUUID(), record)\n  }\n\n  fun record(failure: Failure<StoveMessage.Failed>) {\n    failures.put(UUID.randomUUID(), failure)\n  }\n\n  fun consumedRecords(): List<StoveMessage.Consumed> = consumed.asMap().values.toList()\n\n  fun producedRecords(): List<StoveMessage.Published> = produced.asMap().values.toList()\n\n  fun failedRecords(): List<StoveMessage.Failed> = failures\n    .asMap()\n    .values\n    .map { failure -> failure.message.actual }\n    .toList()\n\n  override fun toString(): String = \"\"\"\n    |Consumed: ${pprint(consumedRecords().map { it.copy(value = ByteArray(0)) })}\n    |Published: ${pprint(producedRecords().map { it.copy(value = ByteArray(0)) })}\n    |Failed: ${pprint(failedRecords().map { it.copy(value = ByteArray(0)) })}\n  \"\"\".trimIndent().trimMargin()\n}\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/Options.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport arrow.core.getOrElse\nimport com.trendyol.stove.containers.*\nimport com.trendyol.stove.database.migrations.*\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.*\nimport com.trendyol.stove.system.annotations.StoveDsl\nimport org.apache.kafka.clients.admin.Admin\nimport org.apache.kafka.clients.producer.ProducerRecord\nimport org.apache.kafka.common.serialization.*\nimport org.springframework.kafka.core.KafkaTemplate\nimport org.testcontainers.kafka.ConfluentKafkaContainer\nimport org.testcontainers.utility.DockerImageName\n\nopen class StoveKafkaContainer(\n  override val imageNameAccess: DockerImageName\n) : ConfluentKafkaContainer(imageNameAccess),\n  StoveContainer\n\n@StoveDsl\ndata class KafkaExposedConfiguration(\n  val bootstrapServers: String\n) : ExposedConfiguration\n\n@StoveDsl\ndata class KafkaContainerOptions(\n  override val registry: String = DEFAULT_REGISTRY,\n  override val image: String = \"confluentinc/cp-kafka\",\n  override val tag: String = \"latest\",\n  override val compatibleSubstitute: String? = null,\n  override val useContainerFn: UseContainerFn<StoveKafkaContainer> = { StoveKafkaContainer(it) },\n  override val containerFn: ContainerFn<StoveKafkaContainer> = { }\n) : ContainerOptions<StoveKafkaContainer>\n\n/**\n * Operations for Kafka. It is used to customize the operations of Kafka.\n * The reason why this exists is to provide a way to interact with lower versions of Spring-Kafka dependencies.\n */\ndata class KafkaOps(\n  val send: suspend (\n    KafkaTemplate<*, *>,\n    ProducerRecord<*, *>\n  ) -> Unit\n)\n\ndata class FallbackTemplateSerde(\n  val keySerializer: Serializer<*> = StringSerializer(),\n  val valueSerializer: Serializer<*> = StringSerializer()\n)\n\n/**\n * Context provided to Kafka migrations.\n * Contains the Admin client and options for performing setup operations.\n *\n * @property admin The Kafka Admin client for managing topics, ACLs, etc.\n * @property options The Kafka system options\n */\n@StoveDsl\ndata class KafkaMigrationContext(\n  val admin: Admin,\n  val options: KafkaSystemOptions\n)\n\n/**\n * Options for configuring the Spring Kafka system in container mode.\n */\n@StoveDsl\nopen class KafkaSystemOptions(\n  /**\n   * The registry of the Kafka image. The default value is `DEFAULT_REGISTRY`.\n   */\n  open val registry: String = DEFAULT_REGISTRY,\n  /**\n   * The ports of the Kafka container. The default value is `DEFAULT_KAFKA_PORTS`.\n   */\n  open val ports: List<Int> = DEFAULT_KAFKA_PORTS,\n  /**\n   * The fallback serde for Kafka. It is used to serialize and deserialize the messages before sending them to Kafka.\n   * If no [KafkaTemplate] is provided, it will be used to create a new [KafkaTemplate].\n   * Most of the time you won't need this.\n   */\n  open val fallbackSerde: FallbackTemplateSerde = FallbackTemplateSerde(),\n  /**\n   * Container options for Kafka.\n   */\n  open val containerOptions: KafkaContainerOptions = KafkaContainerOptions(),\n  /**\n   * Operations for Kafka. It is used to customize the operations of Kafka.\n   * Defaults to [defaultKafkaOps] which works with Spring Kafka 2.x, 3.x, and 4.x.\n   * @see KafkaOps\n   * @see defaultKafkaOps\n   */\n  open val ops: KafkaOps = defaultKafkaOps(),\n  /**\n   * A suspend function to clean up data after tests complete.\n   */\n  open val cleanup: suspend (Admin) -> Unit = {},\n  /**\n   * Additional Kafka client properties applied to all internal clients (admin, fallback producer).\n   * Use this to pass security configs (SASL_SSL, truststore, etc.) when connecting to a secured cluster.\n   *\n   * Example:\n   * ```kotlin\n   * properties = mapOf(\n   *   \"security.protocol\" to \"SASL_SSL\",\n   *   \"sasl.mechanism\" to \"PLAIN\",\n   *   \"sasl.jaas.config\" to \"...PlainLoginModule required username=\\\"user\\\" password=\\\"pass\\\";\"\n   * )\n   * ```\n   */\n  open val properties: Map<String, Any> = emptyMap(),\n  /**\n   * The configuration of the Kafka settings that is exposed to the Application Under Test(AUT).\n   */\n  override val configureExposedConfiguration: (KafkaExposedConfiguration) -> List<String>\n) : SystemOptions,\n  ConfiguresExposedConfiguration<KafkaExposedConfiguration>,\n  SupportsMigrations<KafkaMigrationContext, KafkaSystemOptions> {\n  override val migrationCollection: MigrationCollection<KafkaMigrationContext> = MigrationCollection()\n\n  companion object {\n    val DEFAULT_KAFKA_PORTS = listOf(9092, 9093)\n\n    /**\n     * Creates options configured to use an externally provided Kafka instance\n     * instead of a testcontainer.\n     *\n     * @param bootstrapServers The Kafka bootstrap servers (e.g., \"localhost:9092\")\n     * @param registry The registry for the container (not used for provided instances)\n     * @param ports The ports for the container (not used for provided instances)\n     * @param fallbackSerde The fallback serde for serialization\n     * @param ops Operations for Kafka\n     * @param runMigrations Whether to run migrations on the external instance (default: true)\n     * @param cleanup A suspend function to clean up data after tests complete\n     * @param configureExposedConfiguration Function to map exposed config to application properties\n     */\n    fun provided(\n      bootstrapServers: String,\n      registry: String = DEFAULT_REGISTRY,\n      ports: List<Int> = DEFAULT_KAFKA_PORTS,\n      fallbackSerde: FallbackTemplateSerde = FallbackTemplateSerde(),\n      ops: KafkaOps = defaultKafkaOps(),\n      properties: Map<String, Any> = emptyMap(),\n      runMigrations: Boolean = true,\n      cleanup: suspend (Admin) -> Unit = {},\n      configureExposedConfiguration: (KafkaExposedConfiguration) -> List<String>\n    ): ProvidedKafkaSystemOptions = ProvidedKafkaSystemOptions(\n      config = KafkaExposedConfiguration(bootstrapServers = bootstrapServers),\n      registry = registry,\n      ports = ports,\n      fallbackSerde = fallbackSerde,\n      ops = ops,\n      properties = properties,\n      runMigrations = runMigrations,\n      cleanup = cleanup,\n      configureExposedConfiguration = configureExposedConfiguration\n    )\n  }\n}\n\n/**\n * Options for using an externally provided Kafka instance.\n * This class holds the configuration for the external instance directly (non-nullable).\n */\n@StoveDsl\nclass ProvidedKafkaSystemOptions(\n  /**\n   * The configuration for the provided Kafka instance.\n   */\n  val config: KafkaExposedConfiguration,\n  registry: String = DEFAULT_REGISTRY,\n  ports: List<Int> = DEFAULT_KAFKA_PORTS,\n  fallbackSerde: FallbackTemplateSerde = FallbackTemplateSerde(),\n  ops: KafkaOps = defaultKafkaOps(),\n  cleanup: suspend (Admin) -> Unit = {},\n  properties: Map<String, Any> = emptyMap(),\n  /**\n   * Whether to run migrations on the external instance.\n   */\n  val runMigrations: Boolean = true,\n  configureExposedConfiguration: (KafkaExposedConfiguration) -> List<String>\n) : KafkaSystemOptions(\n  registry = registry,\n  ports = ports,\n  fallbackSerde = fallbackSerde,\n  containerOptions = KafkaContainerOptions(),\n  ops = ops,\n  cleanup = cleanup,\n  properties = properties,\n  configureExposedConfiguration = configureExposedConfiguration\n),\n  ProvidedSystemOptions<KafkaExposedConfiguration> {\n  override val providedConfig: KafkaExposedConfiguration = config\n  override val runMigrationsForProvided: Boolean = runMigrations\n}\n\n@StoveDsl\ndata class KafkaContext(\n  val runtime: SystemRuntime,\n  val options: KafkaSystemOptions\n)\n\ninternal fun Stove.withKafka(\n  options: KafkaSystemOptions,\n  runtime: SystemRuntime\n): Stove {\n  getOrRegister(KafkaSystem(this, KafkaContext(runtime, options)))\n  return this\n}\n\ninternal fun Stove.kafka(): KafkaSystem = getOrNone<KafkaSystem>().getOrElse {\n  throw SystemNotRegisteredException(KafkaSystem::class)\n}\n\n/**\n * Configures Spring Kafka system.\n *\n * For container-based setup:\n * ```kotlin\n * kafka {\n *   KafkaSystemOptions(\n *     cleanup = { admin -> admin.deleteTopics(...).all().get() },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   ).migrations {\n *     register<CreateTopicsMigration>()\n *   }\n * }\n * ```\n *\n * For provided (external) instance:\n * ```kotlin\n * kafka {\n *   KafkaSystemOptions.provided(\n *     bootstrapServers = \"localhost:9092\",\n *     runMigrations = true,\n *     cleanup = { admin -> admin.deleteTopics(...).all().get() },\n *     configureExposedConfiguration = { cfg -> listOf(...) }\n *   ).migrations {\n *     register<CreateTopicsMigration>()\n *   }\n * }\n * ```\n */\nfun WithDsl.kafka(\n  configure: () -> KafkaSystemOptions\n): Stove {\n  SpringKafkaVersionCheck.ensureSpringKafkaAvailable()\n  val options = configure()\n\n  val runtime: SystemRuntime = if (options is ProvidedKafkaSystemOptions) {\n    ProvidedRuntime\n  } else {\n    withProvidedRegistry(\n      options.containerOptions.imageWithTag,\n      registry = options.registry,\n      compatibleSubstitute = options.containerOptions.compatibleSubstitute\n    ) {\n      options.containerOptions\n        .useContainerFn(it)\n        .withExposedPorts(*options.ports.toTypedArray())\n        .withReuse(stove.keepDependenciesRunning)\n        .let { c -> c as StoveKafkaContainer }\n        .apply(options.containerOptions.containerFn)\n    }\n  }\n\n  return stove.withKafka(options, runtime)\n}\n\nsuspend fun ValidationDsl.kafka(validation: suspend KafkaSystem.() -> Unit): Unit = validation(this.stove.kafka())\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/SpringKafkaVersionCheck.kt",
    "content": "package com.trendyol.stove.kafka\n\n/**\n * Utility object to check Spring Kafka availability at runtime.\n * Since Spring Kafka is a `compileOnly` dependency, users must bring their own version.\n */\ninternal object SpringKafkaVersionCheck {\n  private const val KAFKA_TEMPLATE_CLASS = \"org.springframework.kafka.core.KafkaTemplate\"\n\n  /**\n   * Checks if Spring Kafka is available on the classpath.\n   * @throws IllegalStateException if Spring Kafka is not found\n   */\n  fun ensureSpringKafkaAvailable() {\n    try {\n      Class.forName(KAFKA_TEMPLATE_CLASS)\n    } catch (e: ClassNotFoundException) {\n      throw IllegalStateException(\n        \"\"\"\n        |\n        |═══════════════════════════════════════════════════════════════════════════════\n        |  Spring Kafka Not Found on Classpath!\n        |═══════════════════════════════════════════════════════════════════════════════\n        |\n        |  stove-spring-testing-e2e-kafka requires Spring Kafka to be on your classpath.\n        |  Spring Kafka is declared as a 'compileOnly' dependency, so you must add it\n        |  to your project.\n        |\n        |  Add one of the following to your build.gradle.kts:\n        |\n        |  For Spring Boot 2.x:\n        |    testImplementation(\"org.springframework.kafka:spring-kafka:2.9.x\")\n        |    // or use the starter:\n        |    testImplementation(\"org.springframework.boot:spring-boot-starter-kafka:2.7.x\")\n        |\n        |  For Spring Boot 3.x:\n        |    testImplementation(\"org.springframework.kafka:spring-kafka:3.x.x\")\n        |    // or use the starter:\n        |    testImplementation(\"org.springframework.boot:spring-boot-starter-kafka:3.x.x\")\n        |\n        |  For Spring Boot 4.x:\n        |    testImplementation(\"org.springframework.kafka:spring-kafka:4.x.x\")\n        |    // or use the starter:\n        |    testImplementation(\"org.springframework.boot:spring-boot-starter-kafka:4.x.x\")\n        |\n        |═══════════════════════════════════════════════════════════════════════════════\n        \"\"\".trimMargin(),\n        e\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/StoveMessage.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.trendyol.stove.messaging.MessageMetadata\nimport io.exoquery.pprint\n\nsealed interface MessageProperties {\n  val topic: String\n  val value: ByteArray\n  val valueAsString: String\n  val metadata: MessageMetadata\n  val partition: Int?\n  val key: String?\n  val timestamp: Long?\n}\n\ninternal sealed class StoveMessage : MessageProperties {\n  override fun equals(other: Any?): Boolean {\n    if (this === other) return true\n    if (javaClass != other?.javaClass) return false\n\n    other as StoveMessage\n\n    if (topic != other.topic) return false\n    if (!value.contentEquals(other.value)) return false\n    if (metadata != other.metadata) return false\n    if (partition != other.partition) return false\n    if (key != other.key) return false\n    if (timestamp != other.timestamp) return false\n\n    return true\n  }\n\n  override fun hashCode(): Int {\n    var result = topic.hashCode()\n    result = 31 * result + value.contentHashCode()\n    result = 31 * result + metadata.hashCode()\n    result = 31 * result + (partition ?: 0)\n    result = 31 * result + (key?.hashCode() ?: 0)\n    result = 31 * result + (timestamp?.hashCode() ?: 0)\n    return result\n  }\n\n  data class Consumed(\n    override val topic: String,\n    override val value: ByteArray,\n    override val metadata: MessageMetadata,\n    override val partition: Int?,\n    override val key: String?,\n    override val timestamp: Long?,\n    val offset: Long?,\n    override val valueAsString: String = String(value)\n  ) : StoveMessage() {\n    override fun hashCode(): Int = super.hashCode() + offset.hashCode()\n\n    override fun equals(other: Any?): Boolean = super.equals(other) && other is Consumed && offset == other.offset\n\n    override fun toString(): String = pprint(this.copy(value = ByteArray(0))).toString()\n  }\n\n  data class Published(\n    override val topic: String,\n    override val value: ByteArray,\n    override val metadata: MessageMetadata,\n    override val partition: Int?,\n    override val key: String?,\n    override val timestamp: Long?,\n    override val valueAsString: String = String(value)\n  ) : StoveMessage() {\n    override fun hashCode(): Int = super.hashCode()\n\n    override fun equals(other: Any?): Boolean = super.equals(other)\n\n    override fun toString(): String = pprint(this.copy(value = ByteArray(0))).toString()\n  }\n\n  data class Failed(\n    override val topic: String,\n    override val value: ByteArray,\n    override val metadata: MessageMetadata,\n    override val partition: Int?,\n    override val key: String?,\n    override val timestamp: Long?,\n    val reason: Throwable,\n    override val valueAsString: String = String(value)\n  ) : StoveMessage() {\n    override fun hashCode(): Int = super.hashCode() + reason.hashCode()\n\n    override fun equals(other: Any?): Boolean = super.equals(other) && other is Failed && reason == other.reason\n\n    override fun toString(): String = pprint(this.copy(value = ByteArray(0))).toString()\n  }\n\n  companion object {\n    fun consumed(\n      topic: String,\n      value: ByteArray,\n      metadata: MessageMetadata,\n      partition: Int? = null,\n      key: String? = null,\n      timestamp: Long? = null,\n      offset: Long? = null\n    ): Consumed = Consumed(topic, value, metadata, partition, key, timestamp, offset)\n\n    fun published(\n      topic: String,\n      value: ByteArray,\n      metadata: MessageMetadata,\n      partition: Int? = null,\n      key: String? = null,\n      timestamp: Long? = null\n    ): Published = Published(topic, value, metadata, partition, key, timestamp)\n\n    fun failed(\n      topic: String,\n      value: ByteArray,\n      metadata: MessageMetadata,\n      reason: Throwable,\n      partition: Int? = null,\n      key: String? = null,\n      timestamp: Long? = null\n    ): Failed = Failed(topic, value, metadata, partition, key, timestamp, reason)\n  }\n}\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/main/kotlin/com/trendyol/stove/kafka/TestSystemKafkaInterceptor.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport arrow.core.toOption\nimport com.trendyol.stove.messaging.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport kotlinx.coroutines.*\nimport org.apache.kafka.clients.consumer.*\nimport org.apache.kafka.clients.producer.*\nimport org.slf4j.*\nimport org.springframework.kafka.listener.*\nimport org.springframework.kafka.support.ProducerListener\nimport kotlin.reflect.KClass\nimport kotlin.time.Duration\n\n/**\n * This is the main actor between your Kafka Spring Boot application and the test system.\n * It is responsible for intercepting the messages that are produced and consumed by the application.\n * It also provides a way to wait until a message is consumed or produced.\n *\n * @param serde The serializer/deserializer that will be used to serialize/deserialize the messages.\n * It is important to use the same serde that is used in the application.\n * For example, if the application uses Avro, then you should use Avro serde here.\n * Target of the serialization is ByteArray, so the serde should be able to serialize the message to ByteArray.\n */\nclass TestSystemKafkaInterceptor<K : Any, V : Any>(\n  private val serde: StoveSerde<Any, ByteArray>\n) : CompositeRecordInterceptor<K, V>(),\n  ProducerListener<K, V> {\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n  private val store = MessageStore()\n\n  /**\n   * Get access to the message store for reporting purposes.\n   */\n  internal fun getStore(): MessageStore = store\n\n  override fun onSuccess(\n    record: ProducerRecord<K, V>,\n    recordMetadata: RecordMetadata\n  ) {\n    val message = record.toStoveMessage(serde)\n    store.record(message)\n    logger.info(\"Successfully produced:\\n{}\", message)\n  }\n\n  override fun onError(\n    record: ProducerRecord<K, V>,\n    recordMetadata: RecordMetadata?,\n    exception: Exception\n  ) {\n    val underlyingReason = extractCause(exception)\n    val message = record.toFailedStoveMessage(serde, underlyingReason)\n    store.record(Failure(ObservedMessage(message, record.toMetadata()), underlyingReason))\n    logger.error(\"Error while producing:\\n{}\", message, exception)\n  }\n\n  override fun success(record: ConsumerRecord<K, V>, consumer: Consumer<K, V>) {\n    val message = record.toStoveMessage(serde)\n    store.record(message)\n    logger.info(\"Successfully consumed:\\n{}\", message)\n  }\n\n  override fun failure(\n    record: ConsumerRecord<K, V>,\n    exception: Exception,\n    consumer: Consumer<K, V>\n  ) {\n    val underlyingReason = extractCause(exception)\n    val message = record.toFailedStoveMessage(serde, underlyingReason)\n    store.record(Failure(ObservedMessage(message, record.toMetadata()), underlyingReason))\n    logger.error(\"Error while consuming:\\n{}\", message, exception)\n  }\n\n  internal suspend fun <T : Any> waitUntilConsumed(\n    atLeastIn: Duration,\n    clazz: KClass<T>,\n    condition: (metadata: ParsedMessage<T>) -> Boolean\n  ) {\n    val getRecords = { store.consumedRecords() }\n    getRecords.waitUntilConditionMet(atLeastIn, \"While expecting the consume of '${clazz.java.simpleName}'\") {\n      val outcome = deserializeCatching(it.value, clazz)\n      outcome.isSuccess && condition(SuccessfulParsedMessage(outcome.getOrNull().toOption(), it.metadata))\n    }\n\n    throwIfFailed(clazz, condition)\n  }\n\n  internal suspend fun <T : Any> waitUntilFailed(\n    atLeastIn: Duration,\n    clazz: KClass<T>,\n    condition: (metadata: ParsedMessage<T>) -> Boolean\n  ) {\n    val getRecords = { store.failedRecords() }\n    getRecords.waitUntilConditionMet(atLeastIn, \"While expecting the failure of '${clazz.java.simpleName}'\") {\n      val outcome = deserializeCatching(it.value, clazz)\n      outcome.isSuccess && condition(FailedParsedMessage(outcome.getOrNull().toOption(), it.metadata, it.reason))\n    }\n\n    throwIfSucceeded(clazz, condition)\n  }\n\n  internal suspend fun <T : Any> waitUntilPublished(\n    atLeastIn: Duration,\n    clazz: KClass<T>,\n    condition: (message: ParsedMessage<T>) -> Boolean\n  ) {\n    val getRecords = { store.producedRecords() }\n    getRecords.waitUntilConditionMet(atLeastIn, \"While expecting the publish of '${clazz.java.simpleName}'\") {\n      val outcome = deserializeCatching(it.value, clazz)\n      outcome.isSuccess && condition(SuccessfulParsedMessage(outcome.getOrNull().toOption(), it.metadata))\n    }\n  }\n\n  private fun extractCause(\n    listenerException: Exception\n  ): Exception = when (listenerException) {\n    is ListenerExecutionFailedException -> {\n      listenerException.cause\n        ?: AssertionError(\"No cause found: Listener was not able to capture the cause\")\n    }\n\n    else -> {\n      listenerException\n    }\n  } as Exception\n\n  private fun <T : Any> deserializeCatching(\n    value: ByteArray,\n    clazz: KClass<T>\n  ): Result<T> = runCatching { serde.deserialize(value, clazz.java) }\n    .onFailure { logger.debug(\"[Stove#deserializeCatching] Error while deserializing: '{}'\", String(value), it) }\n\n  private fun <T : Any> throwIfFailed(\n    clazz: KClass<T>,\n    selector: (message: ParsedMessage<T>) -> Boolean\n  ) = store\n    .failedRecords()\n    .filter {\n      selector(\n        FailedParsedMessage(\n          deserializeCatching(it.value, clazz).getOrNull().toOption(),\n          MessageMetadata(it.metadata.topic, it.metadata.key, it.metadata.headers),\n          it.reason\n        )\n      )\n    }.forEach {\n      throw AssertionError(\n        \"Message was expected to be consumed successfully, but failed: $it \\n ${dumpMessages()}\"\n      )\n    }\n\n  private fun <T : Any> throwIfSucceeded(\n    clazz: KClass<T>,\n    selector: (ParsedMessage<T>) -> Boolean\n  ): Unit = store\n    .consumedRecords()\n    .filter { record ->\n      selector(\n        FailedParsedMessage(\n          deserializeCatching(record.value, clazz).getOrNull().toOption(),\n          record.metadata,\n          getExceptionFor(clazz, selector)\n        )\n      )\n    }.forEach { throw AssertionError(\"Expected to fail but succeeded: $it\") }\n\n  private fun <T : Any> getExceptionFor(\n    clazz: KClass<T>,\n    selector: (message: FailedParsedMessage<T>) -> Boolean\n  ): Throwable = store\n    .failedRecords()\n    .first {\n      selector(FailedParsedMessage(deserializeCatching(it.value, clazz).getOrNull().toOption(), it.metadata, it.reason))\n    }.reason\n\n  private suspend fun <T : Any> (() -> Collection<T>).waitUntilConditionMet(\n    duration: Duration,\n    subject: String,\n    delayMs: Long = 50L,\n    condition: (T) -> Boolean\n  ): Collection<T> = runCatching {\n    val collectionFunc = this\n    withTimeout(duration) { while (!collectionFunc().any { condition(it) }) delay(delayMs) }\n    collectionFunc().filter { condition(it) }\n  }.fold(\n    onSuccess = { it },\n    onFailure = { throw AssertionError(\"GOT A TIMEOUT: $subject. ${dumpMessages()}\") }\n  )\n\n  private fun dumpMessages(): String = \"Messages so far:\\n$store\"\n}\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/test/kotlin/com/trendyol/stove/kafka/CachingTests.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.nulls.shouldBeNull\nimport io.kotest.matchers.shouldBe\n\nclass CachingTests :\n  FunSpec({\n\n    test(\"should create cache that stores and retrieves values\") {\n      val cache = Caching.of<String, Int>()\n\n      cache.put(\"key1\", 100)\n      cache.put(\"key2\", 200)\n\n      cache.getIfPresent(\"key1\") shouldBe 100\n      cache.getIfPresent(\"key2\") shouldBe 200\n    }\n\n    test(\"should return null for non-existent keys\") {\n      val cache = Caching.of<String, String>()\n\n      cache.getIfPresent(\"non-existent\").shouldBeNull()\n    }\n\n    test(\"should overwrite existing values\") {\n      val cache = Caching.of<String, String>()\n\n      cache.put(\"key\", \"original\")\n      cache.put(\"key\", \"updated\")\n\n      cache.getIfPresent(\"key\") shouldBe \"updated\"\n    }\n\n    test(\"should support complex key types\") {\n      data class ComplexKey(\n        val id: Int,\n        val name: String\n      )\n\n      val cache = Caching.of<ComplexKey, String>()\n      val key = ComplexKey(1, \"test\")\n\n      cache.put(key, \"value\")\n\n      cache.getIfPresent(key) shouldBe \"value\"\n    }\n\n    test(\"should support any value types\") {\n      data class ComplexValue(\n        val data: List<String>,\n        val count: Int\n      )\n\n      val cache = Caching.of<String, ComplexValue>()\n      val value = ComplexValue(listOf(\"a\", \"b\", \"c\"), 3)\n\n      cache.put(\"key\", value)\n\n      cache.getIfPresent(\"key\") shouldBe value\n    }\n\n    test(\"asMap should return all cached entries\") {\n      val cache = Caching.of<String, Int>()\n\n      cache.put(\"one\", 1)\n      cache.put(\"two\", 2)\n      cache.put(\"three\", 3)\n\n      val map = cache.asMap()\n      map.size shouldBe 3\n      map[\"one\"] shouldBe 1\n      map[\"two\"] shouldBe 2\n      map[\"three\"] shouldBe 3\n    }\n\n    test(\"should handle invalidation\") {\n      val cache = Caching.of<String, String>()\n\n      cache.put(\"key\", \"value\")\n      cache.invalidate(\"key\")\n\n      cache.getIfPresent(\"key\").shouldBeNull()\n    }\n  })\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/test/kotlin/com/trendyol/stove/kafka/ExtensionsTests.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport arrow.core.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass ExtensionsTests :\n  FunSpec({\n\n    test(\"addTestCase should add testCase to map when not present\") {\n      val map = mutableMapOf<String, String>(\"existingKey\" to \"existingValue\")\n\n      map.addTestCase(\"myTestCase\".some())\n\n      map[\"testCase\"] shouldBe \"myTestCase\"\n      map[\"existingKey\"] shouldBe \"existingValue\"\n    }\n\n    test(\"addTestCase should not overwrite existing testCase\") {\n      val map = mutableMapOf<String, String>(\"testCase\" to \"existingTestCase\")\n\n      map.addTestCase(\"newTestCase\".some())\n\n      map[\"testCase\"] shouldBe \"existingTestCase\"\n    }\n\n    test(\"addTestCase should do nothing when Option is None\") {\n      val map = mutableMapOf<String, String>(\"key\" to \"value\")\n\n      map.addTestCase(none())\n\n      map.containsKey(\"testCase\") shouldBe false\n      map[\"key\"] shouldBe \"value\"\n    }\n\n    test(\"addTestCase should return the same map\") {\n      val map = mutableMapOf<String, String>()\n\n      val result = map.addTestCase(\"test\".some())\n\n      result shouldBe map\n    }\n\n    test(\"addTestCase with empty map and Some value\") {\n      val map = mutableMapOf<String, String>()\n\n      map.addTestCase(\"firstTest\".some())\n\n      map.size shouldBe 1\n      map[\"testCase\"] shouldBe \"firstTest\"\n    }\n\n    test(\"addTestCase should preserve all existing entries\") {\n      val map = mutableMapOf(\n        \"header1\" to \"value1\",\n        \"header2\" to \"value2\",\n        \"header3\" to \"value3\"\n      )\n\n      map.addTestCase(\"myTest\".some())\n\n      map.size shouldBe 4\n      map[\"header1\"] shouldBe \"value1\"\n      map[\"header2\"] shouldBe \"value2\"\n      map[\"header3\"] shouldBe \"value3\"\n      map[\"testCase\"] shouldBe \"myTest\"\n    }\n  })\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/test/kotlin/com/trendyol/stove/kafka/KafkaOptionsTest.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\n\nclass KafkaOptionsTest :\n  FunSpec({\n    test(\"provided options should expose config and runMigrations flag\") {\n      val kafkaAvailable = runCatching {\n        Class.forName(\"org.apache.kafka.common.serialization.StringSerializer\")\n      }.isSuccess\n      if (!kafkaAvailable) return@test\n\n      val options = KafkaSystemOptions.provided(\n        bootstrapServers = \"localhost:9092\",\n        runMigrations = false,\n        configureExposedConfiguration = { listOf(\"bootstrap=${it.bootstrapServers}\") }\n      )\n\n      options.providedConfig.bootstrapServers shouldBe \"localhost:9092\"\n      options.runMigrationsForProvided shouldBe false\n      options.configureExposedConfiguration(options.providedConfig) shouldBe listOf(\"bootstrap=localhost:9092\")\n    }\n\n    test(\"default kafka ports should include 9092 and 9093\") {\n      KafkaSystemOptions.DEFAULT_KAFKA_PORTS shouldBe listOf(9092, 9093)\n    }\n  })\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/test/kotlin/com/trendyol/stove/kafka/MessageStoreTests.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.trendyol.stove.messaging.*\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.collections.*\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\n\nclass MessageStoreTests :\n  FunSpec({\n\n    test(\"should record and retrieve consumed messages\") {\n      val store = MessageStore()\n      val metadata = MessageMetadata(\"test-topic\", \"key\", emptyMap())\n      val message = StoveMessage.consumed(\n        topic = \"test-topic\",\n        value = \"test-value\".toByteArray(),\n        metadata = metadata,\n        offset = 1L\n      )\n\n      store.record(message)\n\n      val records = store.consumedRecords()\n      records shouldHaveSize 1\n      records.first() shouldBe message\n    }\n\n    test(\"should record and retrieve multiple consumed messages\") {\n      val store = MessageStore()\n      val metadata = MessageMetadata(\"topic\", \"key\", emptyMap())\n\n      repeat(5) { i ->\n        store.record(\n          StoveMessage.consumed(\n            topic = \"topic-$i\",\n            value = \"value-$i\".toByteArray(),\n            metadata = metadata,\n            offset = i.toLong()\n          )\n        )\n      }\n\n      store.consumedRecords() shouldHaveSize 5\n    }\n\n    test(\"should record and retrieve published messages\") {\n      val store = MessageStore()\n      val metadata = MessageMetadata(\"pub-topic\", \"pub-key\", emptyMap())\n      val message = StoveMessage.published(\n        topic = \"pub-topic\",\n        value = \"published-value\".toByteArray(),\n        metadata = metadata\n      )\n\n      store.record(message)\n\n      val records = store.producedRecords()\n      records shouldHaveSize 1\n      records.first() shouldBe message\n    }\n\n    test(\"should record and retrieve failed messages\") {\n      val store = MessageStore()\n      val metadata = MessageMetadata(\"fail-topic\", \"fail-key\", emptyMap())\n      val failedMessage = StoveMessage.failed(\n        topic = \"fail-topic\",\n        value = \"failed-value\".toByteArray(),\n        metadata = metadata,\n        reason = RuntimeException(\"Test error\")\n      )\n      val failure = Failure(\n        message = ObservedMessage(failedMessage, metadata),\n        reason = failedMessage.reason\n      )\n\n      store.record(failure)\n\n      val records = store.failedRecords()\n      records shouldHaveSize 1\n      records.first().topic shouldBe \"fail-topic\"\n      records.first().reason.message shouldBe \"Test error\"\n    }\n\n    test(\"should maintain separate stores for consumed, produced, and failed\") {\n      val store = MessageStore()\n      val metadata = MessageMetadata(\"topic\", \"key\", emptyMap())\n\n      store.record(\n        StoveMessage.consumed(\n          topic = \"consumed-topic\",\n          value = \"consumed\".toByteArray(),\n          metadata = metadata\n        )\n      )\n      store.record(\n        StoveMessage.published(\n          topic = \"published-topic\",\n          value = \"published\".toByteArray(),\n          metadata = metadata\n        )\n      )\n\n      val failedMessage = StoveMessage.failed(\n        topic = \"failed-topic\",\n        value = \"failed\".toByteArray(),\n        metadata = metadata,\n        reason = RuntimeException(\"Error\")\n      )\n      store.record(\n        Failure(\n          message = ObservedMessage(failedMessage, metadata),\n          reason = failedMessage.reason\n        )\n      )\n\n      store.consumedRecords() shouldHaveSize 1\n      store.producedRecords() shouldHaveSize 1\n      store.failedRecords() shouldHaveSize 1\n\n      store.consumedRecords().first().topic shouldBe \"consumed-topic\"\n      store.producedRecords().first().topic shouldBe \"published-topic\"\n      store.failedRecords().first().topic shouldBe \"failed-topic\"\n    }\n\n    test(\"toString should include all message types\") {\n      val store = MessageStore()\n      val metadata = MessageMetadata(\"topic\", \"key\", emptyMap())\n\n      store.record(\n        StoveMessage.consumed(\n          topic = \"consumed-topic\",\n          value = \"consumed\".toByteArray(),\n          metadata = metadata\n        )\n      )\n      store.record(\n        StoveMessage.published(\n          topic = \"published-topic\",\n          value = \"published\".toByteArray(),\n          metadata = metadata\n        )\n      )\n\n      val output = store.toString()\n      output shouldContain \"Consumed\"\n      output shouldContain \"Published\"\n      output shouldContain \"Failed\"\n    }\n\n    test(\"should return empty lists when no messages recorded\") {\n      val store = MessageStore()\n\n      store.consumedRecords() shouldBe emptyList()\n      store.producedRecords() shouldBe emptyList()\n      store.failedRecords() shouldBe emptyList()\n    }\n  })\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/test/kotlin/com/trendyol/stove/kafka/SpringKafkaVersionCheckTest.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.string.shouldContain\n\nclass SpringKafkaVersionCheckTest :\n  FunSpec({\n    test(\"ensureSpringKafkaAvailable should throw when Spring Kafka is missing\") {\n      val error = shouldThrow<IllegalStateException> {\n        SpringKafkaVersionCheck.ensureSpringKafkaAvailable()\n      }\n\n      error.message shouldContain \"Spring Kafka Not Found on Classpath\"\n    }\n  })\n"
  },
  {
    "path": "starters/spring/stove-spring-kafka/src/test/kotlin/com/trendyol/stove/kafka/StoveMessageTests.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.trendyol.stove.messaging.MessageMetadata\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.shouldNotBe\n\nclass StoveMessageTests :\n  FunSpec({\n\n    test(\"consumed message should be created with factory method\") {\n      val metadata = MessageMetadata(\"test-topic\", \"test-key\", mapOf(\"header1\" to \"value1\"))\n      val message = StoveMessage.consumed(\n        topic = \"test-topic\",\n        value = \"test-value\".toByteArray(),\n        metadata = metadata,\n        partition = 0,\n        key = \"test-key\",\n        timestamp = 1234567890L,\n        offset = 100L\n      )\n\n      message.topic shouldBe \"test-topic\"\n      message.valueAsString shouldBe \"test-value\"\n      message.metadata shouldBe metadata\n      message.partition shouldBe 0\n      message.key shouldBe \"test-key\"\n      message.timestamp shouldBe 1234567890L\n      message.offset shouldBe 100L\n    }\n\n    test(\"published message should be created with factory method\") {\n      val metadata = MessageMetadata(\"test-topic\", \"test-key\", emptyMap())\n      val message = StoveMessage.published(\n        topic = \"test-topic\",\n        value = \"published-value\".toByteArray(),\n        metadata = metadata,\n        partition = 1,\n        key = \"pub-key\",\n        timestamp = 9876543210L\n      )\n\n      message.topic shouldBe \"test-topic\"\n      message.valueAsString shouldBe \"published-value\"\n      message.metadata shouldBe metadata\n      message.partition shouldBe 1\n      message.key shouldBe \"pub-key\"\n      message.timestamp shouldBe 9876543210L\n    }\n\n    test(\"failed message should be created with factory method\") {\n      val metadata = MessageMetadata(\"error-topic\", \"error-key\", mapOf(\"error\" to \"true\"))\n      val exception = RuntimeException(\"Test failure\")\n      val message = StoveMessage.failed(\n        topic = \"error-topic\",\n        value = \"failed-value\".toByteArray(),\n        metadata = metadata,\n        reason = exception,\n        partition = 2,\n        key = \"error-key\",\n        timestamp = 1111111111L\n      )\n\n      message.topic shouldBe \"error-topic\"\n      message.valueAsString shouldBe \"failed-value\"\n      message.metadata shouldBe metadata\n      message.partition shouldBe 2\n      message.key shouldBe \"error-key\"\n      message.timestamp shouldBe 1111111111L\n      message.reason shouldBe exception\n    }\n\n    test(\"consumed messages with same content should be equal\") {\n      val metadata = MessageMetadata(\"topic\", \"key\", emptyMap())\n      val value = \"same-value\".toByteArray()\n\n      val message1 = StoveMessage.consumed(\n        topic = \"topic\",\n        value = value,\n        metadata = metadata,\n        partition = 0,\n        key = \"key\",\n        timestamp = 123L,\n        offset = 1L\n      )\n\n      val message2 = StoveMessage.consumed(\n        topic = \"topic\",\n        value = value.copyOf(),\n        metadata = metadata,\n        partition = 0,\n        key = \"key\",\n        timestamp = 123L,\n        offset = 1L\n      )\n\n      message1 shouldBe message2\n      message1.hashCode() shouldBe message2.hashCode()\n    }\n\n    test(\"consumed messages with different offsets should not be equal\") {\n      val metadata = MessageMetadata(\"topic\", \"key\", emptyMap())\n      val value = \"same-value\".toByteArray()\n\n      val message1 = StoveMessage.consumed(\n        topic = \"topic\",\n        value = value,\n        metadata = metadata,\n        offset = 1L\n      )\n\n      val message2 = StoveMessage.consumed(\n        topic = \"topic\",\n        value = value.copyOf(),\n        metadata = metadata,\n        offset = 2L\n      )\n\n      message1 shouldNotBe message2\n    }\n\n    test(\"failed messages with different reasons should not be equal\") {\n      val metadata = MessageMetadata(\"topic\", \"key\", emptyMap())\n      val value = \"value\".toByteArray()\n\n      val message1 = StoveMessage.failed(\n        topic = \"topic\",\n        value = value,\n        metadata = metadata,\n        reason = RuntimeException(\"Error 1\")\n      )\n\n      val message2 = StoveMessage.failed(\n        topic = \"topic\",\n        value = value.copyOf(),\n        metadata = metadata,\n        reason = RuntimeException(\"Error 2\")\n      )\n\n      message1 shouldNotBe message2\n    }\n\n    test(\"message with null optional fields should be created successfully\") {\n      val metadata = MessageMetadata(\"topic\", \"null\", emptyMap())\n      val message = StoveMessage.consumed(\n        topic = \"topic\",\n        value = \"value\".toByteArray(),\n        metadata = metadata\n      )\n\n      message.partition shouldBe null\n      message.key shouldBe null\n      message.timestamp shouldBe null\n      message.offset shouldBe null\n    }\n  })\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-kafka-tests/build.gradle.kts",
    "content": "dependencies {\n  api(projects.starters.spring.stoveSpringKafka)\n  implementation(libs.spring.boot.kafka)\n}\n\ndependencies {\n  testImplementation(testFixtures(projects.starters.spring.tests.springTestFixtures))\n  testImplementation(libs.spring.boot.autoconfigure)\n  testImplementation(projects.starters.spring.tests.spring2xTests)\n  testImplementation(libs.logback.classic)\n}\n\ntasks.test.configure {\n  systemProperty(\"kotest.framework.config.fqn\", \"com.trendyol.stove.kafka.Setup\")\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/protobufserde/ProtobufSerdeKafkaSystemTest.kt",
    "content": "package com.trendyol.stove.kafka.protobufserde\n\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.spring.springBoot\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport org.springframework.context.support.beans\n\n/**\n * Spring Boot 2.x Protobuf Serde Kafka tests.\n * Test cases are inherited from [ProtobufSerdeKafkaSystemTests] in fixtures.\n */\nclass Boot2xProtobufSerdeKafkaSystemTest : ProtobufSerdeKafkaSystemTests() {\n  init {\n    beforeSpec {\n      Stove()\n        .with {\n          kafka {\n            KafkaSystemOptions(\n              configureExposedConfiguration = {\n                listOf(\n                  \"kafka.bootstrapServers=${it.bootstrapServers}\",\n                  \"kafka.groupId=test-group\",\n                  \"kafka.offset=earliest\",\n                  \"kafka.schemaRegistryUrl=mock://mock-registry\"\n                )\n              },\n              containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n            )\n          }\n          springBoot(\n            runner = { params ->\n              KafkaTestSpringBotApplicationForProtobufSerde.run(params) {\n                addInitializers(\n                  beans {\n                    bean<TestSystemKafkaInterceptor<*, *>>()\n                    bean { StoveProtobufSerde() }\n                  }\n                )\n              }\n            },\n            withParameters = listOf(\n              \"spring.lifecycle.timeout-per-shutdown-phase=0s\"\n            )\n          )\n        }.run()\n    }\n\n    afterSpec {\n      Stove.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/protobufserde/app.kt",
    "content": "package com.trendyol.stove.kafka.protobufserde\n\nimport com.google.protobuf.Message\nimport com.trendyol.stove.kafka.KafkaRegistry // From fixtures\nimport com.trendyol.stove.kafka.StoveBusinessException // From fixtures\nimport io.confluent.kafka.streams.serdes.protobuf.KafkaProtobufSerde\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.*\nimport org.slf4j.*\nimport org.springframework.boot.*\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.properties.*\nimport org.springframework.context.ConfigurableApplicationContext\nimport org.springframework.context.annotation.Bean\nimport org.springframework.kafka.annotation.*\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.*\nimport org.springframework.util.backoff.FixedBackOff\n\nclass ProtobufValueSerializer<T : Any> : Serializer<T> {\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde()\n\n  override fun serialize(\n    topic: String,\n    data: T\n  ): ByteArray = when (data) {\n    is ByteArray -> data\n    else -> protobufSerde.serializer().serialize(topic, data as Message)\n  }\n}\n\nclass ProtobufValueDeserializer : Deserializer<Message> {\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde()\n\n  override fun deserialize(\n    topic: String,\n    data: ByteArray\n  ): Message = protobufSerde.deserializer().deserialize(topic, data)\n}\n\n@SpringBootApplication(scanBasePackages = [\"com.trendyol.stove.kafka.protobufserde\"])\n@EnableKafka\n@EnableConfigurationProperties(KafkaTestSpringBotApplicationForProtobufSerde.ProtobufSerdeKafkaConf::class)\nopen class KafkaTestSpringBotApplicationForProtobufSerde {\n  companion object {\n    fun run(\n      args: Array<String>,\n      init: SpringApplication.() -> Unit = {}\n    ): ConfigurableApplicationContext {\n      System.setProperty(\"org.springframework.boot.logging.LoggingSystem\", \"none\")\n      return runApplication<KafkaTestSpringBotApplicationForProtobufSerde>(args = args) {\n        webApplicationType = WebApplicationType.NONE\n        init()\n      }\n    }\n  }\n\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  @ConfigurationProperties(prefix = \"kafka\")\n  @ConstructorBinding\n  data class ProtobufSerdeKafkaConf(\n    val bootstrapServers: String,\n    val groupId: String,\n    val offset: String,\n    val schemaRegistryUrl: String\n  )\n\n  @Bean\n  open fun createConfiguredSerdeForRecordValues(config: ProtobufSerdeKafkaConf): KafkaProtobufSerde<Message> {\n    val registry = when {\n      config.schemaRegistryUrl.contains(\"mock://\") -> KafkaRegistry.Mock\n      else -> KafkaRegistry.Defined(config.schemaRegistryUrl)\n    }\n    return KafkaRegistry.createSerde(registry)\n  }\n\n  @Bean\n  open fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, String>,\n    interceptor: RecordInterceptor<String, String>,\n    recoverer: DeadLetterPublishingRecoverer\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.consumerFactory = consumerFactory\n    factory.setCommonErrorHandler(\n      DefaultErrorHandler(\n        recoverer,\n        FixedBackOff(20, 1)\n      ).also { it.addNotRetryableExceptions(StoveBusinessException::class.java) }\n    )\n    factory.setRecordInterceptor(interceptor)\n    return factory\n  }\n\n  @Bean\n  open fun recoverer(\n    kafkaTemplate: KafkaTemplate<*, *>\n  ): DeadLetterPublishingRecoverer = DeadLetterPublishingRecoverer(kafkaTemplate)\n\n  @Bean\n  open fun consumerFactory(\n    config: ProtobufSerdeKafkaConf\n  ): ConsumerFactory<String, String> = DefaultKafkaConsumerFactory(\n    mapOf(\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n      ConsumerConfig.GROUP_ID_CONFIG to config.groupId,\n      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to config.offset,\n      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to Serdes.String().deserializer().javaClass,\n      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to ProtobufValueDeserializer().javaClass,\n      ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to 10000,\n      ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG to 30000,\n      ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 30000\n    )\n  )\n\n  @Bean\n  open fun kafkaTemplate(\n    config: ProtobufSerdeKafkaConf\n  ): KafkaTemplate<String, String> = KafkaTemplate(\n    DefaultKafkaProducerFactory(\n      mapOf(\n        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to Serdes.String().serializer().javaClass,\n        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to ProtobufValueSerializer<Any>().javaClass,\n        ProducerConfig.ACKS_CONFIG to \"1\"\n      )\n    )\n  )\n\n  @KafkaListener(topics = [\"topic-protobuf\"], groupId = \"group_id\")\n  fun listen(message: Message) {\n    logger.info(\"Received Message in consumer: $message\")\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/shared.kt",
    "content": "package com.trendyol.stove.kafka\n\n/** Spring Boot 2.x Kafka test setup - uses shared fixtures */\nclass Setup : KafkaTestSetup()\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/stringserde/StringSerdeKafkaSystemTest.kt",
    "content": "package com.trendyol.stove.kafka.stringserde\n\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.spring.springBoot\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport org.springframework.context.support.beans\n\n/**\n * Spring Boot 2.x String Serde Kafka tests.\n * Test cases are inherited from [StringSerdeKafkaSystemTests] in fixtures.\n */\nclass Boot2xStringSerdeKafkaSystemTests : StringSerdeKafkaSystemTests() {\n  init {\n    beforeSpec {\n      Stove()\n        .with {\n          kafka {\n            KafkaSystemOptions(\n              configureExposedConfiguration = {\n                listOf(\n                  \"kafka.bootstrapServers=${it.bootstrapServers}\",\n                  \"kafka.groupId=test-group\",\n                  \"kafka.offset=earliest\"\n                )\n              },\n              containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n            )\n          }\n          springBoot(\n            runner = { params ->\n              KafkaTestSpringBotApplicationForStringSerde.run(params) {\n                addInitializers(\n                  beans {\n                    bean<TestSystemKafkaInterceptor<*, *>>()\n                    bean { StoveSerde.jackson.anyByteArraySerde() }\n                  }\n                )\n              }\n            },\n            withParameters = listOf(\n              \"spring.lifecycle.timeout-per-shutdown-phase=0s\"\n            )\n          )\n        }.run()\n    }\n\n    afterSpec {\n      Stove.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/stringserde/app.kt",
    "content": "package com.trendyol.stove.kafka.stringserde\n\nimport com.trendyol.stove.kafka.StoveBusinessException // From fixtures\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.Serdes\nimport org.slf4j.*\nimport org.springframework.boot.*\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.properties.*\nimport org.springframework.context.ConfigurableApplicationContext\nimport org.springframework.context.annotation.Bean\nimport org.springframework.kafka.annotation.*\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.*\nimport org.springframework.util.backoff.FixedBackOff\n\n@SpringBootApplication(scanBasePackages = [\"com.trendyol.stove.kafka.stringserde\"])\n@EnableKafka\n@EnableConfigurationProperties(KafkaTestSpringBotApplicationForStringSerde.StringSerdeKafkaConf::class)\nopen class KafkaTestSpringBotApplicationForStringSerde {\n  companion object {\n    fun run(\n      args: Array<String>,\n      init: SpringApplication.() -> Unit = {}\n    ): ConfigurableApplicationContext {\n      System.setProperty(\"org.springframework.boot.logging.LoggingSystem\", \"none\")\n      return runApplication<KafkaTestSpringBotApplicationForStringSerde>(args = args) {\n        webApplicationType = WebApplicationType.NONE\n        init()\n      }\n    }\n  }\n\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  @ConfigurationProperties(prefix = \"kafka\")\n  @ConstructorBinding\n  data class StringSerdeKafkaConf(\n    val bootstrapServers: String,\n    val groupId: String,\n    val offset: String\n  )\n\n  @Bean\n  open fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, String>,\n    interceptor: RecordInterceptor<String, String>,\n    recoverer: DeadLetterPublishingRecoverer\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.consumerFactory = consumerFactory\n    factory.setCommonErrorHandler(\n      DefaultErrorHandler(\n        recoverer,\n        FixedBackOff(20, 1)\n      ).also { it.addNotRetryableExceptions(StoveBusinessException::class.java) }\n    )\n    factory.setRecordInterceptor(interceptor)\n    return factory\n  }\n\n  @Bean\n  open fun recoverer(\n    kafkaTemplate: KafkaTemplate<*, *>\n  ): DeadLetterPublishingRecoverer = DeadLetterPublishingRecoverer(kafkaTemplate)\n\n  @Bean\n  open fun consumerFactory(\n    config: StringSerdeKafkaConf\n  ): ConsumerFactory<String, String> = DefaultKafkaConsumerFactory(\n    mapOf(\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n      ConsumerConfig.GROUP_ID_CONFIG to config.groupId,\n      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to config.offset,\n      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to Serdes.String().deserializer().javaClass,\n      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to Serdes.String().deserializer().javaClass,\n      ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to 10000,\n      ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG to 30000,\n      ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 30000\n    )\n  )\n\n  @Bean\n  open fun kafkaTemplate(\n    config: StringSerdeKafkaConf\n  ): KafkaTemplate<String, String> = KafkaTemplate(\n    DefaultKafkaProducerFactory(\n      mapOf(\n        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to Serdes.String().serializer().javaClass,\n        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to Serdes.String().serializer().javaClass,\n        ProducerConfig.ACKS_CONFIG to \"1\"\n      )\n    )\n  )\n\n  @KafkaListener(topics = [\"topic\"], groupId = \"group_id\")\n  fun listen(message: String) {\n    logger.info(\"Received Message in consumer: \\n$message\")\n  }\n\n  @KafkaListener(topics = [\"topic-failed\"], groupId = \"group_id\")\n  fun listenFailed(message: String) {\n    logger.info(\"Received Message in failed consumer: \\n$message\")\n    throw StoveBusinessException(\"This exception is thrown intentionally for testing purposes.\")\n  }\n\n  @KafkaListener(topics = [\"topic-failed.DLT\"], groupId = \"group_id\")\n  fun listenDeadLetter(message: String) {\n    logger.info(\"Received Message in the lead letter, and allowing the fail by just logging: \\n$message\")\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-kafka-tests/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.kafka.Setup\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-kafka-tests/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-tests/build.gradle.kts",
    "content": "dependencies {\n  api(projects.starters.spring.stoveSpring)\n  implementation(libs.spring.boot)\n\n  testImplementation(projects.testExtensions.stoveExtensionsKotest)\n  testImplementation(testFixtures(projects.starters.spring.tests.springTestFixtures))\n  testImplementation(libs.spring.boot.autoconfigure)\n  testImplementation(libs.slf4j.simple)\n}\n\ntasks.test.configure {\n  systemProperty(\"kotest.framework.config.fqn\", \"com.trendyol.stove.Stove\")\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-tests/src/test/kotlin/com/trendyol/stove/StoveConfig.kt",
    "content": "package com.trendyol.stove\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.Stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.runApplication\n\n@SpringBootApplication\nopen class TestSpringBootApp\n\n/**\n * Spring Boot 2.x test setup.\n * Uses [com.trendyol.stove.spring.stoveSpringRegistrar] with `bean<T>()` DSL.\n */\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        bridge()\n        springBoot(\n          runner = { params ->\n            runApplication<TestSpringBootApp>(args = params) {\n              addInitializers(\n                stoveSpringRegistrar {\n                  bean<ParameterCollectorOfSpringBoot>()\n                  bean<TestAppInitializers>()\n                  bean<ObjectMapper> { StoveSerde.jackson.default }\n                  bean { SystemTimeGetUtcNow() }\n                }\n              )\n            }\n          },\n          withParameters = listOf(\"context=SetupOfBridgeSystemTests\")\n        )\n      }.run()\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n\n/** Concrete test class for Spring Boot 2.x - inherits all tests from fixtures */\nclass Boot2xBridgeSystemTests : BridgeSystemTests()\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-tests/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.StoveConfig\n"
  },
  {
    "path": "starters/spring/tests/spring-2x-tests/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-kafka-tests/build.gradle.kts",
    "content": "dependencies {\n  api(projects.starters.spring.stoveSpringKafka)\n  implementation(libs.spring.boot.three.kafka)\n}\n\ndependencies {\n  testImplementation(testFixtures(projects.starters.spring.tests.springTestFixtures))\n  testImplementation(libs.spring.boot.three.autoconfigure)\n  testImplementation(projects.starters.spring.tests.spring3xTests)\n  testImplementation(libs.logback.classic)\n}\n\ntasks.test.configure {\n  systemProperty(\"kotest.framework.config.fqn\", \"com.trendyol.stove.kafka.Setup\")\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/protobufserde/ProtobufSerdeKafkaSystemTest.kt",
    "content": "package com.trendyol.stove.kafka.protobufserde\n\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.spring.springBoot\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport org.springframework.context.support.beans\n\n/**\n * Spring Boot 3.x Protobuf Serde Kafka tests.\n * Test cases are inherited from [ProtobufSerdeKafkaSystemTests] in fixtures.\n */\nclass Boot3xProtobufSerdeKafkaSystemTest : ProtobufSerdeKafkaSystemTests() {\n  init {\n    beforeSpec {\n      Stove()\n        .with {\n          kafka {\n            KafkaSystemOptions(\n              configureExposedConfiguration = {\n                listOf(\n                  \"kafka.bootstrapServers=${it.bootstrapServers}\",\n                  \"kafka.groupId=test-group\",\n                  \"kafka.offset=earliest\",\n                  \"kafka.schemaRegistryUrl=mock://mock-registry\"\n                )\n              },\n              containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n            )\n          }\n          springBoot(\n            runner = { params ->\n              KafkaTestSpringBotApplicationForProtobufSerde.run(params) {\n                addInitializers(\n                  beans {\n                    bean<TestSystemKafkaInterceptor<*, *>>()\n                    bean { StoveProtobufSerde() }\n                  }\n                )\n              }\n            },\n            withParameters = listOf(\n              \"spring.lifecycle.timeout-per-shutdown-phase=0s\"\n            )\n          )\n        }.run()\n    }\n\n    afterSpec {\n      Stove.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/protobufserde/app.kt",
    "content": "package com.trendyol.stove.kafka.protobufserde\n\nimport com.google.protobuf.Message\nimport com.trendyol.stove.kafka.KafkaRegistry // From fixtures\nimport com.trendyol.stove.kafka.StoveBusinessException // From fixtures\nimport io.confluent.kafka.streams.serdes.protobuf.KafkaProtobufSerde\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.*\nimport org.slf4j.*\nimport org.springframework.boot.*\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.properties.*\nimport org.springframework.context.ConfigurableApplicationContext\nimport org.springframework.context.annotation.Bean\nimport org.springframework.kafka.annotation.*\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.*\nimport org.springframework.util.backoff.FixedBackOff\n\nclass ProtobufValueSerializer<T : Any> : Serializer<T> {\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde()\n\n  override fun serialize(\n    topic: String,\n    data: T\n  ): ByteArray = when (data) {\n    is ByteArray -> data\n    else -> protobufSerde.serializer().serialize(topic, data as Message)\n  }\n}\n\nclass ProtobufValueDeserializer : Deserializer<Message> {\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde()\n\n  override fun deserialize(\n    topic: String,\n    data: ByteArray\n  ): Message = protobufSerde.deserializer().deserialize(topic, data)\n}\n\n@SpringBootApplication(scanBasePackages = [\"com.trendyol.stove.kafka.protobufserde\"])\n@EnableKafka\n@EnableConfigurationProperties(KafkaTestSpringBotApplicationForProtobufSerde.ProtobufSerdeKafkaConf::class)\nopen class KafkaTestSpringBotApplicationForProtobufSerde {\n  companion object {\n    fun run(\n      args: Array<String>,\n      init: SpringApplication.() -> Unit = {}\n    ): ConfigurableApplicationContext {\n      System.setProperty(\"org.springframework.boot.logging.LoggingSystem\", \"none\")\n      return runApplication<KafkaTestSpringBotApplicationForProtobufSerde>(args = args) {\n        webApplicationType = WebApplicationType.NONE\n        init()\n      }\n    }\n  }\n\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  @ConfigurationProperties(prefix = \"kafka\")\n  data class ProtobufSerdeKafkaConf(\n    val bootstrapServers: String,\n    val groupId: String,\n    val offset: String,\n    val schemaRegistryUrl: String\n  )\n\n  @Bean\n  open fun createConfiguredSerdeForRecordValues(config: ProtobufSerdeKafkaConf): KafkaProtobufSerde<Message> {\n    val registry = when {\n      config.schemaRegistryUrl.contains(\"mock://\") -> KafkaRegistry.Mock\n      else -> KafkaRegistry.Defined(config.schemaRegistryUrl)\n    }\n    return KafkaRegistry.createSerde(registry)\n  }\n\n  @Bean\n  open fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, String>,\n    interceptor: RecordInterceptor<String, String>,\n    recoverer: DeadLetterPublishingRecoverer\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.consumerFactory = consumerFactory\n    factory.setCommonErrorHandler(\n      DefaultErrorHandler(\n        recoverer,\n        FixedBackOff(20, 1)\n      ).also { it.addNotRetryableExceptions(StoveBusinessException::class.java) }\n    )\n    factory.setRecordInterceptor(interceptor)\n    return factory\n  }\n\n  @Bean\n  open fun recoverer(\n    kafkaTemplate: KafkaTemplate<*, *>\n  ): DeadLetterPublishingRecoverer = DeadLetterPublishingRecoverer(kafkaTemplate)\n\n  @Bean\n  open fun consumerFactory(\n    config: ProtobufSerdeKafkaConf\n  ): ConsumerFactory<String, String> = DefaultKafkaConsumerFactory(\n    mapOf(\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n      ConsumerConfig.GROUP_ID_CONFIG to config.groupId,\n      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to config.offset,\n      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to Serdes.String().deserializer().javaClass,\n      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to ProtobufValueDeserializer().javaClass,\n      ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to 10000,\n      ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG to 30000,\n      ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 30000\n    )\n  )\n\n  @Bean\n  open fun kafkaTemplate(\n    config: ProtobufSerdeKafkaConf\n  ): KafkaTemplate<String, String> = KafkaTemplate(\n    DefaultKafkaProducerFactory(\n      mapOf(\n        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to Serdes.String().serializer().javaClass,\n        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to ProtobufValueSerializer<Any>().javaClass,\n        ProducerConfig.ACKS_CONFIG to \"1\"\n      )\n    )\n  )\n\n  @KafkaListener(topics = [\"topic-protobuf\"], groupId = \"group_id\")\n  fun listen(message: Message) {\n    logger.info(\"Received Message in consumer: $message\")\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/shared.kt",
    "content": "package com.trendyol.stove.kafka\n\n/** Spring Boot 3.x Kafka test setup - uses shared fixtures */\nclass Setup : KafkaTestSetup()\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/stringserde/StringSerdeKafkaSystemTest.kt",
    "content": "package com.trendyol.stove.kafka.stringserde\n\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.spring.springBoot\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\nimport org.springframework.context.support.beans\n\n/**\n * Spring Boot 3.x String Serde Kafka tests.\n * Test cases are inherited from [StringSerdeKafkaSystemTests] in fixtures.\n */\nclass Boot3xStringSerdeKafkaSystemTests : StringSerdeKafkaSystemTests(dltTopicSuffix = \"-dlt\") {\n  init {\n    beforeSpec {\n      Stove()\n        .with {\n          kafka {\n            KafkaSystemOptions(\n              configureExposedConfiguration = {\n                listOf(\n                  \"kafka.bootstrapServers=${it.bootstrapServers}\",\n                  \"kafka.groupId=test-group\",\n                  \"kafka.offset=earliest\"\n                )\n              },\n              containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n            )\n          }\n          springBoot(\n            runner = { params ->\n              KafkaTestSpringBotApplicationForStringSerde.run(params) {\n                addInitializers(\n                  beans {\n                    bean<TestSystemKafkaInterceptor<*, *>>()\n                    bean { StoveSerde.jackson.anyByteArraySerde() }\n                  }\n                )\n              }\n            },\n            withParameters = listOf(\n              \"spring.lifecycle.timeout-per-shutdown-phase=0s\"\n            )\n          )\n        }.run()\n    }\n\n    afterSpec {\n      Stove.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/stringserde/app.kt",
    "content": "package com.trendyol.stove.kafka.stringserde\n\nimport com.trendyol.stove.kafka.StoveBusinessException // From fixtures\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.Serdes\nimport org.slf4j.*\nimport org.springframework.boot.*\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.properties.*\nimport org.springframework.context.ConfigurableApplicationContext\nimport org.springframework.context.annotation.Bean\nimport org.springframework.kafka.annotation.*\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.*\nimport org.springframework.util.backoff.FixedBackOff\n\n@SpringBootApplication(scanBasePackages = [\"com.trendyol.stove.kafka.stringserde\"])\n@EnableKafka\n@EnableConfigurationProperties(KafkaTestSpringBotApplicationForStringSerde.StringSerdeKafkaConf::class)\nopen class KafkaTestSpringBotApplicationForStringSerde {\n  companion object {\n    fun run(\n      args: Array<String>,\n      init: SpringApplication.() -> Unit = {}\n    ): ConfigurableApplicationContext {\n      System.setProperty(\"org.springframework.boot.logging.LoggingSystem\", \"none\")\n      return runApplication<KafkaTestSpringBotApplicationForStringSerde>(args = args) {\n        webApplicationType = WebApplicationType.NONE\n        init()\n      }\n    }\n  }\n\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  @ConfigurationProperties(prefix = \"kafka\")\n  data class StringSerdeKafkaConf(\n    val bootstrapServers: String,\n    val groupId: String,\n    val offset: String\n  )\n\n  @Bean\n  open fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, String>,\n    interceptor: RecordInterceptor<String, String>,\n    recoverer: DeadLetterPublishingRecoverer\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.consumerFactory = consumerFactory\n    factory.setCommonErrorHandler(\n      DefaultErrorHandler(\n        recoverer,\n        FixedBackOff(20, 1)\n      ).also { it.addNotRetryableExceptions(StoveBusinessException::class.java) }\n    )\n    factory.setRecordInterceptor(interceptor)\n    return factory\n  }\n\n  @Bean\n  open fun recoverer(\n    kafkaTemplate: KafkaTemplate<*, *>\n  ): DeadLetterPublishingRecoverer = DeadLetterPublishingRecoverer(kafkaTemplate)\n\n  @Bean\n  open fun consumerFactory(\n    config: StringSerdeKafkaConf\n  ): ConsumerFactory<String, String> = DefaultKafkaConsumerFactory(\n    mapOf(\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n      ConsumerConfig.GROUP_ID_CONFIG to config.groupId,\n      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to config.offset,\n      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to Serdes.String().deserializer().javaClass,\n      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to Serdes.String().deserializer().javaClass,\n      ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to 10000,\n      ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG to 30000,\n      ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 30000\n    )\n  )\n\n  @Bean\n  open fun kafkaTemplate(\n    config: StringSerdeKafkaConf\n  ): KafkaTemplate<String, String> = KafkaTemplate(\n    DefaultKafkaProducerFactory(\n      mapOf(\n        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to Serdes.String().serializer().javaClass,\n        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to Serdes.String().serializer().javaClass,\n        ProducerConfig.ACKS_CONFIG to \"1\"\n      )\n    )\n  )\n\n  @KafkaListener(topics = [\"topic\"], groupId = \"group_id\")\n  fun listen(message: String) {\n    logger.info(\"Received Message in consumer: \\n$message\")\n  }\n\n  @KafkaListener(topics = [\"topic-failed\"], groupId = \"group_id\")\n  fun listenFailed(message: String) {\n    logger.info(\"Received Message in failed consumer: \\n$message\")\n    throw StoveBusinessException(\"This exception is thrown intentionally for testing purposes.\")\n  }\n\n  @KafkaListener(topics = [\"topic-failed-dlt\"], groupId = \"group_id\")\n  fun listenDeadLetter(message: String) {\n    logger.info(\"Received Message in the lead letter, and allowing the fail by just logging: \\n$message\")\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-kafka-tests/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.kafka.Setup\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-kafka-tests/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-tests/build.gradle.kts",
    "content": "dependencies {\n  api(projects.starters.spring.stoveSpring)\n  implementation(libs.spring.boot.three)\n\n  testImplementation(projects.testExtensions.stoveExtensionsKotest)\n  testImplementation(testFixtures(projects.starters.spring.tests.springTestFixtures))\n  testImplementation(libs.spring.boot.three.autoconfigure)\n  testImplementation(libs.slf4j.simple)\n}\n\ntasks.test.configure {\n  systemProperty(\"kotest.framework.config.fqn\", \"com.trendyol.stove.Stove\")\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-tests/src/test/kotlin/com/trendyol/stove/StoveConfig.kt",
    "content": "package com.trendyol.stove\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.Stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.runApplication\n\n@SpringBootApplication\nopen class TestSpringBootApp\n\n/**\n * Spring Boot 3.x test setup.\n * Uses [com.trendyol.stove.spring.stoveSpringRegistrar] with `bean<T>()` DSL.\n */\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        bridge()\n        springBoot(\n          runner = { params ->\n            runApplication<TestSpringBootApp>(args = params) {\n              addInitializers(\n                stoveSpringRegistrar {\n                  bean<ParameterCollectorOfSpringBoot>()\n                  bean<TestAppInitializers>()\n                  bean<ObjectMapper> { StoveSerde.jackson.default }\n                  bean { SystemTimeGetUtcNow() }\n                }\n              )\n            }\n          },\n          withParameters = listOf(\"context=SetupOfBridgeSystemTests\")\n        )\n      }.run()\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n\n/** Concrete test class for Spring Boot 3.x - inherits all tests from fixtures */\nclass Boot3xBridgeSystemTests : BridgeSystemTests()\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-tests/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.StoveConfig\n"
  },
  {
    "path": "starters/spring/tests/spring-3x-tests/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-kafka-tests/build.gradle.kts",
    "content": "dependencies {\n  api(projects.starters.spring.stoveSpringKafka)\n  implementation(libs.spring.boot.four.kafka)\n}\n\ndependencies {\n  testImplementation(testFixtures(projects.starters.spring.tests.springTestFixtures))\n  testImplementation(libs.spring.boot.four.autoconfigure)\n  testImplementation(projects.starters.spring.tests.spring4xTests)\n  testImplementation(libs.logback.classic)\n}\n\ntasks.test.configure {\n  systemProperty(\"kotest.framework.config.fqn\", \"com.trendyol.stove.kafka.Setup\")\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/protobufserde/ProtobufSerdeKafkaSystemTest.kt",
    "content": "package com.trendyol.stove.kafka.protobufserde\n\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.spring.springBoot\nimport com.trendyol.stove.spring.stoveSpring4xRegistrar\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\n\n/**\n * Spring Boot 4.x Protobuf Serde Kafka tests.\n * Test cases are inherited from [ProtobufSerdeKafkaSystemTests] in fixtures.\n */\nclass Boot4xProtobufSerdeKafkaSystemTest : ProtobufSerdeKafkaSystemTests() {\n  init {\n    beforeSpec {\n      Stove()\n        .with {\n          kafka {\n            KafkaSystemOptions(\n              configureExposedConfiguration = {\n                listOf(\n                  \"kafka.bootstrapServers=${it.bootstrapServers}\",\n                  \"kafka.groupId=test-group\",\n                  \"kafka.offset=earliest\",\n                  \"kafka.schemaRegistryUrl=mock://mock-registry\"\n                )\n              },\n              containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n            )\n          }\n          springBoot(\n            runner = { params ->\n              KafkaTestSpringBotApplicationForProtobufSerde.run(params) {\n                addInitializers(\n                  stoveSpring4xRegistrar {\n                    registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)\n                    registerBean { StoveProtobufSerde() }\n                  }\n                )\n              }\n            },\n            withParameters = listOf(\n              \"spring.lifecycle.timeout-per-shutdown-phase=0s\"\n            )\n          )\n        }.run()\n    }\n\n    afterSpec {\n      Stove.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/protobufserde/app.kt",
    "content": "package com.trendyol.stove.kafka.protobufserde\n\nimport com.google.protobuf.Message\nimport com.trendyol.stove.kafka.KafkaRegistry // From fixtures\nimport com.trendyol.stove.kafka.StoveBusinessException // From fixtures\nimport io.confluent.kafka.streams.serdes.protobuf.KafkaProtobufSerde\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.*\nimport org.slf4j.*\nimport org.springframework.boot.*\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.properties.*\nimport org.springframework.context.ConfigurableApplicationContext\nimport org.springframework.context.annotation.Bean\nimport org.springframework.kafka.annotation.*\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.*\nimport org.springframework.util.backoff.FixedBackOff\n\nclass ProtobufValueSerializer<T : Any> : Serializer<T> {\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde()\n\n  override fun serialize(\n    topic: String,\n    data: T\n  ): ByteArray = when (data) {\n    is ByteArray -> data\n    else -> protobufSerde.serializer().serialize(topic, data as Message)\n  }\n}\n\nclass ProtobufValueDeserializer : Deserializer<Message> {\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde()\n\n  override fun deserialize(\n    topic: String,\n    data: ByteArray\n  ): Message = protobufSerde.deserializer().deserialize(topic, data)\n}\n\n@SpringBootApplication(scanBasePackages = [\"com.trendyol.stove.kafka.protobufserde\"])\n@EnableKafka\n@EnableConfigurationProperties(KafkaTestSpringBotApplicationForProtobufSerde.ProtobufSerdeKafkaConf::class)\nopen class KafkaTestSpringBotApplicationForProtobufSerde {\n  companion object {\n    fun run(\n      args: Array<String>,\n      init: SpringApplication.() -> Unit = {}\n    ): ConfigurableApplicationContext {\n      System.setProperty(\"org.springframework.boot.logging.LoggingSystem\", \"none\")\n      return runApplication<KafkaTestSpringBotApplicationForProtobufSerde>(args = args) {\n        setWebApplicationType(WebApplicationType.NONE)\n        init()\n      }\n    }\n  }\n\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  @ConfigurationProperties(prefix = \"kafka\")\n  data class ProtobufSerdeKafkaConf(\n    val bootstrapServers: String,\n    val groupId: String,\n    val offset: String,\n    val schemaRegistryUrl: String\n  )\n\n  @Bean\n  open fun createConfiguredSerdeForRecordValues(config: ProtobufSerdeKafkaConf): KafkaProtobufSerde<Message> {\n    val registry = when {\n      config.schemaRegistryUrl.contains(\"mock://\") -> KafkaRegistry.Mock\n      else -> KafkaRegistry.Defined(config.schemaRegistryUrl)\n    }\n    return KafkaRegistry.createSerde(registry)\n  }\n\n  @Bean\n  open fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, String>,\n    interceptor: RecordInterceptor<String, String>,\n    recoverer: DeadLetterPublishingRecoverer\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.setConsumerFactory(consumerFactory)\n    factory.setCommonErrorHandler(\n      DefaultErrorHandler(\n        recoverer,\n        FixedBackOff(20, 1)\n      ).also { it.addNotRetryableExceptions(StoveBusinessException::class.java) }\n    )\n    factory.setRecordInterceptor(interceptor)\n    return factory\n  }\n\n  @Bean\n  open fun recoverer(\n    kafkaTemplate: KafkaTemplate<*, *>\n  ): DeadLetterPublishingRecoverer = DeadLetterPublishingRecoverer(kafkaTemplate)\n\n  @Bean\n  open fun consumerFactory(\n    config: ProtobufSerdeKafkaConf\n  ): ConsumerFactory<String, String> = DefaultKafkaConsumerFactory(\n    mapOf(\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n      ConsumerConfig.GROUP_ID_CONFIG to config.groupId,\n      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to config.offset,\n      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to Serdes.String().deserializer().javaClass,\n      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to ProtobufValueDeserializer().javaClass,\n      ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to 10000,\n      ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG to 30000,\n      ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 30000\n    )\n  )\n\n  @Bean\n  open fun kafkaTemplate(\n    config: ProtobufSerdeKafkaConf\n  ): KafkaTemplate<String, String> = KafkaTemplate(\n    DefaultKafkaProducerFactory(\n      mapOf(\n        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to Serdes.String().serializer().javaClass,\n        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to ProtobufValueSerializer<Any>().javaClass,\n        ProducerConfig.ACKS_CONFIG to \"1\"\n      )\n    )\n  )\n\n  @KafkaListener(topics = [\"topic-protobuf\"], groupId = \"group_id\")\n  fun listen(message: Message) {\n    logger.info(\"Received Message in consumer: $message\")\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/shared.kt",
    "content": "package com.trendyol.stove.kafka\n\n/** Spring Boot 4.x Kafka test setup - uses shared fixtures */\nclass Setup : KafkaTestSetup()\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/stringserde/StringSerdeKafkaSystemTest.kt",
    "content": "package com.trendyol.stove.kafka.stringserde\n\nimport com.trendyol.stove.kafka.*\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.spring.springBoot\nimport com.trendyol.stove.spring.stoveSpring4xRegistrar\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.system.stove\n\n/**\n * Spring Boot 4.x String Serde Kafka tests.\n * Test cases are inherited from [StringSerdeKafkaSystemTests] in fixtures.\n * Uses [com.trendyol.stove.spring.stoveSpring4xRegistrar] with `registerBean<T>()` DSL.\n */\nclass Boot4xStringSerdeKafkaSystemTests : StringSerdeKafkaSystemTests(dltTopicSuffix = \"-dlt\") {\n  init {\n    beforeSpec {\n      Stove()\n        .with {\n          kafka {\n            KafkaSystemOptions(\n              configureExposedConfiguration = {\n                listOf(\n                  \"kafka.bootstrapServers=${it.bootstrapServers}\",\n                  \"kafka.groupId=test-group\",\n                  \"kafka.offset=earliest\"\n                )\n              },\n              containerOptions = KafkaContainerOptions(tag = \"8.0.3\")\n            )\n          }\n          springBoot(\n            runner = { params ->\n              KafkaTestSpringBotApplicationForStringSerde.run(params) {\n                addInitializers(\n                  stoveSpring4xRegistrar {\n                    registerBean<TestSystemKafkaInterceptor<*, *>>(primary = true)\n                    registerBean { StoveSerde.jackson.anyByteArraySerde() }\n                  }\n                )\n              }\n            },\n            withParameters = listOf(\n              \"spring.lifecycle.timeout-per-shutdown-phase=0s\"\n            )\n          )\n        }.run()\n    }\n\n    afterSpec {\n      Stove.stop()\n    }\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-kafka-tests/src/test/kotlin/com/trendyol/stove/kafka/stringserde/app.kt",
    "content": "package com.trendyol.stove.kafka.stringserde\n\nimport com.trendyol.stove.kafka.StoveBusinessException // From fixtures\nimport org.apache.kafka.clients.consumer.ConsumerConfig\nimport org.apache.kafka.clients.producer.ProducerConfig\nimport org.apache.kafka.common.serialization.Serdes\nimport org.slf4j.*\nimport org.springframework.boot.*\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.context.properties.*\nimport org.springframework.context.ConfigurableApplicationContext\nimport org.springframework.context.annotation.Bean\nimport org.springframework.kafka.annotation.*\nimport org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory\nimport org.springframework.kafka.core.*\nimport org.springframework.kafka.listener.*\nimport org.springframework.util.backoff.FixedBackOff\n\n@SpringBootApplication(scanBasePackages = [\"com.trendyol.stove.kafka.stringserde\"])\n@EnableKafka\n@EnableConfigurationProperties(KafkaTestSpringBotApplicationForStringSerde.StringSerdeKafkaConf::class)\nopen class KafkaTestSpringBotApplicationForStringSerde {\n  companion object {\n    fun run(\n      args: Array<String>,\n      init: SpringApplication.() -> Unit = {}\n    ): ConfigurableApplicationContext {\n      System.setProperty(\"org.springframework.boot.logging.LoggingSystem\", \"none\")\n      return runApplication<KafkaTestSpringBotApplicationForStringSerde>(args = args) {\n        setWebApplicationType(WebApplicationType.NONE)\n        init()\n      }\n    }\n  }\n\n  private val logger: Logger = LoggerFactory.getLogger(javaClass)\n\n  @ConfigurationProperties(prefix = \"kafka\")\n  data class StringSerdeKafkaConf(\n    val bootstrapServers: String,\n    val groupId: String,\n    val offset: String\n  )\n\n  @Bean\n  open fun kafkaListenerContainerFactory(\n    consumerFactory: ConsumerFactory<String, String>,\n    interceptor: RecordInterceptor<String, String>,\n    recoverer: DeadLetterPublishingRecoverer\n  ): ConcurrentKafkaListenerContainerFactory<String, String> {\n    val factory = ConcurrentKafkaListenerContainerFactory<String, String>()\n    factory.setConsumerFactory(consumerFactory)\n    factory.setCommonErrorHandler(\n      DefaultErrorHandler(\n        recoverer,\n        FixedBackOff(20, 1)\n      ).also { it.addNotRetryableExceptions(StoveBusinessException::class.java) }\n    )\n    factory.setRecordInterceptor(interceptor)\n    return factory\n  }\n\n  @Bean\n  open fun recoverer(\n    kafkaTemplate: KafkaTemplate<*, *>\n  ): DeadLetterPublishingRecoverer = DeadLetterPublishingRecoverer(kafkaTemplate)\n\n  @Bean\n  open fun consumerFactory(\n    config: StringSerdeKafkaConf\n  ): ConsumerFactory<String, String> = DefaultKafkaConsumerFactory(\n    mapOf(\n      ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n      ConsumerConfig.GROUP_ID_CONFIG to config.groupId,\n      ConsumerConfig.AUTO_OFFSET_RESET_CONFIG to config.offset,\n      ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to Serdes.String().deserializer().javaClass,\n      ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to Serdes.String().deserializer().javaClass,\n      ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG to 10000,\n      ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG to 30000,\n      ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG to 30000\n    )\n  )\n\n  @Bean\n  open fun kafkaTemplate(\n    config: StringSerdeKafkaConf\n  ): KafkaTemplate<String, String> = KafkaTemplate(\n    DefaultKafkaProducerFactory(\n      mapOf(\n        ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to config.bootstrapServers,\n        ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to Serdes.String().serializer().javaClass,\n        ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to Serdes.String().serializer().javaClass,\n        ProducerConfig.ACKS_CONFIG to \"1\"\n      )\n    )\n  )\n\n  @KafkaListener(topics = [\"topic\"], groupId = \"group_id\")\n  fun listen(message: String) {\n    logger.info(\"Received Message in consumer: \\n$message\")\n  }\n\n  @KafkaListener(topics = [\"topic-failed\"], groupId = \"group_id\")\n  fun listenFailed(message: String) {\n    logger.info(\"Received Message in failed consumer: \\n$message\")\n    throw StoveBusinessException(\"This exception is thrown intentionally for testing purposes.\")\n  }\n\n  @KafkaListener(topics = [\"topic-failed-dlt\"], groupId = \"group_id\")\n  fun listenDeadLetter(message: String) {\n    logger.info(\"Received Message in the lead letter, and allowing the fail by just logging: \\n$message\")\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-kafka-tests/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.kafka.Setup\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-kafka-tests/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-tests/build.gradle.kts",
    "content": "dependencies {\n  api(projects.starters.spring.stoveSpring)\n  implementation(libs.spring.boot.four)\n\n  testImplementation(projects.testExtensions.stoveExtensionsKotest)\n  testImplementation(testFixtures(projects.starters.spring.tests.springTestFixtures))\n  testImplementation(libs.spring.boot.four.autoconfigure)\n  testImplementation(libs.slf4j.simple)\n}\n\ntasks.test.configure {\n  systemProperty(\"kotest.framework.config.fqn\", \"com.trendyol.stove.Stove\")\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-tests/src/test/kotlin/com/trendyol/stove/StoveConfig.kt",
    "content": "package com.trendyol.stove\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.trendyol.stove.extensions.kotest.StoveKotestExtension\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.spring.*\nimport com.trendyol.stove.system.Stove\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport org.springframework.boot.autoconfigure.SpringBootApplication\nimport org.springframework.boot.runApplication\n\n@SpringBootApplication\nopen class TestSpringBootApp\n\n/**\n * Spring Boot 4.x test setup.\n * Uses [com.trendyol.stove.spring.stoveSpring4xRegistrar] with `registerBean<T>()` DSL.\n */\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        bridge()\n        springBoot(\n          runner = { params ->\n            runApplication<TestSpringBootApp>(args = params) {\n              addInitializers(\n                stoveSpring4xRegistrar {\n                  registerBean<ParameterCollectorOfSpringBoot>()\n                  registerBean<TestAppInitializers>()\n                  registerBean<ObjectMapper> { StoveSerde.jackson.default }\n                  registerBean { SystemTimeGetUtcNow() }\n                }\n              )\n            }\n          },\n          withParameters = listOf(\"context=SetupOfBridgeSystemTests\")\n        )\n      }.run()\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n\n/** Concrete test class for Spring Boot 4.x - inherits all tests from fixtures */\nclass Boot4xBridgeSystemTests : BridgeSystemTests()\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-tests/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.StoveConfig\n"
  },
  {
    "path": "starters/spring/tests/spring-4x-tests/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>\n"
  },
  {
    "path": "starters/spring/tests/spring-test-fixtures/build.gradle.kts",
    "content": "import com.google.protobuf.gradle.id\n\nplugins {\n  `java-test-fixtures`\n  alias(libs.plugins.protobuf)\n}\n\ndependencies {\n  testFixturesApi(projects.starters.spring.stoveSpring)\n  testFixturesApi(projects.starters.spring.stoveSpringKafka)\n  testFixturesApi(libs.kotest.runner.junit5)\n  testFixturesApi(libs.google.protobuf.kotlin)\n  testFixturesApi(libs.kafka.streams.protobuf.serde)\n\n  // Spring Boot as compileOnly - version provided by consuming module\n  testFixturesCompileOnly(libs.spring.boot)\n  testFixturesCompileOnly(libs.spring.boot.autoconfigure)\n  testFixturesCompileOnly(libs.spring.boot.kafka)\n}\n\nprotobuf {\n  protoc {\n    artifact = libs.protoc.get().toString()\n  }\n\n  generateProtoTasks {\n    all().forEach {\n      it.descriptorSetOptions.includeSourceInfo = true\n      it.descriptorSetOptions.includeImports = true\n      it.builtins { id(\"kotlin\") }\n    }\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/BridgeSystemTests.kt",
    "content": "package com.trendyol.stove\n\nimport com.fasterxml.jackson.databind.ObjectMapper\nimport com.trendyol.stove.system.stove\nimport com.trendyol.stove.system.using\nimport io.kotest.core.spec.style.ShouldSpec\nimport io.kotest.matchers.shouldBe\nimport kotlinx.coroutines.delay\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * Shared bridge system tests that work across all Spring Boot versions.\n * Each version module should create their own test class that extends this.\n */\nabstract class BridgeSystemTests :\n  ShouldSpec({\n    should(\"bridge to application\") {\n      stove {\n        using<ExampleService> {\n          whatIsTheTime() shouldBe GetUtcNow.frozenTime\n        }\n\n        using<ParameterCollectorOfSpringBoot> {\n          parameters shouldBe listOf(\n            \"--test-system=true\",\n            \"--context=SetupOfBridgeSystemTests\"\n          )\n        }\n\n        delay(5.seconds)\n        using<TestAppInitializers> {\n          appReady shouldBe true\n          onEvent shouldBe true\n        }\n      }\n    }\n\n    should(\"resolve multiple\") {\n      stove {\n        using<GetUtcNow, TestAppInitializers> { getUtcNow: GetUtcNow, testAppInitializers: TestAppInitializers ->\n          getUtcNow() shouldBe GetUtcNow.frozenTime\n          testAppInitializers.appReady shouldBe true\n          testAppInitializers.onEvent shouldBe true\n        }\n\n        using<GetUtcNow, TestAppInitializers, ParameterCollectorOfSpringBoot> {\n            getUtcNow: GetUtcNow,\n            testAppInitializers: TestAppInitializers,\n            parameterCollectorOfSpringBoot: ParameterCollectorOfSpringBoot\n          ->\n          getUtcNow() shouldBe GetUtcNow.frozenTime\n          testAppInitializers.appReady shouldBe true\n          testAppInitializers.onEvent shouldBe true\n          parameterCollectorOfSpringBoot.parameters shouldBe listOf(\n            \"--test-system=true\",\n            \"--context=SetupOfBridgeSystemTests\"\n          )\n        }\n\n        using<GetUtcNow, TestAppInitializers, ParameterCollectorOfSpringBoot, ExampleService> {\n            getUtcNow: GetUtcNow,\n            testAppInitializers: TestAppInitializers,\n            parameterCollectorOfSpringBoot: ParameterCollectorOfSpringBoot,\n            exampleService: ExampleService\n          ->\n          getUtcNow() shouldBe GetUtcNow.frozenTime\n          testAppInitializers.appReady shouldBe true\n          testAppInitializers.onEvent shouldBe true\n          parameterCollectorOfSpringBoot.parameters shouldBe listOf(\n            \"--test-system=true\",\n            \"--context=SetupOfBridgeSystemTests\"\n          )\n          exampleService.whatIsTheTime() shouldBe GetUtcNow.frozenTime\n        }\n\n        using<GetUtcNow, TestAppInitializers, ParameterCollectorOfSpringBoot, ExampleService, ObjectMapper> {\n            getUtcNow: GetUtcNow,\n            testAppInitializers: TestAppInitializers,\n            parameterCollectorOfSpringBoot: ParameterCollectorOfSpringBoot,\n            exampleService: ExampleService,\n            objectMapper: ObjectMapper\n          ->\n          getUtcNow() shouldBe GetUtcNow.frozenTime\n          testAppInitializers.appReady shouldBe true\n          testAppInitializers.onEvent shouldBe true\n          parameterCollectorOfSpringBoot.parameters shouldBe listOf(\n            \"--test-system=true\",\n            \"--context=SetupOfBridgeSystemTests\"\n          )\n          exampleService.whatIsTheTime() shouldBe GetUtcNow.frozenTime\n          objectMapper.writeValueAsString(mapOf(\"a\" to \"b\")) shouldBe \"\"\"{\"a\":\"b\"}\"\"\"\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "starters/spring/tests/spring-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/TestDomain.kt",
    "content": "package com.trendyol.stove\n\nimport org.springframework.boot.ApplicationArguments\nimport org.springframework.boot.context.event.ApplicationReadyEvent\nimport org.springframework.context.event.EventListener\nimport org.springframework.stereotype.Component\nimport java.time.Instant\n\n/**\n * Common test domain classes shared across Spring Boot version tests.\n */\n\nfun interface GetUtcNow {\n  companion object {\n    val frozenTime: Instant = Instant.parse(\"2021-01-01T00:00:00Z\")\n  }\n\n  operator fun invoke(): Instant\n}\n\nclass SystemTimeGetUtcNow : GetUtcNow {\n  override fun invoke(): Instant = GetUtcNow.frozenTime\n}\n\nclass TestAppInitializers {\n  var onEvent: Boolean = false\n  var appReady: Boolean = false\n\n  @EventListener(ApplicationReadyEvent::class)\n  fun applicationReady() {\n    onEvent = true\n    appReady = true\n  }\n}\n\n@Component\nclass ExampleService(\n  private val getUtcNow: GetUtcNow\n) {\n  fun whatIsTheTime(): Instant = getUtcNow()\n}\n\nclass ParameterCollectorOfSpringBoot(\n  private val applicationArguments: ApplicationArguments\n) {\n  val parameters: List<String>\n    get() = applicationArguments.sourceArgs.toList()\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/kafka/KafkaTestDomain.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport io.kotest.assertions.*\nimport io.kotest.assertions.print.Printed\nimport io.kotest.common.reflection.bestName\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.engine.concurrency.SpecExecutionMode\n\n/**\n * Shared Kafka test configuration - runs tests sequentially.\n */\nabstract class KafkaTestSetup : AbstractProjectConfig() {\n  override val specExecutionMode: SpecExecutionMode = SpecExecutionMode.Sequential\n}\n\n/**\n * Test exception thrown to verify error handling in Kafka consumers.\n */\nclass StoveBusinessException(\n  message: String\n) : Exception(message)\n\n/**\n * Asserts that a block either completes successfully or throws the expected exception type.\n * Unlike shouldThrow, this doesn't fail if no exception is thrown.\n */\ninline fun <reified T : Throwable> shouldThrowMaybe(block: () -> Any) {\n  val expectedExceptionClass = T::class\n  val thrownThrowable = try {\n    block()\n    null\n  } catch (thrown: Throwable) {\n    thrown\n  }\n\n  when (thrownThrowable) {\n    null -> Unit\n\n    is T -> Unit\n\n    is AssertionError -> errorCollector.collectOrThrow(thrownThrowable)\n\n    else -> errorCollector.collectOrThrow(\n      createAssertionError(\n        \"Expected exception ${expectedExceptionClass.bestName()} but a ${thrownThrowable::class.simpleName} was thrown instead.\",\n        cause = thrownThrowable,\n        expected = Expected(Printed(expectedExceptionClass.bestName())),\n        actual = Actual(Printed(thrownThrowable::class.simpleName ?: \"null\"))\n      )\n    )\n  }\n}\n"
  },
  {
    "path": "starters/spring/tests/spring-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/kafka/ProtobufSerdeKafkaSystemTests.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.trendyol.stove.spring.testing.e2e.kafka.v1.*\nimport com.trendyol.stove.spring.testing.e2e.kafka.v1.Example.*\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.ShouldSpec\nimport kotlin.random.Random\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * Shared Kafka protobuf serde tests that work across all Spring Boot versions.\n * Each version module should create their own test class that extends this\n * and provides the TestSystem setup.\n */\nabstract class ProtobufSerdeKafkaSystemTests :\n  ShouldSpec({\n    should(\"publish and consume\") {\n      stove {\n        kafka {\n          val userId = Random.nextInt().toString()\n          val productId = Random.nextInt().toString()\n          val testProduct = product {\n            id = productId\n            name = \"product-${Random.nextInt()}\"\n            price = Random.nextDouble()\n            currency = \"eur\"\n            description = \"description-${Random.nextInt()}\"\n          }\n          val headers = mapOf(\"x-user-id\" to userId)\n          publish(\"topic-protobuf\", testProduct, headers = headers)\n          shouldBePublished<Product>(20.seconds) {\n            actual == testProduct && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic-protobuf\"\n          }\n          shouldBeConsumed<Product>(20.seconds) {\n            actual == testProduct && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic-protobuf\"\n          }\n\n          val orderId = Random.nextInt().toString()\n          val testOrder = order {\n            id = orderId\n            customerId = userId\n            products += testProduct\n          }\n          publish(\"topic-protobuf\", testOrder, headers = headers)\n          shouldBePublished<Order>(20.seconds) {\n            actual == testOrder && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic-protobuf\"\n          }\n\n          shouldBeConsumed<Order>(20.seconds) {\n            actual == testOrder && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic-protobuf\"\n          }\n        }\n      }\n    }\n  })\n"
  },
  {
    "path": "starters/spring/tests/spring-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/kafka/ProtobufTestUtils.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport com.google.protobuf.Message\nimport com.trendyol.stove.serialization.StoveSerde\nimport io.confluent.kafka.schemaregistry.protobuf.ProtobufSchemaProvider\nimport io.confluent.kafka.schemaregistry.testutil.MockSchemaRegistry\nimport io.confluent.kafka.serializers.AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG\nimport io.confluent.kafka.streams.serdes.protobuf.KafkaProtobufSerde\n\n/**\n * Schema registry abstraction for protobuf tests.\n */\nsealed class KafkaRegistry(\n  open val url: String\n) {\n  object Mock : KafkaRegistry(\"mock://mock-registry\")\n\n  data class Defined(\n    override val url: String\n  ) : KafkaRegistry(url)\n\n  companion object {\n    fun createSerde(registry: KafkaRegistry = Mock): KafkaProtobufSerde<Message> {\n      val schemaRegistryClient = when (registry) {\n        is Mock -> MockSchemaRegistry.getClientForScope(\"mock-registry\", listOf(ProtobufSchemaProvider()))\n        is Defined -> MockSchemaRegistry.getClientForScope(registry.url, listOf(ProtobufSchemaProvider()))\n      }\n      val serde: KafkaProtobufSerde<Message> = KafkaProtobufSerde<Message>(schemaRegistryClient)\n      val serdeConfig: MutableMap<String, Any?> = HashMap()\n      serdeConfig[SCHEMA_REGISTRY_URL_CONFIG] = registry.url\n      serde.configure(serdeConfig, false)\n      return serde\n    }\n  }\n}\n\n/**\n * Shared protobuf serde for Stove Kafka tests.\n */\n@Suppress(\"UNCHECKED_CAST\")\nclass StoveProtobufSerde : StoveSerde<Any, ByteArray> {\n  private val parseFromMethod = \"parseFrom\"\n  private val protobufSerde: KafkaProtobufSerde<Message> = KafkaRegistry.createSerde()\n\n  override fun serialize(value: Any): ByteArray = protobufSerde.serializer().serialize(\"any\", value as Message)\n\n  override fun <T : Any> deserialize(value: ByteArray, clazz: Class<T>): T {\n    val incoming: Message = protobufSerde.deserializer().deserialize(\"any\", value)\n    incoming.isAssignableFrom(clazz).also { isAssignableFrom ->\n      require(isAssignableFrom) {\n        \"Expected '${clazz.simpleName}' but got '${incoming.descriptorForType.name}'. \" +\n          \"This could be transient ser/de problem since the message stream is constantly checked if the expected message is arrived, \" +\n          \"so you can ignore this error if you are sure that the message is the expected one.\"\n      }\n    }\n\n    val parseFromMethod = clazz.getDeclaredMethod(parseFromMethod, ByteArray::class.java)\n    val parsed = parseFromMethod(incoming, incoming.toByteArray()) as T\n    return parsed\n  }\n}\n\nprivate fun Message.isAssignableFrom(clazz: Class<*>): Boolean = this.descriptorForType.name == clazz.simpleName\n"
  },
  {
    "path": "starters/spring/tests/spring-test-fixtures/src/testFixtures/kotlin/com/trendyol/stove/kafka/StringSerdeKafkaSystemTests.kt",
    "content": "package com.trendyol.stove.kafka\n\nimport arrow.core.some\nimport com.trendyol.stove.serialization.StoveSerde\nimport com.trendyol.stove.system.stove\nimport io.kotest.core.spec.style.ShouldSpec\nimport io.kotest.matchers.shouldBe\nimport org.apache.kafka.clients.admin.NewTopic\nimport kotlin.random.Random\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * Shared Kafka string serde tests that work across all Spring Boot versions.\n * Each version module should create their own test class that extends this.\n *\n * @param dltTopicSuffix Dead Letter Topic suffix - \".DLT\" for Spring Boot 2.x, \"-dlt\" for 3.x/4.x\n */\nabstract class StringSerdeKafkaSystemTests(\n  private val dltTopicSuffix: String = \".DLT\"\n) : ShouldSpec({\n  should(\"publish and consume\") {\n    stove {\n      kafka {\n        val userId = Random.nextInt().toString()\n        val message =\n          \"this message is coming from ${testCase.descriptor.id.value} and testName is ${testCase.name.name}\"\n        val headers = mapOf(\"x-user-id\" to userId)\n        publish(\"topic\", message, headers = headers)\n        shouldBePublished<Any>(20.seconds) {\n          actual == message && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic\"\n        }\n        shouldBeConsumed<Any>(20.seconds) {\n          actual == message && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic\"\n        }\n      }\n    }\n  }\n\n  should(\"publish and consume with failed consumer\") {\n    shouldThrowMaybe<StoveBusinessException> {\n      stove {\n        kafka {\n          val userId = Random.nextInt().toString()\n          val message =\n            \"this message is coming from ${testCase.descriptor.id.value} and testName is ${testCase.name.name}\"\n          val headers = mapOf(\"x-user-id\" to userId)\n          publish(\"topic-failed\", message, headers = headers)\n          shouldBePublished<Any>(20.seconds) {\n            actual == message && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic-failed\"\n          }\n          shouldBeFailed<Any>(20.seconds) {\n            actual == message && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic-failed\" &&\n              reason is StoveBusinessException\n          }\n\n          shouldBePublished<Any>(20.seconds) {\n            actual == message && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic-failed$dltTopicSuffix\"\n          }\n        }\n      }\n    }\n  }\n\n  should(\"admin operations\") {\n    stove {\n      kafka {\n        adminOperations {\n          val topic = \"admin-test-topic-${Random.nextInt()}\"\n          createTopics(listOf(NewTopic(topic, 1, 1)))\n          listTopics().names().get().contains(topic) shouldBe true\n          deleteTopics(listOf(topic))\n          listTopics().names().get().contains(topic) shouldBe false\n        }\n      }\n    }\n  }\n\n  should(\"publish with ser/de\") {\n    stove {\n      kafka {\n        val userId = Random.nextInt().toString()\n        val message = \"this message is coming from ${testCase.descriptor.id.value} and testName is ${testCase.name.name}\"\n        val headers = mapOf(\"x-user-id\" to userId)\n        publish(\"topic\", message, serde = StoveSerde.jackson.anyJsonStringSerde().some(), headers = headers)\n        shouldBePublished<String>(atLeastIn = 20.seconds) {\n          actual == message && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic\"\n        }\n        shouldBeConsumed<String>(atLeastIn = 20.seconds) {\n          actual == message && this.metadata.headers[\"x-user-id\"] == userId && this.metadata.topic == \"topic\"\n        }\n      }\n    }\n  }\n})\n"
  },
  {
    "path": "starters/spring/tests/spring-test-fixtures/src/testFixtures/proto/example.proto",
    "content": "syntax = \"proto3\";\n\n// buf:lint:ignore PACKAGE_DIRECTORY_MATCH\npackage com.trendyol.stove.spring.testing.e2e.kafka.v1;\n\nmessage Product {\n  string id = 1;\n  string name = 2;\n  string description = 3;\n  double price = 4;\n  string currency = 5;\n}\n\nmessage Order {\n  string id = 1;\n  string customerId = 2;\n  repeated Product products = 3;\n}\n"
  },
  {
    "path": "test-extensions/stove-extensions-junit/api/stove-extensions-junit.api",
    "content": "public final class com/trendyol/stove/extensions/junit/StoveJUnitExtension : org/junit/jupiter/api/extension/AfterEachCallback, org/junit/jupiter/api/extension/BeforeEachCallback, org/junit/jupiter/api/extension/TestExecutionExceptionHandler {\n\tpublic fun <init> ()V\n\tpublic fun afterEach (Lorg/junit/jupiter/api/extension/ExtensionContext;)V\n\tpublic fun beforeEach (Lorg/junit/jupiter/api/extension/ExtensionContext;)V\n\tpublic fun handleTestExecutionException (Lorg/junit/jupiter/api/extension/ExtensionContext;Ljava/lang/Throwable;)V\n}\n\n"
  },
  {
    "path": "test-extensions/stove-extensions-junit/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(projects.lib.stoveTracing)\n  api(libs.junit.jupiter.api)\n}\n\ndependencies {\n  testImplementation(projects.lib.stoveHttp)\n  testImplementation(projects.lib.stoveWiremock)\n  testImplementation(libs.kotest.runner.junit5)\n  testImplementation(libs.kotest.assertions.core)\n  testImplementation(libs.logback.classic)\n  testImplementation(testFixtures(projects.lib.stove))\n}\n\nkover {\n  currentProject {\n    sources {\n      excludedSourceSets.addAll(\"test\")\n    }\n  }\n  reports {\n    filters {\n      excludes {\n        classes(\n          \"*Test\",\n          \"*Tests\",\n          \"*Config\"\n        )\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "test-extensions/stove-extensions-junit/src/main/kotlin/com/trendyol/stove/extensions/junit/StoveJUnitExtension.kt",
    "content": "package com.trendyol.stove.extensions.junit\n\nimport com.trendyol.stove.reporting.StoveTestContext\nimport com.trendyol.stove.reporting.StoveTestContextHolder\nimport com.trendyol.stove.reporting.StoveTestFailureException\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.tracing.TraceContext\nimport com.trendyol.stove.tracing.TraceReportBuilder\nimport com.trendyol.stove.tracing.TraceReportBuilder.shouldEnrichFailures\nimport org.junit.jupiter.api.extension.AfterEachCallback\nimport org.junit.jupiter.api.extension.BeforeEachCallback\nimport org.junit.jupiter.api.extension.ExtensionContext\nimport org.junit.jupiter.api.extension.TestExecutionExceptionHandler\n\n/**\n * JUnit extension that automatically manages test context and enriches test failures\n * with Stove's execution report.\n *\n * When a test fails, the report is included in the exception message so that JUnit's\n * test engine displays what happened during the test execution.\n *\n * This extension works with both JUnit 5 and JUnit 6, as both use the JUnit Jupiter API.\n *\n * Register this extension on your test class:\n * ```kotlin\n * @ExtendWith(StoveJUnitExtension::class)\n * class MyE2ETests { ... }\n * ```\n *\n * Or globally via @RegisterExtension:\n * ```kotlin\n * companion object {\n *     @JvmField\n *     @RegisterExtension\n *     val stove = StoveJUnitExtension()\n * }\n * ```\n */\nclass StoveJUnitExtension :\n  BeforeEachCallback,\n  AfterEachCallback,\n  TestExecutionExceptionHandler {\n  override fun beforeEach(context: ExtensionContext) {\n    if (!Stove.instanceInitialized()) return\n\n    val ctx = context.toStoveContext()\n    StoveTestContextHolder.set(ctx)\n    Stove.reporter().startTest(ctx)\n    TraceContext.start(ctx.testId)\n  }\n\n  override fun handleTestExecutionException(context: ExtensionContext, throwable: Throwable) {\n    if (!Stove.instanceInitialized()) throw throwable\n\n    Stove.reporter().reportFailure(throwable.message ?: throwable::class.simpleName ?: \"Test failed\")\n\n    val options = Stove.options()\n    if (!options.shouldEnrichFailures()) throw throwable\n\n    val fullReport = TraceReportBuilder.buildFullReport()\n    if (fullReport.isNotEmpty()) {\n      throw StoveTestFailureException(\n        originalMessage = throwable.message ?: TraceReportBuilder.DEFAULT_ERROR_MESSAGE,\n        stoveReport = fullReport,\n        cause = throwable\n      )\n    }\n\n    throw throwable\n  }\n\n  override fun afterEach(context: ExtensionContext) {\n    if (!Stove.instanceInitialized()) return\n\n    TraceContext.clear()\n    Stove.reporter().run {\n      endTest()\n      clear()\n    }\n    StoveTestContextHolder.clear()\n  }\n\n  private fun ExtensionContext.toStoveContext(): StoveTestContext {\n    val path = buildTestPath()\n    val fullName = path.joinToString(\" / \")\n    val rootClass = findRootTestClass()\n    return StoveTestContext(\n      testId = TraceContext.sanitizeToAscii(\"$rootClass::$fullName\"),\n      testName = displayName,\n      specName = rootClass,\n      testPath = path\n    )\n  }\n\n  /**\n   * Builds a test path by traversing the [ExtensionContext] parent chain.\n   * For `@Nested` classes, each nested class display name becomes a path segment.\n   * For flat tests, the path contains just the test method display name.\n   */\n  private fun ExtensionContext.buildTestPath(): List<String> {\n    val segments = mutableListOf<String>()\n    var ctx: ExtensionContext? = this\n    while (ctx != null) {\n      when {\n        // Test method — always include\n        ctx.testMethod.isPresent -> segments.add(0, ctx.displayName)\n\n        // @Nested class — include if its parent also has a test class (meaning it's nested, not root)\n        ctx.testClass.isPresent && ctx.parent.flatMap { it.testClass }.isPresent ->\n          segments.add(0, ctx.displayName)\n      }\n      ctx = ctx.parent.orElse(null)\n    }\n    return segments\n  }\n\n  /**\n   * Finds the outermost (root) test class name by traversing the parent chain.\n   */\n  private fun ExtensionContext.findRootTestClass(): String {\n    var rootClass = requiredTestClass.simpleName\n    var ctx: ExtensionContext? = parent.orElse(null)\n    while (ctx != null) {\n      if (ctx.testClass.isPresent) {\n        rootClass = ctx.requiredTestClass.simpleName\n      }\n      ctx = ctx.parent.orElse(null)\n    }\n    return rootClass\n  }\n}\n"
  },
  {
    "path": "test-extensions/stove-extensions-junit/src/test/kotlin/com/trendyol/stove/extensions/junit/StoveJUnitExtensionTest.kt",
    "content": "package com.trendyol.stove.extensions.junit\n\nimport arrow.core.some\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.reporting.ReportEntry\nimport com.trendyol.stove.reporting.StoveTestContextHolder\nimport com.trendyol.stove.reporting.StoveTestFailureException\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.matchers.nulls.shouldNotBeNull\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport kotlinx.coroutines.runBlocking\nimport org.junit.jupiter.api.*\nimport org.junit.jupiter.api.extension.ExtendWith\nimport org.junit.jupiter.api.extension.ExtensionContext\n\nprivate val WIREMOCK_PORT = PortFinder.findAvailablePort()\n\nclass NoApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) {\n    // do nothing\n  }\n\n  override suspend fun stop() {\n    // do nothing\n  }\n}\n\ndata class TestDto(\n  val name: String\n)\n\n@ExtendWith(StoveJUnitExtension::class)\nclass StoveJUnitExtensionTest {\n  companion object {\n    @JvmStatic\n    @BeforeAll\n    fun setup() = runBlocking {\n      Stove()\n        .with {\n          httpClient {\n            HttpClientSystemOptions(\n              baseUrl = \"http://localhost:$WIREMOCK_PORT\"\n            )\n          }\n\n          wiremock {\n            WireMockSystemOptions(WIREMOCK_PORT)\n          }\n\n          applicationUnderTest(NoApplication())\n        }.run()\n    }\n\n    @JvmStatic\n    @AfterAll\n    fun teardown() = runBlocking {\n      Stove.stop()\n    }\n  }\n\n  @Test\n  fun `beforeEach should set up test context correctly`() {\n    val context = StoveTestContextHolder.get()\n    context.shouldNotBeNull()\n    context.testId shouldContain \"StoveJUnitExtensionTest\"\n    context.testName shouldContain \"beforeEach should set up test context correctly\"\n    context.specName shouldBe \"StoveJUnitExtensionTest\"\n  }\n\n  @Test\n  fun `afterEach should clear test context`() {\n    val context = StoveTestContextHolder.get()\n    context.shouldNotBeNull()\n    // Context should be cleared after test execution\n  }\n\n  @Test\n  suspend fun `extension should integrate with WireMock and HTTP systems`() {\n    val expectedName = \"test-integration\"\n    stove {\n      wiremock {\n        mockGet(\"/test\", statusCode = 200, responseBody = TestDto(expectedName).some())\n      }\n\n      http {\n        get<TestDto>(\"/test\") { actual ->\n          actual.name shouldBe expectedName\n        }\n      }\n    }\n  }\n\n  @Test\n  suspend fun `handleTestExecutionException should enrich failures with Stove report`() {\n    val exception = shouldThrow<AssertionError> {\n      stove {\n        wiremock {\n          mockGet(\"/failing-endpoint\", statusCode = 200, responseBody = TestDto(\"expected\").some())\n        }\n\n        http {\n          get<TestDto>(\"/failing-endpoint\") { actual ->\n            actual.name shouldBe \"wrong-value\" // This will fail\n          }\n        }\n      }\n    }\n\n    // The exception should be enriched with Stove report\n    exception.message.shouldNotBeNull()\n    exception.message shouldContain \"wrong-value\"\n  }\n\n  @Test\n  suspend fun `extension should handle multiple sequential HTTP calls`() {\n    stove {\n      wiremock {\n        mockGet(\"/first\", statusCode = 200, responseBody = TestDto(\"first\").some())\n        mockGet(\"/second\", statusCode = 200, responseBody = TestDto(\"second\").some())\n      }\n\n      http {\n        get<TestDto>(\"/first\") { actual ->\n          actual.name shouldBe \"first\"\n        }\n\n        get<TestDto>(\"/second\") { actual ->\n          actual.name shouldBe \"second\"\n        }\n      }\n    }\n  }\n\n  @Test\n  suspend fun `extension should work with reporter to track test execution`() {\n    val reporter = Stove.reporter()\n    val testId = reporter.currentTestId()\n\n    testId.shouldNotBeNull()\n    testId shouldContain \"StoveJUnitExtensionTest\"\n    testId shouldContain \"extension should work with reporter to track test execution\"\n\n    stove {\n      wiremock {\n        mockGet(\"/reporter-test\", statusCode = 200, responseBody = TestDto(\"reporter\").some())\n      }\n\n      http {\n        get<TestDto>(\"/reporter-test\") { actual ->\n          actual.name shouldBe \"reporter\"\n        }\n      }\n    }\n  }\n\n  @Test\n  suspend fun `extension should handle test context isolation between tests`() {\n    // Each test should have its own isolated context\n    val context = StoveTestContextHolder.get()\n    context.shouldNotBeNull()\n    context.testId shouldContain \"extension should handle test context isolation between tests\"\n\n    stove {\n      wiremock {\n        mockGet(\"/isolation-test\", statusCode = 200, responseBody = TestDto(\"isolated\").some())\n      }\n\n      http {\n        get<TestDto>(\"/isolation-test\") { actual ->\n          actual.name shouldBe \"isolated\"\n        }\n      }\n    }\n  }\n\n  @Test\n  fun `handleTestExecutionException should enrich failures when reporter has recorded failures`() {\n    // Record a failure in the reporter so buildFullReport() returns non-empty\n    val reporter = Stove.reporter()\n    reporter.record(\n      ReportEntry.failure(\n        system = \"TestSystem\",\n        testId = reporter.currentTestId(),\n        action = \"simulated action\",\n        error = \"simulated failure for enrichment test\"\n      )\n    )\n\n    val extension = StoveJUnitExtension()\n    val originalError = AssertionError(\"Original test assertion failure\")\n\n    // Create a stub ExtensionContext via Proxy since handleTestExecutionException\n    // doesn't use the context parameter\n    val stubContext = java.lang.reflect.Proxy.newProxyInstance(\n      ExtensionContext::class.java.classLoader,\n      arrayOf(ExtensionContext::class.java)\n    ) { _, _, _ -> null } as ExtensionContext\n\n    // handleTestExecutionException always throws - verify it wraps with StoveTestFailureException\n    val thrown = shouldThrow<StoveTestFailureException> {\n      extension.handleTestExecutionException(stubContext, originalError)\n    }\n\n    thrown.message shouldContain \"Original test assertion failure\"\n  }\n\n  @Test\n  fun `handleTestExecutionException should rethrow original when report is empty`() {\n    // Don't record any failures, so buildFullReport() returns empty\n    // Clear the current test report to ensure no prior failures\n    Stove.reporter().clear()\n\n    val extension = StoveJUnitExtension()\n    val originalError = IllegalStateException(\"Original error without enrichment\")\n\n    val stubContext = java.lang.reflect.Proxy.newProxyInstance(\n      ExtensionContext::class.java.classLoader,\n      arrayOf(ExtensionContext::class.java)\n    ) { _, _, _ -> null } as ExtensionContext\n\n    // When report is empty, it should rethrow the original exception\n    val thrown = shouldThrow<IllegalStateException> {\n      extension.handleTestExecutionException(stubContext, originalError)\n    }\n\n    thrown.message shouldBe \"Original error without enrichment\"\n  }\n}\n"
  },
  {
    "path": "test-extensions/stove-extensions-junit/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>"
  },
  {
    "path": "test-extensions/stove-extensions-kotest/api/stove-extensions-kotest.api",
    "content": "public final class com/trendyol/stove/extensions/kotest/StoveKotestExtension : io/kotest/core/extensions/TestCaseExtension {\n\tpublic fun <init> ()V\n\tpublic fun intercept (Lio/kotest/core/test/TestCase;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;\n}\n\n"
  },
  {
    "path": "test-extensions/stove-extensions-kotest/build.gradle.kts",
    "content": "dependencies {\n  api(projects.lib.stove)\n  api(projects.lib.stoveTracing)\n  api(libs.kotest.framework.engine)\n}\n\ndependencies {\n  testImplementation(projects.lib.stoveHttp)\n  testImplementation(projects.lib.stoveWiremock)\n  testImplementation(libs.kotest.runner.junit5)\n  testImplementation(libs.kotest.assertions.core)\n  testImplementation(libs.logback.classic)\n  testImplementation(testFixtures(projects.lib.stove))\n}\n\nkover {\n  currentProject {\n    sources {\n      excludedSourceSets.addAll(\"test\")\n    }\n  }\n  reports {\n    filters {\n      excludes {\n        classes(\n          \"*Test\",\n          \"*Tests\",\n          \"*Config\"\n        )\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "test-extensions/stove-extensions-kotest/src/main/kotlin/com/trendyol/stove/extensions/kotest/StoveKotestExtension.kt",
    "content": "package com.trendyol.stove.extensions.kotest\n\nimport com.trendyol.stove.reporting.StoveReporter\nimport com.trendyol.stove.reporting.StoveTestContext\nimport com.trendyol.stove.reporting.StoveTestErrorException\nimport com.trendyol.stove.reporting.StoveTestFailureException\nimport com.trendyol.stove.system.Stove\nimport com.trendyol.stove.tracing.TraceContext\nimport com.trendyol.stove.tracing.TraceReportBuilder\nimport com.trendyol.stove.tracing.TraceReportBuilder.shouldEnrichFailures\nimport io.kotest.core.extensions.TestCaseExtension\nimport io.kotest.core.test.TestCase\nimport io.kotest.core.test.TestType\nimport io.kotest.engine.test.TestResult\n\n/**\n * Kotest extension that automatically manages test context and enriches test failures\n * with Stove's execution report.\n *\n * When a test fails, the report is included in the exception message so that Kotest's\n * test engine displays what happened during the test execution.\n *\n * Register this extension in your Kotest project config:\n * ```kotlin\n * class TestConfig : AbstractProjectConfig() {\n *     override fun extensions() = listOf(StoveKotestExtension())\n * }\n * ```\n */\nclass StoveKotestExtension : TestCaseExtension {\n  override suspend fun intercept(\n    testCase: TestCase,\n    execute: suspend (TestCase) -> TestResult\n  ): TestResult {\n    if (!Stove.instanceInitialized()) {\n      return execute(testCase)\n    }\n\n    // Only wrap leaf tests in test context — containers (context/given/when/describe blocks)\n    // should pass through without starting/ending a test report.\n    if (testCase.type != TestType.Test) {\n      return execute(testCase)\n    }\n\n    return Stove.reporter().withTestContext(testCase.toStoveContext()) {\n      execute(testCase)\n        .reportFailureIfNeeded()\n        .enrichIfFailed()\n    }\n  }\n\n  private fun TestCase.toStoveContext(): StoveTestContext {\n    val path = buildDisplayPath()\n    val fullName = path.joinToString(\" / \")\n    return StoveTestContext(\n      testId = TraceContext.sanitizeToAscii(\"${spec::class.simpleName}::$fullName\"),\n      testName = name.name,\n      specName = spec::class.simpleName,\n      testPath = path\n    )\n  }\n\n  /**\n   * Builds a display path by traversing the parent chain and prepending\n   * each test case's prefix (if any) to its name.\n   *\n   * For BehaviourSpec: [\"Given: valid request\", \"When: creating\", \"Then: should succeed\"]\n   * For FunSpec with context: [\"context order creation\", \"should create order\"]\n   * For flat FunSpec: [\"should create order\"]\n   */\n  private fun TestCase.buildDisplayPath(): List<String> {\n    val chain = mutableListOf<TestCase>()\n    var current: TestCase? = this\n    while (current != null) {\n      chain.add(0, current)\n      current = current.parent\n    }\n    return chain.map { tc ->\n      val prefix = tc.name.prefix ?: \"\"\n      \"$prefix${tc.name.name}\"\n    }\n  }\n\n  private fun TestResult.enrichIfFailed(): TestResult {\n    if (!Stove.options().shouldEnrichFailures()) return this\n\n    return when (this) {\n      is TestResult.Failure -> enrichFailure()\n      is TestResult.Error -> enrichError()\n      else -> this\n    }\n  }\n\n  private fun TestResult.reportFailureIfNeeded(): TestResult {\n    when (this) {\n      is TestResult.Failure -> Stove.reporter().reportFailure(cause.toFailureMessage())\n      is TestResult.Error -> Stove.reporter().reportFailure(cause.toFailureMessage())\n      else -> Unit\n    }\n    return this\n  }\n\n  private fun TestResult.Failure.enrichFailure(): TestResult {\n    val fullReport = TraceReportBuilder.buildFullReport()\n    return if (fullReport.isNotEmpty()) {\n      TestResult.Failure(\n        duration,\n        StoveTestFailureException(cause.toFailureMessage(), fullReport, cause)\n      )\n    } else {\n      this\n    }\n  }\n\n  private fun TestResult.Error.enrichError(): TestResult {\n    val fullReport = TraceReportBuilder.buildFullReport()\n    return if (fullReport.isNotEmpty()) {\n      TestResult.Error(\n        duration,\n        StoveTestErrorException(cause.toFailureMessage(), fullReport, cause)\n      )\n    } else {\n      this\n    }\n  }\n\n  private fun Throwable.toFailureMessage(): String =\n    message ?: this::class.simpleName ?: TraceReportBuilder.DEFAULT_ERROR_MESSAGE\n}\n\n/**\n * Executes the block within a test context, ensuring proper setup and cleanup.\n * Also starts/ends tracing automatically for the test.\n */\nprivate suspend fun <T> StoveReporter.withTestContext(\n  ctx: StoveTestContext,\n  block: suspend () -> T\n): T {\n  startTest(ctx)\n  TraceContext.start(ctx.testId)\n  return try {\n    TraceContext.withCurrentPropagation {\n      block()\n    }\n  } finally {\n    TraceContext.clear()\n    endTest()\n    clear(ctx.testId)\n  }\n}\n"
  },
  {
    "path": "test-extensions/stove-extensions-kotest/src/test/kotlin/com/trendyol/stove/extensions/kotest/KotestHierarchyExplorationTest.kt",
    "content": "package com.trendyol.stove.extensions.kotest\n\nimport io.kotest.core.extensions.TestCaseExtension\nimport io.kotest.core.spec.style.BehaviorSpec\nimport io.kotest.core.spec.style.DescribeSpec\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.core.spec.style.StringSpec\nimport io.kotest.core.test.TestCase\nimport io.kotest.core.test.TestType\nimport io.kotest.engine.test.TestResult\nimport io.kotest.matchers.collections.shouldContainExactly\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.shouldBe\nimport java.util.concurrent.CopyOnWriteArrayList\n\n/**\n * Data captured by the [HierarchyCaptureExtension] for each intercepted test case.\n */\ndata class CapturedTestInfo(\n  val specSimpleName: String?,\n  val testParts: List<String>,\n  val displayPath: List<String>,\n  val leafName: String,\n  val prefix: String?,\n  val type: TestType,\n  val hasParent: Boolean,\n  val parentType: TestType?\n)\n\n/**\n * A [TestCaseExtension] that captures hierarchy metadata from every intercepted test case\n * into a shared list for later assertion.\n */\nclass HierarchyCaptureExtension(\n  private val captured: MutableList<CapturedTestInfo>\n) : TestCaseExtension {\n  override suspend fun intercept(\n    testCase: TestCase,\n    execute: suspend (TestCase) -> TestResult\n  ): TestResult {\n    // Build display path by traversing parents and prepending prefix to each name\n    val displayPath = buildDisplayPath(testCase)\n    captured.add(\n      CapturedTestInfo(\n        specSimpleName = testCase.spec::class.simpleName,\n        testParts = testCase.descriptor.testParts(),\n        displayPath = displayPath,\n        leafName = testCase.name.name,\n        prefix = testCase.name.prefix,\n        type = testCase.type,\n        hasParent = testCase.parent != null,\n        parentType = testCase.parent?.type\n      )\n    )\n    return execute(testCase)\n  }\n\n  private fun buildDisplayPath(testCase: TestCase): List<String> {\n    val chain = mutableListOf<TestCase>()\n    var current: TestCase? = testCase\n    while (current != null) {\n      chain.add(0, current)\n      current = current.parent\n    }\n    return chain.map { tc ->\n      val prefix = tc.name.prefix ?: \"\"\n      \"$prefix${tc.name.name}\"\n    }\n  }\n}\n\n// -- FunSpec flat tests --\n\nprivate val funSpecFlatCaptures = CopyOnWriteArrayList<CapturedTestInfo>()\n\nclass FunSpecFlatHierarchyTest : FunSpec({\n  extensions(HierarchyCaptureExtension(funSpecFlatCaptures))\n\n  test(\"flat test one\") {\n    val mine = funSpecFlatCaptures.first { it.leafName == \"flat test one\" }\n    mine.testParts shouldContainExactly listOf(\"flat test one\")\n    mine.displayPath shouldContainExactly listOf(\"flat test one\")\n    mine.type shouldBe TestType.Test\n    mine.hasParent shouldBe false\n    mine.parentType shouldBe null\n    mine.prefix shouldBe null\n    mine.specSimpleName shouldBe \"FunSpecFlatHierarchyTest\"\n  }\n})\n\n// -- FunSpec with context blocks --\n\nprivate val funSpecContextCaptures = CopyOnWriteArrayList<CapturedTestInfo>()\n\nclass FunSpecContextHierarchyTest : FunSpec({\n  extensions(HierarchyCaptureExtension(funSpecContextCaptures))\n\n  context(\"order creation\") {\n    test(\"should create order\") {\n      val mine = funSpecContextCaptures.first { it.leafName == \"should create order\" }\n      // testParts() uses raw DescriptorId values (no prefixes)\n      mine.testParts shouldContainExactly listOf(\"order creation\", \"should create order\")\n      // displayPath includes prefix+name for each level (FunSpec context has \"context \" prefix)\n      mine.displayPath shouldContainExactly listOf(\"context order creation\", \"should create order\")\n      mine.type shouldBe TestType.Test\n      mine.hasParent shouldBe true\n      mine.parentType shouldBe TestType.Container\n    }\n\n    test(\"should validate order\") {\n      val mine = funSpecContextCaptures.first { it.leafName == \"should validate order\" }\n      mine.testParts shouldContainExactly listOf(\"order creation\", \"should validate order\")\n    }\n  }\n\n  test(\"top level test\") {\n    val mine = funSpecContextCaptures.first { it.leafName == \"top level test\" }\n    mine.testParts shouldContainExactly listOf(\"top level test\")\n    mine.hasParent shouldBe false\n  }\n\n  // This test runs last and verifies container interception\n  test(\"intercept is called for container test cases\") {\n    val containers = funSpecContextCaptures.filter { it.type == TestType.Container }\n    containers shouldHaveSize 1\n    containers.first().leafName shouldBe \"order creation\"\n    containers.first().testParts shouldContainExactly listOf(\"order creation\")\n  }\n})\n\n// -- BehaviourSpec --\n\nprivate val behaviourSpecCaptures = CopyOnWriteArrayList<CapturedTestInfo>()\n\nclass BehaviourSpecHierarchyTest : BehaviorSpec({\n  extensions(HierarchyCaptureExtension(behaviourSpecCaptures))\n\n  given(\"a valid order request\") {\n    `when`(\"creating an order\") {\n      then(\"should succeed\") {\n        val mine = behaviourSpecCaptures.first { it.leafName == \"should succeed\" }\n        // testParts() returns raw names without prefixes\n        mine.testParts shouldContainExactly listOf(\n          \"a valid order request\",\n          \"creating an order\",\n          \"should succeed\"\n        )\n        // displayPath includes the style-specific prefixes\n        mine.displayPath shouldContainExactly listOf(\n          \"Given: a valid order request\",\n          \"When: creating an order\",\n          \"Then: should succeed\"\n        )\n        mine.type shouldBe TestType.Test\n        mine.hasParent shouldBe true\n        mine.parentType shouldBe TestType.Container\n        mine.prefix shouldBe \"Then: \"\n      }\n\n      then(\"should publish event\") {\n        val mine = behaviourSpecCaptures.first { it.leafName == \"should publish event\" }\n        mine.displayPath shouldContainExactly listOf(\n          \"Given: a valid order request\",\n          \"When: creating an order\",\n          \"Then: should publish event\"\n        )\n      }\n    }\n  }\n\n  // Verify containers were intercepted and have correct prefixes\n  given(\"container interception check\") {\n    then(\"given and when containers are intercepted\") {\n      val containers = behaviourSpecCaptures.filter { it.type == TestType.Container }\n      // given(\"a valid order request\") + when(\"creating an order\") + given(\"container interception check\")\n      containers.size shouldBe 3\n      val givenContainer = containers.first { it.leafName == \"a valid order request\" }\n      givenContainer.prefix shouldBe \"Given: \"\n      givenContainer.displayPath shouldContainExactly listOf(\"Given: a valid order request\")\n      val whenContainer = containers.first { it.leafName == \"creating an order\" }\n      whenContainer.prefix shouldBe \"When: \"\n    }\n  }\n})\n\n// -- StringSpec --\n\nprivate val stringSpecCaptures = CopyOnWriteArrayList<CapturedTestInfo>()\n\nclass StringSpecHierarchyTest : StringSpec({\n  extensions(HierarchyCaptureExtension(stringSpecCaptures))\n\n  \"should work as a flat test\" {\n    val mine = stringSpecCaptures.first { it.leafName == \"should work as a flat test\" }\n    mine.testParts shouldContainExactly listOf(\"should work as a flat test\")\n    mine.displayPath shouldContainExactly listOf(\"should work as a flat test\")\n    mine.type shouldBe TestType.Test\n    mine.hasParent shouldBe false\n    mine.parentType shouldBe null\n    mine.prefix shouldBe null\n    mine.specSimpleName shouldBe \"StringSpecHierarchyTest\"\n  }\n\n  \"another flat test\" {\n    val mine = stringSpecCaptures.first { it.leafName == \"another flat test\" }\n    mine.testParts shouldContainExactly listOf(\"another flat test\")\n    mine.displayPath shouldContainExactly listOf(\"another flat test\")\n    mine.hasParent shouldBe false\n  }\n})\n\n// -- DescribeSpec --\n\nprivate val describeSpecCaptures = CopyOnWriteArrayList<CapturedTestInfo>()\n\nclass DescribeSpecHierarchyTest : DescribeSpec({\n  extensions(HierarchyCaptureExtension(describeSpecCaptures))\n\n  describe(\"OrderService\") {\n    it(\"should create order\") {\n      val mine = describeSpecCaptures.first { it.leafName == \"should create order\" }\n      // testParts() returns raw names without prefixes\n      mine.testParts shouldContainExactly listOf(\"OrderService\", \"should create order\")\n      // displayPath includes \"Describe: \" prefix for describe blocks\n      mine.displayPath shouldContainExactly listOf(\"Describe: OrderService\", \"should create order\")\n      mine.type shouldBe TestType.Test\n      mine.hasParent shouldBe true\n      mine.parentType shouldBe TestType.Container\n    }\n\n    describe(\"with invalid input\") {\n      it(\"should fail validation\") {\n        val mine = describeSpecCaptures.first { it.leafName == \"should fail validation\" }\n        mine.testParts shouldContainExactly listOf(\n          \"OrderService\",\n          \"with invalid input\",\n          \"should fail validation\"\n        )\n        mine.displayPath shouldContainExactly listOf(\n          \"Describe: OrderService\",\n          \"Describe: with invalid input\",\n          \"should fail validation\"\n        )\n      }\n    }\n  }\n})\n"
  },
  {
    "path": "test-extensions/stove-extensions-kotest/src/test/kotlin/com/trendyol/stove/extensions/kotest/StoveKotestExtensionTest.kt",
    "content": "package com.trendyol.stove.extensions.kotest\n\nimport arrow.core.some\nimport com.trendyol.stove.http.*\nimport com.trendyol.stove.reporting.ReportEntry\nimport com.trendyol.stove.reporting.ReportEventListener\nimport com.trendyol.stove.reporting.StoveTestErrorException\nimport com.trendyol.stove.reporting.StoveTestFailureException\nimport com.trendyol.stove.system.*\nimport com.trendyol.stove.system.abstractions.ApplicationUnderTest\nimport com.trendyol.stove.wiremock.*\nimport io.kotest.assertions.throwables.shouldThrow\nimport io.kotest.core.config.AbstractProjectConfig\nimport io.kotest.core.extensions.Extension\nimport io.kotest.core.spec.style.FunSpec\nimport io.kotest.engine.test.TestResult\nimport io.kotest.matchers.collections.shouldHaveSize\nimport io.kotest.matchers.nulls.shouldNotBeNull\nimport io.kotest.matchers.shouldBe\nimport io.kotest.matchers.string.shouldContain\nimport io.kotest.matchers.types.shouldBeInstanceOf\nimport kotlin.time.Duration.Companion.milliseconds\n\nprivate val WIREMOCK_PORT = PortFinder.findAvailablePort()\n\nclass NoApplication : ApplicationUnderTest<Unit> {\n  override suspend fun start(configurations: List<String>) {\n    // do nothing\n  }\n\n  override suspend fun stop() {\n    // do nothing\n  }\n}\n\nclass StoveConfig : AbstractProjectConfig() {\n  override val extensions: List<Extension> = listOf(StoveKotestExtension())\n\n  override suspend fun beforeProject(): Unit =\n    Stove()\n      .with {\n        httpClient {\n          HttpClientSystemOptions(\n            baseUrl = \"http://localhost:$WIREMOCK_PORT\"\n          )\n        }\n\n        wiremock {\n          WireMockSystemOptions(WIREMOCK_PORT)\n        }\n\n        applicationUnderTest(NoApplication())\n      }.run()\n\n  override suspend fun afterProject(): Unit = Stove.stop()\n}\n\ndata class TestDto(\n  val name: String\n)\n\nclass StoveKotestExtensionTest :\n  FunSpec({\n    test(\"extension should set test context during test execution\") {\n      // The extension sets context via reporter, verify it's accessible\n      val reporter = Stove.reporter()\n      val testId = reporter.currentTestId()\n      testId.shouldNotBeNull()\n      testId shouldContain \"StoveKotestExtensionTest\"\n      testId shouldContain \"extension should set test context during test execution\"\n    }\n\n    test(\"extension should integrate with WireMock and HTTP systems\") {\n      val expectedName = \"test-integration\"\n      stove {\n        wiremock {\n          mockGet(\"/test\", statusCode = 200, responseBody = TestDto(expectedName).some())\n        }\n\n        http {\n          get<TestDto>(\"/test\") { actual ->\n            actual.name shouldBe expectedName\n          }\n        }\n      }\n    }\n\n    test(\"extension should enrich test failures with Stove report\") {\n      // This test verifies that failures are enriched with Stove reports\n      // The enrichment is visible in the test output when the test fails\n      shouldThrow<AssertionError> {\n        stove {\n          wiremock {\n            mockGet(\"/failing-endpoint\", statusCode = 200, responseBody = TestDto(\"expected\").some())\n          }\n\n          http {\n            get<TestDto>(\"/failing-endpoint\") { actual ->\n              actual.name shouldBe \"wrong-value\" // This will fail\n            }\n          }\n        }\n      }\n      // If we reach here, an exception was thrown (enrichment verified by test output)\n    }\n\n    test(\"extension should enrich test errors with Stove report\") {\n      // This test verifies that errors are enriched with Stove reports\n      // The enrichment is visible in the test output when the test fails\n      // Note: HTTP errors may throw IllegalStateException when deserialization fails\n      shouldThrow<Throwable> {\n        stove {\n          wiremock {\n            mockGet(\"/error-endpoint\", statusCode = 500)\n          }\n\n          http {\n            get<TestDto>(\"/error-endpoint\") { _ ->\n              // This will throw an error due to 500 status\n            }\n          }\n        }\n      }\n      // If we reach here, an exception was thrown (enrichment verified by test output)\n    }\n\n    test(\"extension should clear test context after test execution\") {\n      // Context should be available during test via reporter\n      val reporter = Stove.reporter()\n      val testId = reporter.currentTestId()\n      testId.shouldNotBeNull()\n      // After this test, context should be cleared by the extension\n    }\n\n    test(\"extension should handle multiple sequential HTTP calls\") {\n      stove {\n        wiremock {\n          mockGet(\"/first\", statusCode = 200, responseBody = TestDto(\"first\").some())\n          mockGet(\"/second\", statusCode = 200, responseBody = TestDto(\"second\").some())\n        }\n\n        http {\n          get<TestDto>(\"/first\") { actual ->\n            actual.name shouldBe \"first\"\n          }\n\n          get<TestDto>(\"/second\") { actual ->\n            actual.name shouldBe \"second\"\n          }\n        }\n      }\n    }\n\n    test(\"extension should work with reporter to track test execution\") {\n      val reporter = Stove.reporter()\n      val testId = reporter.currentTestId()\n\n      testId.shouldNotBeNull()\n      testId shouldContain \"StoveKotestExtensionTest\"\n      testId shouldContain \"extension should work with reporter to track test execution\"\n\n      stove {\n        wiremock {\n          mockGet(\"/reporter-test\", statusCode = 200, responseBody = TestDto(\"reporter\").some())\n        }\n\n        http {\n          get<TestDto>(\"/reporter-test\") { actual ->\n            actual.name shouldBe \"reporter\"\n          }\n        }\n      }\n    }\n\n    test(\"enrichFailure should wrap test failures with Stove report\") {\n      val extension = StoveKotestExtension()\n\n      val result = extension.intercept(testCase) { tc ->\n        // Record a failure in the reporter so buildFullReport() returns non-empty\n        val reporter = Stove.reporter()\n        reporter.record(\n          ReportEntry.failure(\n            system = \"TestSystem\",\n            testId = reporter.currentTestId(),\n            action = \"simulated action\",\n            error = \"simulated failure\"\n          )\n        )\n        TestResult.Failure(1.milliseconds, AssertionError(\"Original assertion failure\"))\n      }\n\n      result.shouldBeInstanceOf<TestResult.Failure>()\n      result.errorOrNull.shouldNotBeNull()\n      result.errorOrNull.shouldBeInstanceOf<StoveTestFailureException>()\n      result.errorOrNull!!.message shouldContain \"Original assertion failure\"\n    }\n\n    test(\"enrichError should wrap test errors with Stove report\") {\n      val extension = StoveKotestExtension()\n\n      val result = extension.intercept(testCase) { tc ->\n        // Record a failure in the reporter so buildFullReport() returns non-empty\n        val reporter = Stove.reporter()\n        reporter.record(\n          ReportEntry.failure(\n            system = \"TestSystem\",\n            testId = reporter.currentTestId(),\n            action = \"simulated action\",\n            error = \"simulated error\"\n          )\n        )\n        TestResult.Error(1.milliseconds, RuntimeException(\"Original runtime error\"))\n      }\n\n      result.shouldBeInstanceOf<TestResult.Error>()\n      result.errorOrNull.shouldNotBeNull()\n      result.errorOrNull.shouldBeInstanceOf<StoveTestErrorException>()\n      result.errorOrNull!!.message shouldContain \"Original runtime error\"\n    }\n\n    test(\"enrichIfFailed should not enrich successful tests\") {\n      val extension = StoveKotestExtension()\n\n      val result = extension.intercept(testCase) {\n        TestResult.Success(1.milliseconds)\n      }\n\n      result.shouldBeInstanceOf<TestResult.Success>()\n    }\n\n    test(\"intercept should notify reporter listeners for failed test results\") {\n      val extension = StoveKotestExtension()\n      val reporter = Stove.reporter()\n      val failures = mutableListOf<Pair<String, String>>()\n      val listener = object : ReportEventListener {\n        override fun onTestFailed(testId: String, error: String) {\n          failures += testId to error\n        }\n      }\n\n      reporter.addListener(listener)\n      try {\n        extension.intercept(testCase) {\n          TestResult.Failure(1.milliseconds, AssertionError(\"Original assertion failure\"))\n        }.shouldBeInstanceOf<TestResult.Failure>()\n      } finally {\n        reporter.removeListener(listener)\n      }\n\n      failures shouldHaveSize 1\n      failures.single().first shouldContain \"StoveKotestExtensionTest\"\n      failures.single().second shouldBe \"Original assertion failure\"\n    }\n\n    test(\"intercept should pass through when Stove is not initialized\") {\n      // This test verifies the early return path when Stove IS initialized\n      // (since we can't easily uninitialize Stove in this test context,\n      // we verify the normal path works correctly)\n      val extension = StoveKotestExtension()\n\n      val result = extension.intercept(testCase) {\n        TestResult.Success(2.milliseconds)\n      }\n\n      result.shouldBeInstanceOf<TestResult.Success>()\n    }\n\n    test(\"withTestContext should clear report state between repeated executions\") {\n      val extension = StoveKotestExtension()\n      val reporter = Stove.reporter()\n\n      reporter.clear()\n\n      extension.intercept(testCase) {\n        reporter.record(\n          ReportEntry.success(\n            system = \"TestSystem\",\n            testId = reporter.currentTestId(),\n            action = \"first execution\"\n          )\n        )\n        TestResult.Success(1.milliseconds)\n      }\n\n      extension.intercept(testCase) {\n        reporter.currentTest().entries() shouldHaveSize 0\n        TestResult.Success(1.milliseconds)\n      }.shouldBeInstanceOf<TestResult.Success>()\n    }\n  })\n"
  },
  {
    "path": "test-extensions/stove-extensions-kotest/src/test/resources/kotest.properties",
    "content": "kotest.framework.config.fqn=com.trendyol.stove.extensions.kotest.StoveConfig"
  },
  {
    "path": "test-extensions/stove-extensions-kotest/src/test/resources/logback-test.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>\n                %white([%t]) %highlight(%-5level) %magenta(%c{1}) %cyan(trace.id:%X{traceId} version:%X{version}) -\n                %yellow(%m) %n\n            </pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"INFO\">\n        <appender-ref ref=\"STDOUT\"/>\n    </root>\n    <logger name=\"org.testcontainers\" level=\"WARN\"/>\n    <logger name=\"tc\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava\" level=\"OFF\"/>\n    <logger name=\"com.github.dockerjava.zerodep.shaded.org.apache.hc.client5.http.wire\" level=\"OFF\"/>\n    <logger name=\"org.apache\" level=\"OFF\"/>\n</configuration>"
  },
  {
    "path": "tools/stove-cli/.gitignore",
    "content": "# Rust\n/target/\n\n# SPA\n/spa/node_modules/\n/spa/dist/\n"
  },
  {
    "path": "tools/stove-cli/.idea/.gitignore",
    "content": "# Default ignored files\n/shelf/\n/workspace.xml\n# Ignored default folder with query files\n/queries/\n# Datasource local storage ignored files\n/dataSources/\n/dataSources.local.xml\n# Editor-based HTTP Client requests\n/httpRequests/\n"
  },
  {
    "path": "tools/stove-cli/.idea/copilot.data.migration.ask2agent.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"Ask2AgentMigrationStateService\">\n    <option name=\"migrationStatus\" value=\"COMPLETED\" />\n  </component>\n</project>"
  },
  {
    "path": "tools/stove-cli/.idea/inspectionProfiles/Project_Default.xml",
    "content": "<component name=\"InspectionProjectProfileManager\">\n  <profile version=\"1.0\">\n    <option name=\"myName\" value=\"Project Default\" />\n    <inspection_tool class=\"KotlinUnusedImport\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\" />\n    <inspection_tool class=\"RedundantSemicolon\" enabled=\"true\" level=\"ERROR\" enabled_by_default=\"true\" />\n  </profile>\n</component>"
  },
  {
    "path": "tools/stove-cli/.idea/modules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n      <module fileurl=\"file://$PROJECT_DIR$/.idea/stove-dashboard-cli.iml\" filepath=\"$PROJECT_DIR$/.idea/stove-dashboard-cli.iml\" />\n    </modules>\n  </component>\n</project>"
  },
  {
    "path": "tools/stove-cli/.idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$/../..\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "tools/stove-cli/Cargo.toml",
    "content": "[package]\nname = \"stove-cli\"\nversion = \"0.0.0\"\nedition = \"2024\"\ndescription = \"CLI for Stove — local observability dashboard for e2e test runs\"\n\n[lib]\nname = \"stove\"\npath = \"src/lib.rs\"\n\n[[bin]]\nname = \"stove\"\npath = \"src/main.rs\"\n\n[dependencies]\n# Async runtime\ntokio = { version = \"1\", features = [\"full\"] }\n\n# gRPC server (receives events from Stove test process)\ntonic = \"0.14\"\ntonic-prost = \"0.14\"\nprost = \"0.14\"\nprost-types = \"0.14\"\n\n# HTTP server (serves REST API + SPA to browser)\naxum = { version = \"0.8\", features = [\"json\"] }\ntower-http = { version = \"0.6\", features = [\"cors\"] }\n\n# SSE (live updates to browser)\ntokio-stream = { version = \"0.1\", features = [\"sync\"] }\n\n# Database\nrusqlite = { version = \"0.39\", features = [\"bundled\"] }\n\n\n# Serialization\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n\n# CLI argument parsing\nclap = { version = \"4\", features = [\"derive\"] }\n\n# Embed SPA static files into binary\nrust-embed = \"8\"\nmime_guess = \"2\"\n\n# Error handling\nthiserror = \"2\"\nanyhow = \"1\"\n\n# Logging\ntracing = \"0.1\"\ntracing-subscriber = { version = \"0.3\", features = [\"env-filter\"] }\n\n# Utilities\nchrono = { version = \"0.4\", features = [\"serde\"] }\nuuid = { version = \"1\", features = [\"v4\"] }\n\n# HTTP client (used for the Stove agent skills sync against GitHub)\nreqwest = { version = \"0.13\", default-features = false, features = [\"json\", \"rustls\", \"gzip\", \"http2\"] }\n\n[dev-dependencies]\ntempfile = \"3\"\n\n[build-dependencies]\ntonic-build = \"0.14\"\ntonic-prost-build = \"0.14\"\n"
  },
  {
    "path": "tools/stove-cli/Formula/stove.rb",
    "content": "# Homebrew formula for Stove CLI.\n# Managed by the stove-cli-release workflow — do not edit checksums manually.\n#\n# Install:\n#   brew install Trendyol/trendyol-tap/stove\nclass Stove < Formula\n  desc \"Local observability dashboard for Stove e2e test runs\"\n  homepage \"https://github.com/Trendyol/stove\"\n  version \"__VERSION__\"\n  license \"Apache-2.0\"\n\n  on_macos do\n    if Hardware::CPU.arm?\n      url \"https://github.com/Trendyol/stove/releases/download/v#{version}/stove-#{version}-darwin-arm64.tar.gz\"\n      sha256 \"__SHA256_DARWIN_ARM64__\"\n    end\n    if Hardware::CPU.intel?\n      url \"https://github.com/Trendyol/stove/releases/download/v#{version}/stove-#{version}-darwin-amd64.tar.gz\"\n      sha256 \"__SHA256_DARWIN_AMD64__\"\n    end\n  end\n\n  on_linux do\n    if Hardware::CPU.intel?\n      url \"https://github.com/Trendyol/stove/releases/download/v#{version}/stove-#{version}-linux-amd64.tar.gz\"\n      sha256 \"__SHA256_LINUX_AMD64__\"\n    end\n  end\n\n  def install\n    bin.install \"stove\"\n  end\n\n  test do\n    assert_match version.to_s, shell_output(\"#{bin}/stove --version\")\n  end\nend\n"
  },
  {
    "path": "tools/stove-cli/build.rs",
    "content": "use std::path::Path;\nuse std::process::Command;\n\nfn main() -> Result<(), Box<dyn std::error::Error>> {\n  // ── Proto codegen ──────────────────────────────────────────────\n  tonic_prost_build::configure()\n    .build_server(true)\n    .build_client(false)\n    .compile_protos(\n      &[\n        \"../../lib/stove-dashboard-api/src/main/proto/stove/dashboard/v1/dashboard_events.proto\",\n        \"../../lib/stove-dashboard-api/src/main/proto/stove/dashboard/v1/dashboard_service.proto\",\n      ],\n      &[\"../../lib/stove-dashboard-api/src/main/proto/\"],\n    )?;\n\n  // ── Version from gradle.properties ─────────────────────────────\n  let gradle_props = std::fs::read_to_string(\"../../gradle.properties\")\n    .expect(\"Failed to read gradle.properties — is this running from tools/stove-cli?\");\n  let version = gradle_props\n    .lines()\n    .find_map(|line| line.strip_prefix(\"version=\"))\n    .expect(\"No 'version=' line found in gradle.properties\");\n  println!(\"cargo:rustc-env=STOVE_VERSION={version}\");\n  println!(\"cargo:rerun-if-changed=../../gradle.properties\");\n\n  // ── Build SPA if needed ────────────────────────────────────────\n  build_spa();\n\n  Ok(())\n}\n\n/// Build the SPA when `spa/dist/index.html` is missing or SPA sources changed.\n/// Skipped if `SKIP_SPA_BUILD=1` (useful for CI when SPA is pre-built).\nfn build_spa() {\n  if std::env::var(\"SKIP_SPA_BUILD\").unwrap_or_default() == \"1\" {\n    return;\n  }\n\n  let spa_dir = Path::new(\"spa\");\n\n  // Rebuild when any SPA source file changes\n  println!(\"cargo:rerun-if-changed=spa/src\");\n  println!(\"cargo:rerun-if-changed=spa/index.html\");\n  println!(\"cargo:rerun-if-changed=spa/package.json\");\n\n  if !spa_dir.join(\"package.json\").exists() {\n    eprintln!(\"cargo:warning=spa/package.json not found — skipping SPA build\");\n    return;\n  }\n\n  // Install deps if node_modules is missing\n  if !spa_dir.join(\"node_modules\").exists() {\n    run_npm(spa_dir, &[\"install\"]);\n  }\n\n  // Always rebuild — cargo only re-runs build.rs when spa/src changes,\n  // and Vite's own caching keeps no-op builds fast.\n  run_npm(spa_dir, &[\"run\", \"build\"]);\n}\n\nfn run_npm(dir: &Path, args: &[&str]) {\n  let status = Command::new(\"npm\")\n    .args(args)\n    .current_dir(dir)\n    .status()\n    .unwrap_or_else(|e| panic!(\"Failed to run npm {}: {e}\", args.join(\" \")));\n  assert!(status.success(), \"npm {} failed\", args.join(\" \"));\n}\n"
  },
  {
    "path": "tools/stove-cli/clippy.toml",
    "content": "too-many-arguments-threshold = 8\n"
  },
  {
    "path": "tools/stove-cli/install.sh",
    "content": "#!/usr/bin/env sh\n# Stove CLI installer (https://github.com/Trendyol/stove)\n#\n# Usage:\n#   curl -fsSL https://raw.githubusercontent.com/Trendyol/stove/main/tools/stove-cli/install.sh | sh\n#   curl -fsSL ... | sh -s -- --version 0.23.0\n#   curl -fsSL ... | sh -s -- --dir /usr/local/bin\n\nset -eu\n\nREPO=\"Trendyol/stove\"\nBINARY_NAME=\"stove\"\nINSTALL_DIR=\"${STOVE_INSTALL_DIR:-}\"\nVERSION=\"\"\n\n# ── Parse arguments ─────────────────────────────────────────────────\n\nwhile [ $# -gt 0 ]; do\n  case \"$1\" in\n    --version) VERSION=\"$2\"; shift 2 ;;\n    --dir)     INSTALL_DIR=\"$2\"; shift 2 ;;\n    *)         echo \"Unknown option: $1\"; exit 1 ;;\n  esac\ndone\n\n# ── Detect platform ────────────────────────────────────────────────\n\ndetect_platform() {\n  OS=\"$(uname -s)\"\n  ARCH=\"$(uname -m)\"\n\n  case \"$OS\" in\n    Darwin) OS_LABEL=\"darwin\" ;;\n    Linux)  OS_LABEL=\"linux\" ;;\n    *)      echo \"Error: Unsupported OS: $OS\"; exit 1 ;;\n  esac\n\n  case \"$ARCH\" in\n    arm64|aarch64) ARCH_LABEL=\"arm64\" ;;\n    x86_64|amd64)  ARCH_LABEL=\"amd64\" ;;\n    *)             echo \"Error: Unsupported architecture: $ARCH\"; exit 1 ;;\n  esac\n\n  # Linux arm64 is not currently built\n  if [ \"$OS_LABEL\" = \"linux\" ] && [ \"$ARCH_LABEL\" = \"arm64\" ]; then\n    echo \"Error: Linux arm64 binaries are not available yet.\"\n    echo \"Supported platforms: macOS (arm64, amd64), Linux (amd64)\"\n    exit 1\n  fi\n\n  PLATFORM=\"${OS_LABEL}-${ARCH_LABEL}\"\n}\n\n# ── Resolve latest version ─────────────────────────────────────────\n\nresolve_version() {\n  if [ -n \"$VERSION\" ]; then\n    return\n  fi\n\n  echo \"Fetching latest release...\"\n  VERSION=$(\n    curl -fsSL \"https://api.github.com/repos/${REPO}/releases\" \\\n      | grep -o '\"tag_name\":\\s*\"v[^\"]*\"' \\\n      | head -1 \\\n      | sed 's/.*\"v\\([^\"]*\\)\".*/\\1/'\n  )\n\n  if [ -z \"$VERSION\" ]; then\n    echo \"Error: Could not determine latest version. Specify --version manually.\"\n    exit 1\n  fi\n}\n\n# ── Resolve install directory ──────────────────────────────────────\n\nresolve_install_dir() {\n  if [ -n \"$INSTALL_DIR\" ]; then\n    return\n  fi\n\n  if [ -d \"/usr/local/bin\" ] && [ -w \"/usr/local/bin\" ]; then\n    INSTALL_DIR=\"/usr/local/bin\"\n  elif [ -d \"$HOME/.local/bin\" ]; then\n    INSTALL_DIR=\"$HOME/.local/bin\"\n  else\n    mkdir -p \"$HOME/.local/bin\"\n    INSTALL_DIR=\"$HOME/.local/bin\"\n  fi\n}\n\n# ── Download and install ───────────────────────────────────────────\n\ninstall() {\n  ARCHIVE=\"stove-${VERSION}-${PLATFORM}.tar.gz\"\n  DOWNLOAD_URL=\"https://github.com/${REPO}/releases/download/v${VERSION}/${ARCHIVE}\"\n  CHECKSUM_URL=\"${DOWNLOAD_URL}.sha256\"\n\n  TMPDIR=\"$(mktemp -d)\"\n  trap 'rm -rf \"$TMPDIR\"' EXIT\n\n  echo \"Downloading ${BINARY_NAME} v${VERSION} for ${PLATFORM}...\"\n  curl -fsSL -o \"${TMPDIR}/${ARCHIVE}\" \"$DOWNLOAD_URL\"\n  curl -fsSL -o \"${TMPDIR}/${ARCHIVE}.sha256\" \"$CHECKSUM_URL\"\n\n  # Verify checksum\n  echo \"Verifying checksum...\"\n  cd \"$TMPDIR\"\n  if command -v sha256sum >/dev/null 2>&1; then\n    sha256sum -c \"${ARCHIVE}.sha256\"\n  elif command -v shasum >/dev/null 2>&1; then\n    shasum -a 256 -c \"${ARCHIVE}.sha256\"\n  else\n    echo \"Warning: No sha256sum or shasum found, skipping checksum verification.\"\n  fi\n\n  # Extract\n  tar xzf \"${ARCHIVE}\"\n\n  # Install\n  if [ -w \"$INSTALL_DIR\" ]; then\n    mv \"${BINARY_NAME}\" \"${INSTALL_DIR}/${BINARY_NAME}\"\n  else\n    echo \"Installing to ${INSTALL_DIR} (requires sudo)...\"\n    sudo mv \"${BINARY_NAME}\" \"${INSTALL_DIR}/${BINARY_NAME}\"\n  fi\n\n  chmod +x \"${INSTALL_DIR}/${BINARY_NAME}\"\n}\n\n# ── Main ───────────────────────────────────────────────────────────\n\nmain() {\n  detect_platform\n  resolve_version\n  resolve_install_dir\n  install\n\n  echo \"\"\n  echo \"Stove CLI v${VERSION} installed to ${INSTALL_DIR}/${BINARY_NAME}\"\n\n  # Check if install dir is in PATH\n  case \":$PATH:\" in\n    *\":${INSTALL_DIR}:\"*) ;;\n    *)\n      echo \"\"\n      echo \"Note: ${INSTALL_DIR} is not in your PATH.\"\n      echo \"Add it with:  export PATH=\\\"${INSTALL_DIR}:\\$PATH\\\"\"\n      ;;\n  esac\n}\n\nmain\n"
  },
  {
    "path": "tools/stove-cli/rustfmt.toml",
    "content": "edition = \"2024\"\ntab_spaces = 2\n"
  },
  {
    "path": "tools/stove-cli/spa/biome.json",
    "content": "{\n  \"$schema\": \"https://biomejs.dev/schemas/2.4.9/schema.json\",\n  \"formatter\": {\n    \"indentStyle\": \"space\",\n    \"indentWidth\": 2,\n    \"lineWidth\": 100\n  },\n  \"linter\": {\n    \"enabled\": true,\n    \"rules\": {\n      \"style\": {\n        \"noNonNullAssertion\": \"off\"\n      },\n      \"suspicious\": {\n        \"noUnknownAtRules\": \"off\"\n      }\n    }\n  },\n  \"javascript\": {\n    \"formatter\": {\n      \"quoteStyle\": \"double\",\n      \"semicolons\": \"always\",\n      \"trailingCommas\": \"all\"\n    }\n  },\n  \"css\": {\n    \"parser\": {\n      \"cssModules\": false,\n      \"tailwindDirectives\": true\n    }\n  },\n  \"files\": {\n    \"includes\": [\"src/**\"]\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Stove Dashboard</title>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    <link href=\"https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap\" rel=\"stylesheet\" />\n  </head>\n  <body class=\"bg-stove-base text-gray-300 font-sans\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "tools/stove-cli/spa/package.json",
    "content": "{\n  \"name\": \"stove-dashboard-spa\",\n  \"private\": true,\n  \"version\": \"0.1.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"preview\": \"vite preview\",\n    \"check\": \"biome check src\",\n    \"format\": \"biome check --write src\"\n  },\n  \"dependencies\": {\n    \"@dagrejs/dagre\": \"^3.0.0\",\n    \"@tanstack/react-query\": \"^5.80.0\",\n    \"@xyflow/react\": \"^12.10.2\",\n    \"react\": \"^19.1.0\",\n    \"react-dom\": \"^19.1.0\"\n  },\n  \"devDependencies\": {\n    \"@biomejs/biome\": \"2.4.15\",\n    \"@tailwindcss/postcss\": \"^4.0.0\",\n    \"@types/react\": \"^19.1.0\",\n    \"@types/react-dom\": \"^19.1.0\",\n    \"@vitejs/plugin-react\": \"^6.0.0\",\n    \"tailwindcss\": \"^4.0.0\",\n    \"typescript\": \"~6.0.0\",\n    \"vite\": \"^8.0.0\"\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/postcss.config.js",
    "content": "export default {\n  plugins: {\n    \"@tailwindcss/postcss\": {},\n  },\n};\n"
  },
  {
    "path": "tools/stove-cli/spa/src/App.tsx",
    "content": "import { VersionMismatchBanner } from \"./components/VersionMismatchBanner\";\nimport { useAppData } from \"./hooks/useAppData\";\nimport { Header } from \"./layout/Header\";\nimport { Sidebar } from \"./layout/Sidebar\";\nimport { TestDetail } from \"./layout/TestDetail\";\n\nexport default function App() {\n  const {\n    apps,\n    activeApp,\n    latestRun,\n    tests,\n    selectedTest,\n    liveConnected,\n    mismatchedApps,\n    versionMismatchSummary,\n    selectApp,\n    selectTest,\n  } = useAppData();\n\n  return (\n    <div className=\"flex flex-col h-screen bg-stove-base text-[var(--stove-text)] font-sans\">\n      <Header />\n      {versionMismatchSummary ? <VersionMismatchBanner summary={versionMismatchSummary} /> : null}\n      <div className=\"flex flex-1 overflow-hidden\">\n        <Sidebar\n          apps={apps}\n          mismatchedApps={mismatchedApps}\n          selectedApp={activeApp}\n          onSelectApp={selectApp}\n          run={latestRun}\n          tests={tests}\n          selectedTestId={selectedTest?.id ?? null}\n          onSelectTest={selectTest}\n        />\n        {latestRun && selectedTest ? (\n          <TestDetail runId={latestRun.id} test={selectedTest} liveConnected={liveConnected} />\n        ) : (\n          <div className=\"flex-1 flex items-center justify-center text-[var(--stove-text-muted)] text-sm\">\n            {apps.length === 0 ? \"Waiting for test events...\" : \"Select a test to view details\"}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/api/client.ts",
    "content": "import type { AppSummary, Entry, MetaResponse, Run, Snapshot, Span, Test } from \"./types\";\n\nconst BASE = \"/api/v1\";\nconst encodePath = (value: string) => encodeURIComponent(value);\n\nasync function get<T>(url: string): Promise<T> {\n  const res = await fetch(`${BASE}${url}`);\n  if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);\n  return res.json();\n}\n\nasync function del(url: string): Promise<void> {\n  const res = await fetch(`${BASE}${url}`, { method: \"DELETE\" });\n  if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);\n}\n\nexport const api = {\n  getMeta: () => get<MetaResponse>(\"/meta\"),\n  getApps: () => get<AppSummary[]>(\"/apps\"),\n  getRuns: (app?: string) => get<Run[]>(app ? `/runs?app=${encodeURIComponent(app)}` : \"/runs\"),\n  getRun: (runId: string) => get<Run | null>(`/runs/${encodePath(runId)}`),\n  getTests: (runId: string) => get<Test[]>(`/runs/${encodePath(runId)}/tests`),\n  getEntries: (runId: string, testId: string) =>\n    get<Entry[]>(`/runs/${encodePath(runId)}/tests/${encodePath(testId)}/entries`),\n  getSpans: (runId: string, testId: string) =>\n    get<Span[]>(`/runs/${encodePath(runId)}/tests/${encodePath(testId)}/spans`),\n  getSnapshots: (runId: string, testId: string) =>\n    get<Snapshot[]>(`/runs/${encodePath(runId)}/tests/${encodePath(testId)}/snapshots`),\n  getTrace: (traceId: string) => get<Span[]>(`/traces/${encodePath(traceId)}`),\n  clearAll: () => del(\"/data\"),\n};\n"
  },
  {
    "path": "tools/stove-cli/spa/src/api/live-cache.ts",
    "content": "import type { QueryClient } from \"@tanstack/react-query\";\nimport type { Status } from \"../utils/status\";\nimport type { AppSummary, Entry, LiveDashboardEvent, Run, Snapshot, Span, Test } from \"./types\";\nimport { EVENT_TYPE } from \"./types\";\n\nconst RUNNING: Status = \"RUNNING\";\n\nexport function applyLiveDashboardEvent(queryClient: QueryClient, event: LiveDashboardEvent) {\n  switch (event.event_type) {\n    case EVENT_TYPE.RUN_STARTED: {\n      const run: Run = {\n        id: event.run_id,\n        app_name: event.payload.app_name,\n        started_at: event.payload.started_at,\n        ended_at: null,\n        status: RUNNING,\n        total_tests: 0,\n        passed: 0,\n        failed: 0,\n        duration_ms: null,\n        stove_version: event.payload.stove_version,\n        systems: event.payload.systems,\n      };\n\n      queryClient.setQueryData<AppSummary[]>([\"apps\"], (apps) =>\n        upsertAppSummary(apps, {\n          app_name: event.payload.app_name,\n          latest_run_id: event.run_id,\n          latest_status: RUNNING,\n          stove_version: event.payload.stove_version,\n          total_runs: nextRunCount(apps, event.payload.app_name, event.run_id),\n        }),\n      );\n      queryClient.setQueryData<Run[]>([\"runs\", event.payload.app_name], (runs) =>\n        upsertRun(runs, run),\n      );\n      queryClient.setQueryData<Test[]>([\"tests\", event.run_id], (tests) => tests ?? []);\n      break;\n    }\n    case EVENT_TYPE.RUN_ENDED: {\n      updateCachedRuns(queryClient, event.run_id, (run) => ({\n        ...run,\n        ended_at: event.payload.ended_at,\n        status: event.payload.status,\n        total_tests: event.payload.total_tests,\n        passed: event.payload.passed,\n        failed: event.payload.failed,\n        duration_ms: event.payload.duration_ms,\n      }));\n      queryClient.setQueryData<AppSummary[]>(\n        [\"apps\"],\n        (apps) =>\n          apps?.map((app) =>\n            app.latest_run_id === event.run_id\n              ? { ...app, latest_status: event.payload.status }\n              : app,\n          ) ?? apps,\n      );\n      break;\n    }\n    case EVENT_TYPE.TEST_STARTED: {\n      const test: Test = {\n        id: event.payload.test_id,\n        run_id: event.run_id,\n        test_name: event.payload.test_name,\n        spec_name: event.payload.spec_name,\n        test_path: event.payload.test_path ?? [],\n        started_at: event.payload.started_at,\n        ended_at: null,\n        status: event.payload.status,\n        duration_ms: null,\n        error: null,\n      };\n\n      queryClient.setQueryData<Test[]>([\"tests\", event.run_id], (tests) => upsertTest(tests, test));\n      queryClient.setQueryData<Entry[]>(\n        [\"entries\", event.run_id, event.payload.test_id],\n        (entries) => entries ?? [],\n      );\n      queryClient.setQueryData<Span[]>(\n        [\"spans\", event.run_id, event.payload.test_id],\n        (spans) => spans ?? [],\n      );\n      queryClient.setQueryData<Snapshot[]>(\n        [\"snapshots\", event.run_id, event.payload.test_id],\n        (snapshots) => snapshots ?? [],\n      );\n      break;\n    }\n    case EVENT_TYPE.TEST_ENDED: {\n      updateCachedTests(queryClient, event.run_id, event.payload.test_id, (test) => ({\n        ...test,\n        ended_at: event.payload.ended_at,\n        status: event.payload.status,\n        duration_ms: event.payload.duration_ms,\n        error: event.payload.error,\n      }));\n      break;\n    }\n    case EVENT_TYPE.ENTRY_RECORDED: {\n      const entry: Entry = {\n        id: event.payload.id,\n        run_id: event.run_id,\n        test_id: event.payload.test_id,\n        timestamp: event.payload.timestamp,\n        system: event.payload.system,\n        action: event.payload.action,\n        result: event.payload.result,\n        input: event.payload.input,\n        output: event.payload.output,\n        metadata: event.payload.metadata,\n        expected: event.payload.expected,\n        actual: event.payload.actual,\n        error: event.payload.error,\n        trace_id: event.payload.trace_id,\n      };\n\n      queryClient.setQueryData<Entry[]>(\n        [\"entries\", event.run_id, event.payload.test_id],\n        (entries) => appendEntries(entries, entry),\n      );\n\n      if (event.payload.trace_id) {\n        const traceSpans = queryClient.getQueryData<Span[]>([\"trace\", event.payload.trace_id]);\n        if (traceSpans?.length) {\n          queryClient.setQueryData<Span[]>(\n            [\"spans\", event.run_id, event.payload.test_id],\n            (spans) => mergeSpans(spans, traceSpans),\n          );\n        }\n      }\n      break;\n    }\n    case EVENT_TYPE.SPAN_RECORDED: {\n      const span: Span = {\n        id: event.payload.id,\n        run_id: event.run_id,\n        trace_id: event.payload.trace_id,\n        span_id: event.payload.span_id,\n        parent_span_id: event.payload.parent_span_id,\n        operation_name: event.payload.operation_name,\n        service_name: event.payload.service_name,\n        start_time_nanos: event.payload.start_time_nanos,\n        end_time_nanos: event.payload.end_time_nanos,\n        status: event.payload.status,\n        attributes: event.payload.attributes,\n        exception_type: event.payload.exception_type,\n        exception_message: event.payload.exception_message,\n        exception_stack_trace: event.payload.exception_stack_trace,\n      };\n\n      queryClient.setQueryData<Span[]>([\"trace\", event.payload.trace_id], (trace) =>\n        appendSpan(trace, span),\n      );\n\n      const testId =\n        event.payload.test_id ??\n        findTestIdForTrace(queryClient, event.run_id, event.payload.trace_id);\n      if (testId) {\n        queryClient.setQueryData<Span[]>([\"spans\", event.run_id, testId], (spans) =>\n          appendSpan(spans, span),\n        );\n      }\n      break;\n    }\n    case EVENT_TYPE.SNAPSHOT: {\n      const snapshot: Snapshot = {\n        id: event.payload.id,\n        run_id: event.run_id,\n        test_id: event.payload.test_id,\n        system: event.payload.system,\n        state_json: event.payload.state_json,\n        summary: event.payload.summary,\n      };\n\n      queryClient.setQueryData<Snapshot[]>(\n        [\"snapshots\", event.run_id, event.payload.test_id],\n        (snapshots) => appendSnapshots(snapshots, snapshot),\n      );\n      break;\n    }\n  }\n}\n\nexport function invalidateDashboardQueries(queryClient: QueryClient, runId?: string) {\n  queryClient.invalidateQueries({ queryKey: [\"apps\"] });\n  queryClient.invalidateQueries({ queryKey: [\"runs\"] });\n  if (runId) {\n    queryClient.invalidateQueries({ queryKey: [\"tests\", runId] });\n    queryClient.invalidateQueries({ queryKey: [\"entries\", runId] });\n    queryClient.invalidateQueries({ queryKey: [\"spans\", runId] });\n    queryClient.invalidateQueries({ queryKey: [\"snapshots\", runId] });\n  } else {\n    queryClient.invalidateQueries();\n  }\n}\n\nfunction upsertAppSummary(apps: AppSummary[] | undefined, incoming: AppSummary): AppSummary[] {\n  return [...(apps ?? []).filter((app) => app.app_name !== incoming.app_name), incoming].sort(\n    (left, right) => left.app_name.localeCompare(right.app_name),\n  );\n}\n\nfunction nextRunCount(apps: AppSummary[] | undefined, appName: string, runId: string): number {\n  const existing = apps?.find((app) => app.app_name === appName);\n  if (!existing) {\n    return 1;\n  }\n  return existing.latest_run_id === runId ? existing.total_runs : existing.total_runs + 1;\n}\n\nfunction upsertRun(runs: Run[] | undefined, incoming: Run): Run[] {\n  return [...(runs ?? []).filter((run) => run.id !== incoming.id), incoming].sort(compareRuns);\n}\n\nfunction upsertTest(tests: Test[] | undefined, incoming: Test): Test[] {\n  return [...(tests ?? []).filter((test) => test.id !== incoming.id), incoming].sort(compareTests);\n}\n\nfunction updateCachedRuns(queryClient: QueryClient, runId: string, updater: (run: Run) => Run) {\n  for (const [queryKey, runs] of queryClient.getQueriesData<Run[]>({ queryKey: [\"runs\"] })) {\n    if (!runs?.some((run) => run.id === runId)) {\n      continue;\n    }\n    queryClient.setQueryData(\n      queryKey,\n      runs.map((run) => (run.id === runId ? updater(run) : run)).sort(compareRuns),\n    );\n  }\n}\n\nfunction updateCachedTests(\n  queryClient: QueryClient,\n  runId: string,\n  testId: string,\n  updater: (test: Test) => Test,\n) {\n  queryClient.setQueryData<Test[]>(\n    [\"tests\", runId],\n    (tests) =>\n      tests?.map((test) => (test.id === testId ? updater(test) : test)).sort(compareTests) ?? tests,\n  );\n}\n\nfunction appendEntries(entries: Entry[] | undefined, incoming: Entry): Entry[] {\n  if (entries?.some((entry) => entry.id === incoming.id)) {\n    return entries;\n  }\n  return [...(entries ?? []), incoming].sort((left, right) =>\n    left.timestamp.localeCompare(right.timestamp),\n  );\n}\n\nfunction appendSpan(spans: Span[] | undefined, incoming: Span): Span[] {\n  if (spans?.some((span) => isSameSpan(span, incoming))) {\n    return spans;\n  }\n  return [...(spans ?? []), incoming].sort(\n    (left, right) => left.start_time_nanos - right.start_time_nanos,\n  );\n}\n\nfunction mergeSpans(existing: Span[] | undefined, incoming: Span[]): Span[] {\n  return incoming.reduce<Span[]>((acc, span) => appendSpan(acc, span), existing ?? []);\n}\n\nfunction appendSnapshots(snapshots: Snapshot[] | undefined, incoming: Snapshot): Snapshot[] {\n  if (\n    snapshots?.some(\n      (snapshot) =>\n        snapshot.system === incoming.system &&\n        snapshot.summary === incoming.summary &&\n        snapshot.state_json === incoming.state_json,\n    )\n  ) {\n    return snapshots;\n  }\n  return [...(snapshots ?? []), incoming];\n}\n\nfunction compareRuns(left: Run, right: Run): number {\n  return right.started_at.localeCompare(left.started_at) || right.id.localeCompare(left.id);\n}\n\nfunction compareTests(left: Test, right: Test): number {\n  return left.started_at.localeCompare(right.started_at) || left.id.localeCompare(right.id);\n}\n\nfunction isSameSpan(left: Span, right: Span): boolean {\n  return left.trace_id === right.trace_id && left.span_id === right.span_id;\n}\n\nfunction findTestIdForTrace(\n  queryClient: QueryClient,\n  runId: string,\n  traceId: string,\n): string | null {\n  for (const [queryKey, entries] of queryClient.getQueriesData<Entry[]>({\n    queryKey: [\"entries\", runId],\n  })) {\n    if (!entries?.some((entry) => entry.trace_id === traceId)) {\n      continue;\n    }\n    if (Array.isArray(queryKey) && typeof queryKey[2] === \"string\") {\n      return queryKey[2];\n    }\n  }\n  return null;\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/api/sse.ts",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport type { LiveDashboardEvent } from \"./types\";\n\ninterface UseSSEOptions {\n  onEvent: (event: LiveDashboardEvent) => void;\n  onGap?: (event: LiveDashboardEvent) => void;\n  onReconnect?: () => void;\n  onDisconnect?: () => void;\n}\n\nexport function useSSE({ onEvent, onGap, onReconnect, onDisconnect }: UseSSEOptions) {\n  const callbacksRef = useRef({ onEvent, onGap, onReconnect, onDisconnect });\n  const lastSeqRef = useRef<number | null>(null);\n  const hasConnectedRef = useRef(false);\n  const openRef = useRef(false);\n  const [connected, setConnected] = useState(false);\n\n  callbacksRef.current = { onEvent, onGap, onReconnect, onDisconnect };\n\n  useEffect(() => {\n    let disposed = false;\n    let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n    let source: EventSource | null = null;\n\n    function connect() {\n      if (disposed) {\n        return;\n      }\n\n      source = new EventSource(\"/api/v1/events/stream\");\n\n      source.onopen = () => {\n        const isReconnect = hasConnectedRef.current;\n        hasConnectedRef.current = true;\n        openRef.current = true;\n        setConnected(true);\n        if (isReconnect) {\n          lastSeqRef.current = null;\n          callbacksRef.current.onReconnect?.();\n        }\n      };\n\n      source.onmessage = (message) => {\n        try {\n          const event: LiveDashboardEvent = JSON.parse(message.data);\n          if (\n            typeof event.seq !== \"number\" ||\n            typeof event.run_id !== \"string\" ||\n            typeof event.event_type !== \"string\"\n          ) {\n            return;\n          }\n\n          if (lastSeqRef.current !== null && event.seq !== lastSeqRef.current + 1) {\n            callbacksRef.current.onGap?.(event);\n          }\n          lastSeqRef.current = event.seq;\n          callbacksRef.current.onEvent(event);\n        } catch {\n          // Ignore malformed events\n        }\n      };\n\n      source.onerror = () => {\n        source?.close();\n        source = null;\n        if (openRef.current) {\n          openRef.current = false;\n          setConnected(false);\n          callbacksRef.current.onDisconnect?.();\n        }\n        if (!disposed) {\n          reconnectTimer = setTimeout(connect, 3000);\n        }\n      };\n    }\n\n    connect();\n\n    return () => {\n      disposed = true;\n      if (reconnectTimer != null) {\n        clearTimeout(reconnectTimer);\n      }\n      openRef.current = false;\n      setConnected(false);\n      source?.close();\n    };\n  }, []);\n\n  return { connected };\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/api/types.ts",
    "content": "import type { Status } from \"../utils/status\";\n\nexport type { Status };\n\nexport const EVENT_TYPE = {\n  RUN_STARTED: \"run_started\",\n  RUN_ENDED: \"run_ended\",\n  TEST_STARTED: \"test_started\",\n  TEST_ENDED: \"test_ended\",\n  ENTRY_RECORDED: \"entry_recorded\",\n  SPAN_RECORDED: \"span_recorded\",\n  SNAPSHOT: \"snapshot\",\n} as const;\n\nexport type EventType = (typeof EVENT_TYPE)[keyof typeof EVENT_TYPE];\n\nexport interface AppSummary {\n  app_name: string;\n  latest_run_id: string;\n  latest_status: Status;\n  stove_version: string | null;\n  total_runs: number;\n}\n\nexport interface MetaResponse {\n  stove_cli_version: string;\n}\n\nexport type LiveRecordId = number | string;\n\nexport interface Run {\n  id: string;\n  app_name: string;\n  started_at: string;\n  ended_at: string | null;\n  status: Status;\n  total_tests: number;\n  passed: number;\n  failed: number;\n  duration_ms: number | null;\n  stove_version: string | null;\n  systems: string[];\n}\n\nexport interface Test {\n  id: string;\n  run_id: string;\n  test_name: string;\n  spec_name: string;\n  test_path: string[];\n  started_at: string;\n  ended_at: string | null;\n  status: Status;\n  duration_ms: number | null;\n  error: string | null;\n}\n\nexport interface Entry {\n  id: LiveRecordId;\n  run_id: string;\n  test_id: string;\n  timestamp: string;\n  system: string;\n  action: string;\n  result: string;\n  input: string | null;\n  output: string | null;\n  metadata: string | null;\n  expected: string | null;\n  actual: string | null;\n  error: string | null;\n  trace_id: string | null;\n}\n\nexport interface Span {\n  id: LiveRecordId;\n  run_id: string;\n  trace_id: string;\n  span_id: string;\n  parent_span_id: string | null;\n  operation_name: string;\n  service_name: string;\n  start_time_nanos: number;\n  end_time_nanos: number;\n  status: Status;\n  attributes: string | null;\n  exception_type: string | null;\n  exception_message: string | null;\n  exception_stack_trace: string | null;\n}\n\nexport interface Snapshot {\n  id: LiveRecordId;\n  run_id: string;\n  test_id: string;\n  system: string;\n  state_json: string;\n  summary: string;\n}\n\nexport interface LiveRunStartedPayload {\n  app_name: string;\n  started_at: string;\n  stove_version: string | null;\n  systems: string[];\n}\n\nexport interface LiveRunEndedPayload {\n  ended_at: string;\n  status: Status;\n  total_tests: number;\n  passed: number;\n  failed: number;\n  duration_ms: number;\n}\n\nexport interface LiveTestStartedPayload {\n  test_id: string;\n  test_name: string;\n  spec_name: string;\n  test_path: string[];\n  started_at: string;\n  status: Status;\n}\n\nexport interface LiveTestEndedPayload {\n  test_id: string;\n  status: Status;\n  duration_ms: number;\n  error: string | null;\n  ended_at: string;\n}\n\nexport interface LiveEntryRecordedPayload {\n  id: LiveRecordId;\n  test_id: string;\n  timestamp: string;\n  system: string;\n  action: string;\n  result: string;\n  input: string | null;\n  output: string | null;\n  metadata: string | null;\n  expected: string | null;\n  actual: string | null;\n  error: string | null;\n  trace_id: string | null;\n}\n\nexport interface LiveSpanRecordedPayload {\n  id: LiveRecordId;\n  test_id: string | null;\n  trace_id: string;\n  span_id: string;\n  parent_span_id: string | null;\n  operation_name: string;\n  service_name: string;\n  start_time_nanos: number;\n  end_time_nanos: number;\n  status: Status;\n  attributes: string | null;\n  exception_type: string | null;\n  exception_message: string | null;\n  exception_stack_trace: string | null;\n}\n\nexport interface LiveSnapshotPayload {\n  id: LiveRecordId;\n  test_id: string;\n  system: string;\n  state_json: string;\n  summary: string;\n}\n\ninterface LiveEventBase {\n  seq: number;\n  run_id: string;\n}\n\nexport type LiveDashboardEvent =\n  | (LiveEventBase & { event_type: typeof EVENT_TYPE.RUN_STARTED; payload: LiveRunStartedPayload })\n  | (LiveEventBase & { event_type: typeof EVENT_TYPE.RUN_ENDED; payload: LiveRunEndedPayload })\n  | (LiveEventBase & {\n      event_type: typeof EVENT_TYPE.TEST_STARTED;\n      payload: LiveTestStartedPayload;\n    })\n  | (LiveEventBase & { event_type: typeof EVENT_TYPE.TEST_ENDED; payload: LiveTestEndedPayload })\n  | (LiveEventBase & {\n      event_type: typeof EVENT_TYPE.ENTRY_RECORDED;\n      payload: LiveEntryRecordedPayload;\n    })\n  | (LiveEventBase & {\n      event_type: typeof EVENT_TYPE.SPAN_RECORDED;\n      payload: LiveSpanRecordedPayload;\n    })\n  | (LiveEventBase & { event_type: typeof EVENT_TYPE.SNAPSHOT; payload: LiveSnapshotPayload });\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/Badge.tsx",
    "content": "import type { Status } from \"../api/types\";\nimport { useTheme } from \"../hooks/useTheme\";\nimport { isRunning } from \"../utils/status\";\n\ninterface BadgeProps {\n  status: Status;\n}\n\nexport function Badge({ status }: BadgeProps) {\n  const { theme } = useTheme();\n  const upper = status.toUpperCase() as Status;\n  const configs = theme === \"dark\" ? DARK : LIGHT;\n  const config = configs[upper] ?? configs.DEFAULT;\n\n  return (\n    <span\n      role=\"status\"\n      aria-label={`Status: ${upper}`}\n      className=\"inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-mono font-medium\"\n      style={{ backgroundColor: config.bg, color: config.text }}\n    >\n      {config.icon}\n      {isRunning(upper) && (\n        <span\n          className=\"w-1.5 h-1.5 rounded-full animate-pulse-dot\"\n          style={{ backgroundColor: config.text }}\n        />\n      )}\n      {!isRunning(upper) && upper}\n    </span>\n  );\n}\n\ninterface BadgeStyle {\n  bg: string;\n  text: string;\n  icon: string;\n}\n\nconst DARK: Record<Status | \"DEFAULT\", BadgeStyle> = {\n  PASSED: { bg: \"#06291e\", text: \"#34d399\", icon: \"\\u2713 \" },\n  FAILED: { bg: \"#2d0a0a\", text: \"#f87171\", icon: \"\\u2717 \" },\n  ERROR: { bg: \"#2d0a0a\", text: \"#f87171\", icon: \"\\u2717 \" },\n  RUNNING: { bg: \"#0f1d2e\", text: \"#60a5fa\", icon: \"\" },\n  DEFAULT: { bg: \"#1e293b\", text: \"#94a3b8\", icon: \"\" },\n};\n\nconst LIGHT: Record<Status | \"DEFAULT\", BadgeStyle> = {\n  PASSED: { bg: \"#d1fae5\", text: \"#065f46\", icon: \"\\u2713 \" },\n  FAILED: { bg: \"#fee2e2\", text: \"#991b1b\", icon: \"\\u2717 \" },\n  ERROR: { bg: \"#fee2e2\", text: \"#991b1b\", icon: \"\\u2717 \" },\n  RUNNING: { bg: \"#dbeafe\", text: \"#1e40af\", icon: \"\" },\n  DEFAULT: { bg: \"#e5e7eb\", text: \"#4b5563\", icon: \"\" },\n};\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/CapturedStateLane.tsx",
    "content": "import type { Snapshot } from \"../api/types\";\nimport { describeJsonValue, getJsonPreviewKeys, parseJsonDeep } from \"../utils/json\";\nimport { getSystemInfo } from \"../utils/systems\";\n\ninterface CapturedStateLaneProps {\n  snapshots: Snapshot[];\n  onSelect: (snapshot: Snapshot) => void;\n}\n\nexport function CapturedStateLane({ snapshots, onSelect }: CapturedStateLaneProps) {\n  if (snapshots.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"shrink-0 border-t border-stove-border bg-stove-surface\">\n      <div className=\"flex items-center justify-between px-3 py-2\">\n        <div className=\"text-[11px] font-medium uppercase tracking-[0.18em] text-[var(--stove-text-muted)]\">\n          Captured State\n        </div>\n        <div className=\"text-[11px] text-[var(--stove-text-secondary)]\">\n          {snapshots.length} snapshot{snapshots.length === 1 ? \"\" : \"s\"}\n        </div>\n      </div>\n      <div className=\"flex gap-3 overflow-x-auto px-3 pb-3\">\n        {snapshots.map((snapshot) => (\n          <SnapshotLaneCard key={snapshot.id} snapshot={snapshot} onSelect={onSelect} />\n        ))}\n      </div>\n    </div>\n  );\n}\n\nfunction SnapshotLaneCard({\n  snapshot,\n  onSelect,\n}: {\n  snapshot: Snapshot;\n  onSelect: (snapshot: Snapshot) => void;\n}) {\n  const info = getSystemInfo(snapshot.system);\n  const parsedState = parseJsonDeep(snapshot.state_json);\n  const previewKeys = getJsonPreviewKeys(parsedState, 3);\n  return (\n    <button\n      type=\"button\"\n      className=\"w-[260px] shrink-0 cursor-pointer rounded-lg border border-stove-border bg-stove-card px-3 py-2 text-left hover:bg-[var(--stove-hover)]\"\n      style={{ borderLeftColor: info.color, borderLeftWidth: 3 }}\n      onClick={() => onSelect(snapshot)}\n    >\n      <div className=\"mb-1 flex items-center gap-2\">\n        <span style={{ color: info.color }}>{info.icon}</span>\n        <span className=\"text-sm font-medium text-[var(--stove-text)]\">{snapshot.system}</span>\n      </div>\n\n      <div className=\"line-clamp-2 text-xs text-[var(--stove-text)]\" title={snapshot.summary}>\n        {snapshot.summary}\n      </div>\n\n      <div className=\"mt-2 text-[10px] text-[var(--stove-text-secondary)]\">\n        {parsedState ? describeJsonValue(parsedState) : \"raw text\"}\n      </div>\n\n      {previewKeys.length > 0 && (\n        <div className=\"mt-2 flex flex-wrap gap-1\">\n          {previewKeys.map((key) => (\n            <span\n              key={key}\n              className=\"rounded-full border border-stove-border px-1.5 py-0.5 font-mono text-[10px] text-[var(--stove-text-secondary)]\"\n            >\n              {key}\n            </span>\n          ))}\n        </div>\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/Detail.tsx",
    "content": "import { tryFormatJson } from \"../utils/json\";\n\ninterface DetailProps {\n  label: string;\n  value: string;\n  color?: string;\n}\n\nexport function Detail({ label, value, color }: DetailProps) {\n  return (\n    <div className=\"mt-2\">\n      <span className=\"text-[var(--stove-text-secondary)]\">{label}:</span>\n      <pre\n        className=\"mt-0.5 p-2 bg-stove-base rounded text-xs whitespace-pre-wrap break-words\"\n        style={{ color: color ?? \"var(--stove-text)\" }}\n      >\n        {tryFormatJson(value)}\n      </pre>\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/DurationEdge.tsx",
    "content": "import type { EdgeProps } from \"@xyflow/react\";\nimport { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from \"@xyflow/react\";\nimport type { DurationEdgeData } from \"../utils/flow\";\nimport { formatDuration } from \"../utils/format\";\n\nexport function DurationEdge(props: EdgeProps) {\n  const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd } = props;\n  const d = props.data as DurationEdgeData | undefined;\n\n  const [edgePath, labelX, labelY] = getSmoothStepPath({\n    sourceX,\n    sourceY,\n    targetX,\n    targetY,\n    sourcePosition,\n    targetPosition,\n  });\n\n  const label =\n    d?.label ?? (d?.durationMs != null && d.durationMs > 0 ? formatDuration(d.durationMs) : null);\n\n  return (\n    <>\n      <BaseEdge path={edgePath} markerEnd={markerEnd} style={{ stroke: \"var(--stove-border)\" }} />\n      {label && (\n        <EdgeLabelRenderer>\n          <div\n            className=\"absolute text-[10px] font-mono px-1 py-0.5 rounded bg-stove-surface text-[var(--stove-text-secondary)] border border-stove-border pointer-events-none\"\n            style={{\n              transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,\n            }}\n          >\n            {label}\n          </div>\n        </EdgeLabelRenderer>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/EntryDetails.tsx",
    "content": "import type { Entry } from \"../api/types\";\nimport { Detail } from \"./Detail\";\n\nexport function EntryDetails({ entry }: { entry: Entry }) {\n  return (\n    <>\n      {entry.input && <Detail label=\"Input\" value={entry.input} />}\n      {entry.output && <Detail label=\"Output\" value={entry.output} color=\"var(--stove-green)\" />}\n      {entry.expected && <Detail label=\"Expected\" value={entry.expected} />}\n      {entry.actual && <Detail label=\"Actual\" value={entry.actual} />}\n      {entry.error && <Detail label=\"Error\" value={entry.error} color=\"var(--stove-red)\" />}\n      {entry.metadata && entry.metadata !== \"{}\" && (\n        <Detail label=\"Metadata\" value={entry.metadata} />\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/EntryRow.tsx",
    "content": "import { useState } from \"react\";\nimport type { Entry } from \"../api/types\";\nimport { formatTimestamp } from \"../utils/format\";\nimport { EntryDetails } from \"./EntryDetails\";\nimport { ResultIcon } from \"./ResultIcon\";\nimport { SysBadge } from \"./SysBadge\";\n\ninterface EntryRowProps {\n  entry: Entry;\n}\n\nexport function EntryRow({ entry }: EntryRowProps) {\n  const [expanded, setExpanded] = useState(false);\n  const passed = entry.result === \"PASSED\";\n\n  return (\n    <button\n      type=\"button\"\n      className=\"animate-fade-in border-l-2 bg-stove-card rounded-r mb-1 cursor-pointer hover:bg-[var(--stove-hover)] w-full text-left\"\n      style={{ borderLeftColor: passed ? \"var(--stove-green)\" : \"var(--stove-red)\" }}\n      aria-expanded={expanded}\n      onClick={() => setExpanded(!expanded)}\n    >\n      <div className=\"flex items-center gap-2 px-3 py-2 text-sm\">\n        <span className=\"text-[var(--stove-text-secondary)] font-mono text-xs w-24 shrink-0\">\n          {formatTimestamp(entry.timestamp)}\n        </span>\n        <ResultIcon result={entry.result} />\n        <SysBadge system={entry.system} />\n        <span className=\"text-[var(--stove-text)] truncate\">{entry.action}</span>\n      </div>\n\n      {expanded && (\n        <div className=\"px-4 pb-3 text-xs font-mono space-y-2 border-t border-stove-border\">\n          <EntryDetails entry={entry} />\n        </div>\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/FlowDag.tsx",
    "content": "import {\n  Controls,\n  type Edge,\n  type Node,\n  type NodeMouseHandler,\n  Panel,\n  ReactFlow,\n  useReactFlow,\n} from \"@xyflow/react\";\nimport { useCallback } from \"react\";\nimport type { FlowNodeData } from \"../utils/flow\";\nimport { DurationEdge } from \"./DurationEdge\";\nimport { GapNode } from \"./GapNode\";\nimport { SystemNode } from \"./SystemNode\";\n\nconst nodeTypes = {\n  systemNode: SystemNode,\n  gapNode: GapNode,\n};\nconst edgeTypes = { durationEdge: DurationEdge };\nconst defaultEdgeOptions = { animated: false };\nconst proOptions = { hideAttribution: true };\n\ninterface FlowDagProps {\n  nodes: Node<FlowNodeData>[];\n  edges: Edge[];\n  onNodeClick?: (nodeData: FlowNodeData) => void;\n  compact?: boolean;\n}\n\nexport function FlowDag({ nodes, edges, onNodeClick, compact }: FlowDagProps) {\n  const { fitView } = useReactFlow();\n  const handleNodeClick: NodeMouseHandler = useCallback(\n    (_, node) => {\n      if (onNodeClick) {\n        onNodeClick(node.data as FlowNodeData);\n      }\n    },\n    [onNodeClick],\n  );\n  const handleCenterView = useCallback(() => {\n    void fitView({ padding: 0.18, duration: 250 });\n  }, [fitView]);\n\n  if (nodes.length === 0) {\n    return (\n      <div className=\"flex items-center justify-center h-full text-sm text-[var(--stove-text-secondary)]\">\n        No data to visualize\n      </div>\n    );\n  }\n\n  return (\n    <ReactFlow\n      nodes={nodes}\n      edges={edges}\n      nodeTypes={nodeTypes}\n      edgeTypes={edgeTypes}\n      onNodeClick={handleNodeClick}\n      defaultEdgeOptions={defaultEdgeOptions}\n      fitView\n      nodesDraggable={!compact}\n      nodesConnectable={false}\n      panOnDrag={!compact}\n      zoomOnScroll={!compact}\n      zoomOnPinch={!compact}\n      zoomOnDoubleClick={false}\n      proOptions={proOptions}\n      minZoom={0.2}\n      maxZoom={2}\n      fitViewOptions={{ padding: 0.18 }}\n      className=\"h-full w-full\"\n    >\n      {!compact && <Controls showInteractive={false} />}\n      {!compact && (\n        <Panel position=\"top-right\" className=\"m-3\">\n          <button\n            type=\"button\"\n            className=\"cursor-pointer rounded border border-stove-border bg-stove-surface px-2.5 py-1.5 text-xs text-[var(--stove-text)] shadow-sm hover:bg-[var(--stove-hover)]\"\n            onClick={handleCenterView}\n          >\n            Center View\n          </button>\n        </Panel>\n      )}\n    </ReactFlow>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/FlowTab.tsx",
    "content": "import { ReactFlowProvider } from \"@xyflow/react\";\nimport { useCallback, useMemo, useState } from \"react\";\nimport type { Entry, Snapshot, Span } from \"../api/types\";\nimport type { FlowNodeData, GapNodeData, SystemNodeData } from \"../utils/flow\";\nimport { applyDagreLayout, entriesToDag, spansToTraceDag } from \"../utils/flow\";\nimport { CapturedStateLane } from \"./CapturedStateLane\";\nimport { FlowDag } from \"./FlowDag\";\nimport { NodePopup } from \"./NodePopup\";\nimport { SnapshotStateDialog } from \"./SnapshotStateDialog\";\n\ninterface FlowTabProps {\n  entries: Entry[];\n  spans: Span[];\n  snapshots: Snapshot[];\n  onOpenTraceTab?: (() => void) | undefined;\n}\n\ntype FlowMode = \"timeline\" | \"trace\";\n\nfunction modeButtonClass(active: boolean): string {\n  return `px-2.5 py-1 rounded text-xs cursor-pointer border-0 ${\n    active\n      ? \"bg-[var(--stove-blue)] text-white\"\n      : \"bg-stove-card text-[var(--stove-text-secondary)] hover:text-[var(--stove-text)]\"\n  }`;\n}\n\nexport function FlowTab({ entries, spans, snapshots, onOpenTraceTab }: FlowTabProps) {\n  const [mode, setMode] = useState<FlowMode>(\"timeline\");\n  const [selectedNode, setSelectedNode] = useState<SystemNodeData | null>(null);\n  const [selectedSnapshot, setSelectedSnapshot] = useState<Snapshot | null>(null);\n\n  const { nodes, edges } = useMemo(() => {\n    if (mode === \"trace\" && spans.length > 0) {\n      const dag = spansToTraceDag(spans);\n      return { nodes: applyDagreLayout(dag.nodes, dag.edges), edges: dag.edges };\n    }\n    const dag = entriesToDag(entries);\n    return { nodes: applyDagreLayout(dag.nodes, dag.edges), edges: dag.edges };\n  }, [mode, entries, spans]);\n\n  const handleNodeClick = useCallback((data: FlowNodeData) => {\n    if (!data.inspectable) {\n      return;\n    }\n    setSelectedNode(data);\n  }, []);\n\n  const handleOpenTraceTab = useCallback(() => {\n    setSelectedNode(null);\n    onOpenTraceTab?.();\n  }, [onOpenTraceTab]);\n\n  const summary = useMemo(() => {\n    if (mode === \"trace\") {\n      return `${spans.length} spans`;\n    }\n\n    const stepCount = nodes.filter(\n      (node) => node.type === \"systemNode\" && node.data.kind === \"step\",\n    ).length;\n    const arrangeCount = nodes.filter(\n      (node) => node.type === \"systemNode\" && node.data.kind === \"arrange\",\n    ).length;\n    const gapNodes = nodes.filter((node) => node.type === \"gapNode\");\n    const gapCount = gapNodes.length;\n    const totalGapMs = gapNodes.reduce(\n      (sum, node) => sum + ((node.data as GapNodeData).durationMs ?? 0),\n      0,\n    );\n\n    const parts = [`${stepCount} steps`];\n    if (arrangeCount > 0) {\n      parts.push(`${arrangeCount} arrange`);\n    }\n    if (gapCount > 0) {\n      parts.push(`${gapCount} waits`);\n    }\n    if (snapshots.length > 0) {\n      parts.push(`${snapshots.length} snapshots`);\n    }\n    if (totalGapMs > 0) {\n      parts.push(`${Math.round(totalGapMs / 100) / 10}s idle`);\n    }\n    return parts.join(\" • \");\n  }, [mode, nodes, snapshots.length, spans.length]);\n\n  if (entries.length === 0 && spans.length === 0 && snapshots.length === 0) {\n    return (\n      <div className=\"text-[var(--stove-text-secondary)] text-sm p-4\">No data to visualize</div>\n    );\n  }\n\n  return (\n    <div className=\"flex h-full min-h-0 flex-col overflow-hidden\">\n      <div className=\"flex items-center gap-1 px-3 py-2 border-b border-stove-border shrink-0\">\n        <button\n          type=\"button\"\n          className={modeButtonClass(mode === \"timeline\")}\n          onClick={() => setMode(\"timeline\")}\n        >\n          Timeline Flow\n        </button>\n        {spans.length > 0 && (\n          <button\n            type=\"button\"\n            className={modeButtonClass(mode === \"trace\")}\n            onClick={() => setMode(\"trace\")}\n          >\n            Trace Flow\n          </button>\n        )}\n        <div className=\"ml-auto text-[11px] text-[var(--stove-text-secondary)]\">{summary}</div>\n      </div>\n\n      <div className=\"min-h-0 flex-1\">\n        <ReactFlowProvider>\n          <FlowDag nodes={nodes} edges={edges} onNodeClick={handleNodeClick} />\n        </ReactFlowProvider>\n      </div>\n\n      {mode === \"timeline\" && (\n        <CapturedStateLane snapshots={snapshots} onSelect={setSelectedSnapshot} />\n      )}\n\n      {selectedNode && (\n        <NodePopup\n          entries={selectedNode.entries}\n          traceId={selectedNode.traceId}\n          onClose={() => setSelectedNode(null)}\n          onOpenTrace={selectedNode.traceId ? handleOpenTraceTab : undefined}\n        />\n      )}\n      {selectedSnapshot && (\n        <SnapshotStateDialog\n          snapshot={selectedSnapshot}\n          onClose={() => setSelectedSnapshot(null)}\n        />\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/GapNode.tsx",
    "content": "import type { NodeProps } from \"@xyflow/react\";\nimport { Handle, Position } from \"@xyflow/react\";\nimport type { GapNodeData } from \"../utils/flow\";\nimport { formatDuration, formatTimestamp } from \"../utils/format\";\n\nexport function GapNode({ data }: NodeProps) {\n  const d = data as GapNodeData;\n\n  return (\n    <div className=\"w-[208px] min-h-[96px] rounded-xl border border-dashed border-stove-border bg-stove-base px-3 py-2 text-center\">\n      <Handle type=\"target\" position={Position.Left} className=\"!bg-[var(--stove-border)]\" />\n      <Handle type=\"source\" position={Position.Right} className=\"!bg-[var(--stove-border)]\" />\n\n      <div className=\"text-[10px] font-medium uppercase tracking-[0.18em] text-[var(--stove-text-muted)]\">\n        {d.label}\n      </div>\n      <div className=\"mt-1 text-sm font-semibold text-[var(--stove-text)]\">\n        {formatDuration(d.durationMs)}\n      </div>\n      <div className=\"mt-1 text-[10px] text-[var(--stove-text-secondary)]\">\n        {formatTimestamp(d.startedAt)} to {formatTimestamp(d.endedAt)}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/JsonTree.tsx",
    "content": "import { useState } from \"react\";\n\ninterface JsonTreeProps {\n  value: unknown;\n  defaultExpandedDepth?: number;\n  searchQuery?: string;\n}\n\nexport function JsonTree({ value, defaultExpandedDepth = 1, searchQuery = \"\" }: JsonTreeProps) {\n  return (\n    <div className=\"rounded-lg border border-stove-border bg-stove-base p-3 font-mono text-xs\">\n      <JsonTreeNode\n        value={value}\n        depth={0}\n        label=\"state\"\n        defaultExpandedDepth={defaultExpandedDepth}\n        searchQuery={searchQuery}\n      />\n    </div>\n  );\n}\n\ninterface JsonTreeNodeProps {\n  value: unknown;\n  depth: number;\n  label: string;\n  defaultExpandedDepth: number;\n  searchQuery: string;\n}\n\nfunction JsonTreeNode({\n  value,\n  depth,\n  label,\n  defaultExpandedDepth,\n  searchQuery,\n}: JsonTreeNodeProps) {\n  const expandable = isExpandable(value);\n  const [expanded, setExpanded] = useState(depth < defaultExpandedDepth);\n  const hasActiveSearch = searchQuery.trim().length > 0;\n  const effectiveExpanded = hasActiveSearch || expanded;\n\n  if (!expandable) {\n    return (\n      <div className=\"leading-6\" style={{ marginLeft: depth * 14 }}>\n        <span className=\"text-[var(--stove-text-secondary)]\">\n          <HighlightedText text={label} query={searchQuery} />\n          {\": \"}\n        </span>\n        <JsonPrimitive value={value} searchQuery={searchQuery} />\n      </div>\n    );\n  }\n\n  const children = getChildren(value);\n  const opening = Array.isArray(value) ? \"[\" : \"{\";\n  const closing = Array.isArray(value) ? \"]\" : \"}\";\n  const summary = Array.isArray(value)\n    ? `${children.length} item${children.length === 1 ? \"\" : \"s\"}`\n    : `${children.length} key${children.length === 1 ? \"\" : \"s\"}`;\n\n  return (\n    <div style={{ marginLeft: depth * 14 }}>\n      <button\n        type=\"button\"\n        className=\"flex w-full items-center gap-2 rounded px-1 py-1 text-left leading-6 text-[var(--stove-text)] hover:bg-[var(--stove-hover)]\"\n        onClick={() => setExpanded(!expanded)}\n      >\n        <span className=\"w-3 text-[var(--stove-text-secondary)]\">\n          {effectiveExpanded ? \"v\" : \">\"}\n        </span>\n        <span className=\"text-[var(--stove-text-secondary)]\">\n          <HighlightedText text={label} query={searchQuery} />\n          {\":\"}\n        </span>\n        <span className=\"text-[var(--stove-text)]\">{opening}</span>\n        <span className=\"text-[var(--stove-text-muted)]\">{summary}</span>\n        {!effectiveExpanded && (\n          <span className=\"text-[var(--stove-text)]\">\n            {renderCollapsedPreview(children, Array.isArray(value))}\n          </span>\n        )}\n        <span className=\"text-[var(--stove-text)]\">{!effectiveExpanded ? closing : \"\"}</span>\n      </button>\n\n      {effectiveExpanded && (\n        <>\n          {children.length === 0 ? (\n            <div\n              className=\"leading-6 text-[var(--stove-text-muted)]\"\n              style={{ marginLeft: (depth + 1) * 14 }}\n            >\n              empty\n            </div>\n          ) : (\n            children.map(([childLabel, childValue]) => (\n              <JsonTreeNode\n                key={`${label}-${childLabel}`}\n                value={childValue}\n                depth={depth + 1}\n                label={childLabel}\n                defaultExpandedDepth={defaultExpandedDepth}\n                searchQuery={searchQuery}\n              />\n            ))\n          )}\n          <div\n            className=\"leading-6 text-[var(--stove-text)]\"\n            style={{ marginLeft: depth * 14 + 20 }}\n          >\n            {closing}\n          </div>\n        </>\n      )}\n    </div>\n  );\n}\n\nfunction JsonPrimitive({ value, searchQuery }: { value: unknown; searchQuery: string }) {\n  if (typeof value === \"string\") {\n    return (\n      <span className=\"break-words text-[var(--stove-green)]\">\n        <HighlightedText text={JSON.stringify(value)} query={searchQuery} />\n      </span>\n    );\n  }\n\n  if (typeof value === \"number\") {\n    return (\n      <span className=\"text-[var(--stove-amber)]\">\n        <HighlightedText text={String(value)} query={searchQuery} />\n      </span>\n    );\n  }\n\n  if (typeof value === \"boolean\") {\n    return (\n      <span className=\"text-[var(--stove-blue)]\">\n        <HighlightedText text={String(value)} query={searchQuery} />\n      </span>\n    );\n  }\n\n  if (value === null) {\n    return (\n      <span className=\"italic text-[var(--stove-text-muted)]\">\n        <HighlightedText text=\"null\" query={searchQuery} />\n      </span>\n    );\n  }\n\n  return (\n    <span className=\"text-[var(--stove-text)]\">\n      <HighlightedText text={String(value)} query={searchQuery} />\n    </span>\n  );\n}\n\nfunction isExpandable(value: unknown): value is Record<string, unknown> | unknown[] {\n  return Array.isArray(value) || (typeof value === \"object\" && value !== null);\n}\n\nfunction getChildren(value: Record<string, unknown> | unknown[]): Array<[string, unknown]> {\n  if (Array.isArray(value)) {\n    return value.map((item, index) => [`[${index}]`, item]);\n  }\n\n  return Object.entries(value);\n}\n\nfunction renderCollapsedPreview(children: Array<[string, unknown]>, isArray: boolean): string {\n  if (children.length === 0) {\n    return \"\";\n  }\n\n  const preview = children\n    .slice(0, 3)\n    .map(([label]) => label)\n    .join(\", \");\n  const suffix = children.length > 3 ? \", ...\" : \"\";\n  return isArray ? `${preview}${suffix}` : `${preview}${suffix}`;\n}\n\nfunction HighlightedText({ text, query }: { text: string; query: string }) {\n  const normalizedQuery = query.trim();\n  if (!normalizedQuery) {\n    return text;\n  }\n\n  const parts = splitByQuery(text, normalizedQuery);\n  return parts.map((part) =>\n    part.match ? (\n      <mark\n        key={`${part.start}-${part.match}`}\n        className=\"rounded bg-[rgba(250,204,21,0.18)] px-0.5 text-inherit\"\n      >\n        {part.text}\n      </mark>\n    ) : (\n      <span key={`${part.start}-${part.match}`}>{part.text}</span>\n    ),\n  );\n}\n\nfunction splitByQuery(\n  text: string,\n  query: string,\n): Array<{ text: string; match: boolean; start: number }> {\n  const escapedQuery = escapeRegExp(query);\n  const regex = new RegExp(`(${escapedQuery})`, \"gi\");\n  let offset = 0;\n\n  return text\n    .split(regex)\n    .filter((part) => part.length > 0)\n    .map((part) => {\n      const segment = {\n        text: part,\n        match: part.toLowerCase() === query.toLowerCase(),\n        start: offset,\n      };\n      offset += part.length;\n      return segment;\n    });\n}\n\nfunction escapeRegExp(value: string): string {\n  return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/NodePopup.tsx",
    "content": "import { useEffect } from \"react\";\nimport type { Entry } from \"../api/types\";\nimport { EntryDetails } from \"./EntryDetails\";\nimport { ResultIcon } from \"./ResultIcon\";\n\ninterface NodePopupProps {\n  entries: Entry[];\n  traceId: string | null;\n  onClose: () => void;\n  onOpenTrace?: (() => void) | undefined;\n}\n\nexport function NodePopup({ entries, traceId, onClose, onOpenTrace }: NodePopupProps) {\n  useEffect(() => {\n    function onKey(e: KeyboardEvent) {\n      if (e.key === \"Escape\") onClose();\n    }\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [onClose]);\n\n  return (\n    <div\n      className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/50\"\n      onClick={(e) => {\n        if (e.target === e.currentTarget) onClose();\n      }}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\") onClose();\n      }}\n      role=\"dialog\"\n    >\n      <div className=\"bg-stove-surface border border-stove-border rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] overflow-y-auto m-4\">\n        <div className=\"flex items-center justify-between px-4 py-3 border-b border-stove-border\">\n          <span className=\"text-sm font-medium text-[var(--stove-text-heading)]\">\n            Entry Details\n          </span>\n          <button\n            type=\"button\"\n            className=\"text-[var(--stove-text-secondary)] hover:text-[var(--stove-text)] text-lg cursor-pointer bg-transparent border-0\"\n            onClick={onClose}\n          >\n            {\"\\u2715\"}\n          </button>\n        </div>\n\n        <div className=\"p-4 space-y-4\">\n          {entries.length > 0 && (\n            <div className=\"space-y-3\">\n              {entries.map((entry) => (\n                <div key={entry.id} className=\"text-xs font-mono space-y-2\">\n                  <div className=\"flex items-center gap-2 text-sm\">\n                    <ResultIcon result={entry.result} />\n                    <span className=\"text-[var(--stove-text)]\">{entry.action}</span>\n                  </div>\n                  <EntryDetails entry={entry} />\n                  {entries.length > 1 && <hr className=\"border-stove-border\" />}\n                </div>\n              ))}\n            </div>\n          )}\n\n          {traceId && (\n            <div className=\"border-t border-stove-border pt-3\">\n              <div className=\"mb-2 text-xs text-[var(--stove-text-secondary)]\">Trace Context</div>\n              <div className=\"rounded-lg border border-stove-border bg-stove-base px-3 py-2\">\n                <div className=\"text-[10px] uppercase tracking-[0.16em] text-[var(--stove-text-muted)]\">\n                  Trace Id\n                </div>\n                <div className=\"mt-1 break-all font-mono text-xs text-[var(--stove-text)]\">\n                  {traceId}\n                </div>\n                <div className=\"mt-2 text-xs text-[var(--stove-text-secondary)]\">\n                  Full trace inspection lives in the Trace tab.\n                </div>\n                {onOpenTrace && (\n                  <button\n                    type=\"button\"\n                    className=\"mt-3 cursor-pointer rounded border border-stove-border bg-stove-card px-2.5 py-1.5 text-xs text-[var(--stove-text)] hover:bg-[var(--stove-hover)]\"\n                    onClick={onOpenTrace}\n                  >\n                    Open Trace Tab\n                  </button>\n                )}\n              </div>\n            </div>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/ResultIcon.tsx",
    "content": "import { getResultTone } from \"../utils/result\";\n\nexport function ResultIcon({ result }: { result: string }) {\n  const tone = getResultTone(result);\n  const color =\n    tone === \"failed\"\n      ? \"var(--stove-red)\"\n      : tone === \"success\"\n        ? \"var(--stove-green)\"\n        : \"var(--stove-text-secondary)\";\n  const icon = tone === \"failed\" ? \"\\u2717\" : tone === \"success\" ? \"\\u2713\" : \"\\u2022\";\n\n  return <span style={{ color }}>{icon}</span>;\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/SnapshotCards.tsx",
    "content": "import { useState } from \"react\";\nimport type { Snapshot } from \"../api/types\";\nimport { describeJsonValue, getJsonPreviewKeys, parseJsonDeep } from \"../utils/json\";\nimport { getKafkaSnapshotMetrics } from \"../utils/snapshot-state\";\nimport { getSystemInfo } from \"../utils/systems\";\nimport { SnapshotMetricTiles } from \"./SnapshotMetricTiles\";\nimport { SnapshotStateDialog } from \"./SnapshotStateDialog\";\n\ninterface SnapshotCardsProps {\n  snapshots: Snapshot[];\n  hiddenCount?: number;\n}\n\nexport function SnapshotCards({ snapshots, hiddenCount = 0 }: SnapshotCardsProps) {\n  const [selectedSnapshot, setSelectedSnapshot] = useState<Snapshot | null>(null);\n\n  if (snapshots.length === 0) {\n    return (\n      <div className=\"p-4\">\n        <div className=\"text-sm text-[var(--stove-text-secondary)]\">\n          {hiddenCount > 0 ? \"No detailed snapshots captured\" : \"No snapshots captured\"}\n        </div>\n        <HiddenSnapshotNotice hiddenCount={hiddenCount} className=\"mt-1\" />\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"p-3\">\n      <HiddenSnapshotNotice hiddenCount={hiddenCount} className=\"mb-3\" boxed />\n      <div\n        className=\"grid gap-3\"\n        style={{ gridTemplateColumns: \"repeat(auto-fit, minmax(200px, 1fr))\" }}\n      >\n        {snapshots.map((snap) => {\n          return (\n            <DetailedSnapshotCard\n              key={snap.id}\n              snapshot={snap}\n              onOpen={() => setSelectedSnapshot(snap)}\n            />\n          );\n        })}\n      </div>\n      {selectedSnapshot && (\n        <SnapshotStateDialog\n          snapshot={selectedSnapshot}\n          onClose={() => setSelectedSnapshot(null)}\n        />\n      )}\n    </div>\n  );\n}\n\nfunction DetailedSnapshotCard({ snapshot, onOpen }: { snapshot: Snapshot; onOpen: () => void }) {\n  const info = getSystemInfo(snapshot.system);\n  const parsedState = parseJsonDeep(snapshot.state_json);\n  const previewKeys = getJsonPreviewKeys(parsedState);\n  const kafkaMetrics =\n    snapshot.system === \"Kafka\" ? getKafkaSnapshotMetrics(snapshot, parsedState) : [];\n\n  return (\n    <div\n      className=\"flex flex-col gap-3 rounded-xl border border-stove-border bg-stove-surface p-3\"\n      style={{\n        borderTopColor: info.color,\n        borderTopWidth: 3,\n      }}\n    >\n      <div className=\"flex items-center gap-2 text-sm font-medium\">\n        <span style={{ color: info.color }}>{info.icon}</span>\n        <span>{snapshot.system}</span>\n      </div>\n      <pre className=\"text-xs text-[var(--stove-text-secondary)] whitespace-pre-wrap\">\n        {snapshot.summary}\n      </pre>\n      {kafkaMetrics.length > 0 && <SnapshotMetricTiles metrics={kafkaMetrics} compact />}\n      <div className=\"rounded-lg border border-stove-border bg-stove-base p-3\">\n        <div className=\"flex items-center justify-between gap-3\">\n          <span className=\"text-[10px] font-medium uppercase tracking-[0.16em] text-[var(--stove-text-muted)]\">\n            State\n          </span>\n          <span className=\"text-[10px] uppercase tracking-[0.16em] text-[var(--stove-text-secondary)]\">\n            {parsedState ? describeJsonValue(parsedState) : \"raw text\"}\n          </span>\n        </div>\n        {previewKeys.length > 0 ? (\n          <div className=\"mt-2 flex flex-wrap gap-1.5\">\n            {previewKeys.map((key) => (\n              <span\n                key={key}\n                className=\"rounded-full border border-stove-border px-2 py-1 text-[10px] font-mono text-[var(--stove-text-secondary)]\"\n              >\n                {key}\n              </span>\n            ))}\n          </div>\n        ) : (\n          <div className=\"mt-2 text-xs text-[var(--stove-text-secondary)]\">\n            Open the state explorer to inspect the captured payload.\n          </div>\n        )}\n        <button\n          type=\"button\"\n          className=\"mt-3 w-full cursor-pointer rounded-md border border-stove-border bg-stove-card px-3 py-2 text-left text-xs font-medium text-[var(--stove-text)] hover:bg-[var(--stove-hover)]\"\n          onClick={onOpen}\n        >\n          Open state\n        </button>\n      </div>\n    </div>\n  );\n}\n\nfunction HiddenSnapshotNotice({\n  hiddenCount,\n  className = \"\",\n  boxed = false,\n}: {\n  hiddenCount: number;\n  className?: string;\n  boxed?: boolean;\n}) {\n  if (hiddenCount === 0) {\n    return null;\n  }\n\n  return (\n    <div\n      className={\n        boxed\n          ? `${className} rounded-lg border border-dashed border-stove-border bg-stove-base px-3 py-2 text-xs text-[var(--stove-text-secondary)]`\n          : `${className} text-xs text-[var(--stove-text-muted)]`\n      }\n    >\n      {hiddenCount} system{hiddenCount === 1 ? \"\" : \"s\"} had no detailed state.\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/SnapshotMetricTiles.tsx",
    "content": "import type { SnapshotMetric } from \"../utils/snapshot-state\";\n\ninterface SnapshotMetricTilesProps {\n  metrics: SnapshotMetric[];\n  compact?: boolean;\n}\n\nexport function SnapshotMetricTiles({ metrics, compact = false }: SnapshotMetricTilesProps) {\n  if (metrics.length === 0) {\n    return null;\n  }\n\n  return (\n    <div className={compact ? \"grid grid-cols-2 gap-2\" : \"grid gap-2 sm:grid-cols-4\"}>\n      {metrics.map((metric) => (\n        <SnapshotMetricTile key={metric.key} metric={metric} compact={compact} />\n      ))}\n    </div>\n  );\n}\n\nfunction SnapshotMetricTile({ metric, compact }: { metric: SnapshotMetric; compact: boolean }) {\n  const style = toneStyle(metric.tone);\n\n  return (\n    <div\n      className={\n        compact\n          ? \"rounded-lg border bg-stove-base px-2.5 py-2\"\n          : \"rounded-lg border bg-stove-base px-3 py-2.5\"\n      }\n      style={{ borderColor: style.border }}\n    >\n      <div\n        className=\"text-[10px] font-medium uppercase tracking-[0.16em]\"\n        style={{ color: style.label }}\n      >\n        {metric.label}\n      </div>\n      <div\n        className={\n          compact\n            ? \"mt-1 font-mono text-base font-semibold\"\n            : \"mt-1 font-mono text-lg font-semibold\"\n        }\n        style={{ color: style.value }}\n      >\n        {metric.value}\n      </div>\n    </div>\n  );\n}\n\nfunction toneStyle(tone: SnapshotMetric[\"tone\"]): {\n  border: string;\n  label: string;\n  value: string;\n} {\n  switch (tone) {\n    case \"info\":\n      return {\n        border: \"var(--stove-blue)\",\n        label: \"var(--stove-blue)\",\n        value: \"var(--stove-blue)\",\n      };\n    case \"success\":\n      return {\n        border: \"var(--stove-green)\",\n        label: \"var(--stove-green)\",\n        value: \"var(--stove-green)\",\n      };\n    case \"danger\":\n      return {\n        border: \"var(--stove-red)\",\n        label: \"var(--stove-red)\",\n        value: \"var(--stove-red)\",\n      };\n    case \"warning\":\n      return {\n        border: \"var(--stove-amber)\",\n        label: \"var(--stove-amber)\",\n        value: \"var(--stove-amber)\",\n      };\n    default:\n      return {\n        border: \"var(--stove-border)\",\n        label: \"var(--stove-text-secondary)\",\n        value: \"var(--stove-text)\",\n      };\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/SnapshotStateDialog.tsx",
    "content": "import { useEffect, useMemo, useState } from \"react\";\nimport type { Snapshot } from \"../api/types\";\nimport {\n  describeJsonValue,\n  filterJsonByQuery,\n  parseJsonDeep,\n  tryFormatJsonDeep,\n} from \"../utils/json\";\nimport { getKafkaSnapshotMetrics, hasDetailedSnapshotState } from \"../utils/snapshot-state\";\nimport { getSystemInfo } from \"../utils/systems\";\nimport { JsonTree } from \"./JsonTree\";\nimport { SnapshotMetricTiles } from \"./SnapshotMetricTiles\";\n\ninterface SnapshotStateDialogProps {\n  snapshot: Snapshot;\n  onClose: () => void;\n}\n\nexport function SnapshotStateDialog({ snapshot, onClose }: SnapshotStateDialogProps) {\n  const info = getSystemInfo(snapshot.system);\n  const parsedState = parseJsonDeep(snapshot.state_json);\n  const hasDetailedState = hasDetailedSnapshotState(snapshot, parsedState);\n  const kafkaMetrics =\n    snapshot.system === \"Kafka\" ? getKafkaSnapshotMetrics(snapshot, parsedState) : [];\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const normalizedSearchQuery = searchQuery.trim();\n  const searchResult = useMemo(() => {\n    if (parsedState === null) {\n      return { filteredValue: null, matchCount: 0 };\n    }\n    return filterJsonByQuery(parsedState, normalizedSearchQuery);\n  }, [normalizedSearchQuery, parsedState]);\n\n  useEffect(() => {\n    function onKey(e: KeyboardEvent) {\n      if (e.key === \"Escape\") onClose();\n    }\n\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [onClose]);\n\n  return (\n    <div\n      className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/55\"\n      onClick={(e) => {\n        if (e.target === e.currentTarget) onClose();\n      }}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\") onClose();\n      }}\n      role=\"dialog\"\n    >\n      <div className=\"m-4 flex max-h-[85vh] w-full max-w-4xl flex-col overflow-hidden rounded-xl border border-stove-border bg-stove-surface shadow-xl\">\n        <div className=\"flex items-start justify-between gap-4 border-b border-stove-border px-4 py-3\">\n          <div className=\"min-w-0\">\n            <div className=\"flex items-center gap-2 text-sm font-medium text-[var(--stove-text-heading)]\">\n              <span style={{ color: info.color }}>{info.icon}</span>\n              <span>{snapshot.system} State</span>\n            </div>\n            <div className=\"mt-1 text-xs text-[var(--stove-text-secondary)]\">\n              {snapshot.summary}\n            </div>\n            <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n              <span className=\"text-[10px] uppercase tracking-[0.16em] text-[var(--stove-text-muted)]\">\n                {hasDetailedState\n                  ? parsedState\n                    ? describeJsonValue(parsedState)\n                    : \"raw text\"\n                  : \"no details\"}\n              </span>\n              <span\n                className=\"rounded-full border px-2 py-1 text-[10px] font-medium uppercase tracking-[0.14em]\"\n                style={\n                  hasDetailedState\n                    ? {\n                        borderColor: info.color,\n                        color: info.color,\n                      }\n                    : {\n                        borderColor: \"var(--stove-border)\",\n                        color: \"var(--stove-text-secondary)\",\n                      }\n                }\n              >\n                {hasDetailedState ? \"Detailed state\" : \"Summary only\"}\n              </span>\n            </div>\n          </div>\n          <button\n            type=\"button\"\n            className=\"cursor-pointer border-0 bg-transparent text-lg text-[var(--stove-text-secondary)] hover:text-[var(--stove-text)]\"\n            onClick={onClose}\n          >\n            {\"\\u2715\"}\n          </button>\n        </div>\n\n        <div className=\"flex-1 space-y-3 overflow-y-auto p-4\">\n          {kafkaMetrics.length > 0 && <SnapshotMetricTiles metrics={kafkaMetrics} />}\n\n          {hasDetailedState && parsedState ? (\n            <>\n              <div className=\"rounded-lg border border-stove-border bg-stove-base p-3\">\n                <div className=\"flex flex-wrap items-center gap-2\">\n                  <input\n                    type=\"search\"\n                    value={searchQuery}\n                    onChange={(e) => setSearchQuery(e.target.value)}\n                    placeholder=\"Filter by any key or value\"\n                    className=\"min-w-0 flex-1 rounded-md border border-stove-border bg-stove-surface px-3 py-2 text-sm text-[var(--stove-text)] outline-none placeholder:text-[var(--stove-text-muted)] focus:border-[var(--stove-blue)]\"\n                  />\n                  {normalizedSearchQuery && (\n                    <button\n                      type=\"button\"\n                      className=\"cursor-pointer rounded-md border border-stove-border bg-stove-surface px-3 py-2 text-xs font-medium text-[var(--stove-text-secondary)] hover:text-[var(--stove-text)]\"\n                      onClick={() => setSearchQuery(\"\")}\n                    >\n                      Clear\n                    </button>\n                  )}\n                </div>\n                <div className=\"mt-2 text-[11px] text-[var(--stove-text-secondary)]\">\n                  {normalizedSearchQuery\n                    ? `${searchResult.matchCount} match${searchResult.matchCount === 1 ? \"\" : \"es\"}`\n                    : \"Type to narrow the state by any property name or value\"}\n                </div>\n              </div>\n\n              {searchResult.filteredValue !== null ? (\n                <JsonTree\n                  value={searchResult.filteredValue}\n                  defaultExpandedDepth={2}\n                  searchQuery={normalizedSearchQuery}\n                />\n              ) : (\n                <div className=\"rounded-lg border border-dashed border-stove-border bg-stove-base p-4 text-sm text-[var(--stove-text-secondary)]\">\n                  No matches in this state payload.\n                </div>\n              )}\n            </>\n          ) : hasDetailedState ? (\n            <pre className=\"overflow-x-auto rounded-lg border border-stove-border bg-stove-base p-3 text-xs whitespace-pre-wrap break-words text-[var(--stove-text)]\">\n              {tryFormatJsonDeep(snapshot.state_json)}\n            </pre>\n          ) : (\n            <div className=\"rounded-lg border border-dashed border-stove-border bg-stove-base p-4 text-sm text-[var(--stove-text-secondary)]\">\n              This snapshot only recorded the summary. There is no detailed state payload to\n              inspect.\n            </div>\n          )}\n\n          {hasDetailedState && (\n            <details className=\"rounded-lg border border-stove-border bg-stove-base\">\n              <summary className=\"cursor-pointer select-none px-3 py-2 text-xs font-medium text-[var(--stove-text-secondary)]\">\n                Raw JSON\n              </summary>\n              <pre className=\"max-h-72 overflow-auto border-t border-stove-border p-3 text-xs whitespace-pre-wrap break-words text-[var(--stove-text)]\">\n                {tryFormatJsonDeep(snapshot.state_json)}\n              </pre>\n            </details>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/SpanTree.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport type { Span } from \"../api/types\";\nimport { formatNanosDuration } from \"../utils/format\";\nimport { parseAttrs } from \"../utils/json\";\nimport { getResultTone, isFailed } from \"../utils/result\";\n\ninterface SpanTreeProps {\n  spans: Span[];\n}\n\ninterface SpanNode {\n  span: Span;\n  children: SpanNode[];\n}\n\nexport function SpanTree({ spans }: SpanTreeProps) {\n  const tree = useMemo(() => buildTree(spans), [spans]);\n  const totalFailed = spans.filter((s) => isFailed(s.status)).length;\n  const totalNeutral = spans.filter((s) => getResultTone(s.status) === \"neutral\").length;\n\n  if (spans.length === 0) {\n    return <div className=\"text-[var(--stove-text-secondary)] text-sm p-4\">No spans recorded</div>;\n  }\n\n  return (\n    <div className=\"space-y-1 p-2\">\n      {tree.map((node) => (\n        <SpanNodeView key={node.span.span_id} node={node} depth={0} />\n      ))}\n      <div className=\"mt-3 pt-2 border-t border-stove-border text-xs text-[var(--stove-text-secondary)] flex gap-4\">\n        <span>{spans.length} spans</span>\n        {totalFailed > 0 && <span className=\"text-[var(--stove-red)]\">{totalFailed} failed</span>}\n        {totalNeutral > 0 && <span>{totalNeutral} unset</span>}\n        {tree[0] && <span>root: {tree[0].span.operation_name}</span>}\n      </div>\n    </div>\n  );\n}\n\nfunction SpanNodeView({ node, depth }: { node: SpanNode; depth: number }) {\n  const [collapsed, setCollapsed] = useState(false);\n  const s = node.span;\n  const tone = getResultTone(s.status);\n  const isError = tone === \"failed\";\n  const duration = formatNanosDuration(s.start_time_nanos, s.end_time_nanos);\n  const attrs = parseAttrs(s.attributes);\n  const relevantAttrs = Object.entries(attrs).filter(([k]) =>\n    [\"db.\", \"http.\", \"rpc.\", \"messaging.\"].some((p) => k.startsWith(p)),\n  );\n  const statusColor =\n    tone === \"failed\"\n      ? \"var(--stove-red)\"\n      : tone === \"success\"\n        ? \"var(--stove-green)\"\n        : \"var(--stove-text-secondary)\";\n  const statusIcon = tone === \"failed\" ? \"\\u2717\" : tone === \"success\" ? \"\\u2713\" : \"\\u2022\";\n\n  return (\n    <div style={{ marginLeft: depth * 20 }}>\n      <button\n        type=\"button\"\n        className={`flex items-center gap-2 px-2 py-1 rounded text-sm cursor-pointer hover:bg-[var(--stove-hover)] w-full text-left bg-transparent border-0 ${\n          isError ? \"border-l-2 border-red-500 bg-[rgba(248,113,113,0.04)]\" : \"\"\n        }`}\n        aria-expanded={!collapsed}\n        onClick={() => setCollapsed(!collapsed)}\n      >\n        {node.children.length > 0 && (\n          <span\n            className=\"text-[var(--stove-text-secondary)] text-xs transition-transform\"\n            style={{ transform: collapsed ? \"rotate(-90deg)\" : \"\" }}\n          >\n            {\"\\u25bc\"}\n          </span>\n        )}\n        <span style={{ color: statusColor }}>{statusIcon}</span>\n        <span className=\"text-[var(--stove-text)]\">{s.operation_name}</span>\n        <span className=\"text-[var(--stove-text-secondary)] font-mono text-xs\">[{duration}]</span>\n        <span className=\"text-[var(--stove-text-muted)] text-xs\">{s.service_name}</span>\n        {tone === \"neutral\" && (\n          <span className=\"text-[var(--stove-text-secondary)] text-[10px] font-mono uppercase\">\n            {s.status}\n          </span>\n        )}\n      </button>\n\n      {!collapsed && (\n        <>\n          {isError && s.exception_type && (\n            <div className=\"ml-8 mt-1 text-xs\">\n              <span className=\"text-[var(--stove-amber)]\">{s.exception_type}: </span>\n              <span className=\"text-[var(--stove-red)]\">{s.exception_message}</span>\n              {s.exception_stack_trace && (\n                <pre className=\"mt-1 text-[var(--stove-text-muted)] text-[10px] whitespace-pre-wrap\">\n                  {s.exception_stack_trace}\n                </pre>\n              )}\n            </div>\n          )}\n\n          {relevantAttrs.length > 0 && (\n            <div className=\"ml-8 mt-0.5 text-xs text-[var(--stove-text-muted)] flex flex-wrap gap-2\">\n              {relevantAttrs.map(([k, v]) => (\n                <span key={k}>\n                  {k}=<span className=\"text-[var(--stove-text-secondary)]\">{v}</span>\n                </span>\n              ))}\n            </div>\n          )}\n\n          {node.children.map((child) => (\n            <SpanNodeView key={child.span.span_id} node={child} depth={depth + 1} />\n          ))}\n        </>\n      )}\n    </div>\n  );\n}\n\nfunction buildTree(spans: Span[]): SpanNode[] {\n  const map = new Map<string, SpanNode>();\n  const roots: SpanNode[] = [];\n\n  for (const span of spans) {\n    map.set(span.span_id, { span, children: [] });\n  }\n\n  for (const span of spans) {\n    const node = map.get(span.span_id);\n    if (!node) continue;\n    const parent = span.parent_span_id ? map.get(span.parent_span_id) : undefined;\n    if (parent) {\n      parent.children.push(node);\n    } else {\n      roots.push(node);\n    }\n  }\n\n  return roots;\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/SysBadge.tsx",
    "content": "import { getSystemInfo } from \"../utils/systems\";\n\ninterface SysBadgeProps {\n  system: string;\n}\n\nexport function SysBadge({ system }: SysBadgeProps) {\n  const info = getSystemInfo(system);\n  return (\n    <span\n      className=\"inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-mono\"\n      style={{ color: info.color, backgroundColor: `${info.color}15` }}\n    >\n      {info.icon} {system}\n    </span>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/SystemNode.tsx",
    "content": "import type { NodeProps } from \"@xyflow/react\";\nimport { Handle, Position } from \"@xyflow/react\";\nimport type { SystemNodeData } from \"../utils/flow\";\nimport { formatDuration, formatTimestamp } from \"../utils/format\";\nimport { isFailed } from \"../utils/result\";\nimport { getSystemInfo } from \"../utils/systems\";\nimport { ResultIcon } from \"./ResultIcon\";\nimport { SysBadge } from \"./SysBadge\";\n\nexport function SystemNode({ data }: NodeProps) {\n  const d = data as SystemNodeData;\n  const info = getSystemInfo(d.system);\n  const failed = isFailed(d.result);\n  const isArrange = d.kind === \"arrange\";\n  const sizeClass = d.kind === \"trace\" ? \"w-[240px] min-h-[120px]\" : \"w-[240px] min-h-[128px]\";\n\n  return (\n    <div\n      className={`${sizeClass} rounded-lg border bg-stove-card px-3 py-2`}\n      style={{\n        borderColor: failed ? \"var(--stove-red)\" : \"var(--stove-border)\",\n        borderWidth: failed ? 2 : 1,\n        borderLeftColor: info.color,\n        borderLeftWidth: 3,\n        backgroundColor: isArrange ? \"rgba(148, 163, 184, 0.08)\" : undefined,\n      }}\n    >\n      <Handle type=\"target\" position={Position.Left} className=\"!bg-[var(--stove-border)]\" />\n      <Handle type=\"source\" position={Position.Right} className=\"!bg-[var(--stove-border)]\" />\n\n      <div className=\"flex items-center gap-1.5 mb-1\">\n        <SysBadge system={d.system} />\n        {isArrange && (\n          <span className=\"text-[10px] px-1 py-0.5 rounded bg-stove-base text-[var(--stove-blue)] font-mono\">\n            arrange\n          </span>\n        )}\n        {d.count > 1 && (\n          <span className=\"text-[10px] px-1 py-0.5 rounded bg-[var(--stove-amber-bg)] text-[var(--stove-amber)] font-mono\">\n            {d.count}x\n          </span>\n        )}\n        <span className=\"ml-auto text-xs\">\n          <ResultIcon result={d.result} />\n        </span>\n      </div>\n\n      <div\n        className=\"text-xs text-[var(--stove-text)] break-words line-clamp-2 min-h-[2rem]\"\n        title={d.action}\n      >\n        {d.action}\n      </div>\n\n      {(d.startedAt || d.durationMs) && (\n        <div className=\"mt-1 flex flex-wrap gap-2 text-[10px] text-[var(--stove-text-secondary)]\">\n          {d.startedAt && <span>{formatTimestamp(d.startedAt)}</span>}\n          {d.durationMs != null && d.durationMs > 0 && <span>{formatDuration(d.durationMs)}</span>}\n        </div>\n      )}\n\n      {failed && d.error && (\n        <div className=\"text-[10px] text-[var(--stove-red)] truncate mt-1\" title={d.error}>\n          {d.error}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/components/VersionMismatchBanner.tsx",
    "content": "import {\n  buildVersionMismatchBannerModel,\n  type VersionMismatchSummary,\n} from \"../utils/version-mismatch\";\n\ninterface VersionMismatchBannerProps {\n  summary: VersionMismatchSummary;\n}\n\nexport function VersionMismatchBanner({ summary }: VersionMismatchBannerProps) {\n  const model = buildVersionMismatchBannerModel(summary);\n  const affectedApps = model.affectedApps.join(\", \");\n\n  return (\n    <section className=\"border-b border-amber-500/30 bg-amber-100 text-amber-950 px-4 py-3\">\n      <div className=\"flex items-start gap-3\">\n        <span className=\"text-lg leading-none\" aria-hidden=\"true\">\n          !\n        </span>\n        <div className=\"min-w-0\">\n          <p className=\"text-sm font-semibold\">{model.title}</p>\n          <p className=\"mt-1 text-xs leading-5\">\n            Affected apps: <span className=\"font-medium\">{affectedApps}</span>.\n            {model.switchHint ? ` ${model.switchHint}` : null}\n          </p>\n\n          {model.selectedAppName ? (\n            <div className=\"mt-3 rounded-md border border-amber-500/40 bg-white/60 px-3 py-3 text-xs leading-5\">\n              <p className=\"font-semibold\">\n                Selected app: <span className=\"font-mono\">{model.selectedAppName}</span>\n              </p>\n              <p className=\"mt-1\">\n                Runtime version:{\" \"}\n                <span className=\"font-mono\">{model.runtimeVersion ?? \"not reported\"}</span>\n                {\" · \"}\n                CLI version: <span className=\"font-mono\">{model.cliVersion}</span>\n              </p>\n              <div className=\"mt-2 space-y-1\">\n                {model.remediationSteps.map((step) => (\n                  <p key={step.value}>\n                    {step.kind === \"command\" ? (\n                      <code className=\"font-mono break-all\">{step.value}</code>\n                    ) : (\n                      step.value\n                    )}\n                  </p>\n                ))}\n              </div>\n            </div>\n          ) : null}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/hooks/useAppData.ts",
    "content": "import { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { api } from \"../api/client\";\nimport { applyLiveDashboardEvent, invalidateDashboardQueries } from \"../api/live-cache\";\nimport { useSSE } from \"../api/sse\";\nimport { EVENT_TYPE, type LiveDashboardEvent } from \"../api/types\";\nimport { isRunning } from \"../utils/status\";\nimport { summarizeVersionMismatches } from \"../utils/version-mismatch\";\n\nexport function useAppData() {\n  const queryClient = useQueryClient();\n  const [selectedApp, setSelectedApp] = useState<string | null>(null);\n  const [selectedTestId, setSelectedTestId] = useState<string | null>(null);\n\n  const handleLiveEvent = useCallback(\n    (event: LiveDashboardEvent) => {\n      applyLiveDashboardEvent(queryClient, event);\n      if (event.event_type === EVENT_TYPE.RUN_STARTED) {\n        setSelectedApp(event.payload.app_name);\n        setSelectedTestId(null);\n      }\n    },\n    [queryClient],\n  );\n\n  const { connected: liveConnected } = useSSE({\n    onEvent: handleLiveEvent,\n    onGap: (event) => invalidateDashboardQueries(queryClient, event.run_id),\n    onReconnect: () => invalidateDashboardQueries(queryClient),\n  });\n\n  const { data: apps = [] } = useQuery({\n    queryKey: [\"apps\"],\n    queryFn: api.getApps,\n    refetchInterval: liveConnected ? false : 5000,\n    staleTime: liveConnected ? Number.POSITIVE_INFINITY : 0,\n  });\n\n  const { data: meta } = useQuery({\n    queryKey: [\"meta\"],\n    queryFn: api.getMeta,\n    staleTime: Number.POSITIVE_INFINITY,\n  });\n\n  const activeApp = selectedApp ?? apps[0]?.app_name ?? null;\n  const cliVersion = meta?.stove_cli_version ?? null;\n\n  const { data: runs = [] } = useQuery({\n    queryKey: [\"runs\", activeApp],\n    queryFn: () => api.getRuns(activeApp!),\n    enabled: !!activeApp,\n    refetchInterval: !!activeApp && !liveConnected ? 5000 : false,\n    staleTime: liveConnected ? Number.POSITIVE_INFINITY : 0,\n  });\n\n  const latestRun = runs[0] ?? null;\n\n  const { data: tests = [] } = useQuery({\n    queryKey: [\"tests\", latestRun?.id],\n    queryFn: () => api.getTests(latestRun!.id),\n    enabled: !!latestRun,\n    refetchInterval: latestRun && isRunning(latestRun.status) && !liveConnected ? 5000 : false,\n    staleTime: liveConnected ? Number.POSITIVE_INFINITY : 0,\n  });\n\n  useEffect(() => {\n    if (selectedApp && !apps.some((app) => app.app_name === selectedApp)) {\n      setSelectedApp(null);\n      setSelectedTestId(null);\n    }\n  }, [apps, selectedApp]);\n\n  useEffect(() => {\n    if (selectedTestId && !tests.some((test) => test.id === selectedTestId)) {\n      setSelectedTestId(tests[0]?.id ?? null);\n    }\n  }, [selectedTestId, tests]);\n\n  const selectedTest = tests.find((test) => test.id === selectedTestId) ?? tests[0] ?? null;\n  const versionMismatchSummary = summarizeVersionMismatches(apps, cliVersion, activeApp);\n  const mismatchedApps = versionMismatchSummary?.affectedAppNames ?? [];\n\n  return {\n    apps,\n    activeApp,\n    cliVersion,\n    latestRun,\n    tests,\n    selectedTest,\n    liveConnected,\n    mismatchedApps,\n    versionMismatchSummary,\n    selectApp: (name: string) => {\n      setSelectedApp(name);\n      setSelectedTestId(null);\n    },\n    selectTest: setSelectedTestId,\n  };\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/hooks/useTheme.tsx",
    "content": "import { createContext, type ReactNode, useContext, useEffect, useState } from \"react\";\n\ntype Theme = \"light\" | \"dark\";\n\ninterface ThemeContext {\n  theme: Theme;\n  toggle: () => void;\n}\n\nconst Ctx = createContext<ThemeContext>({ theme: \"dark\", toggle: () => {} });\n\nexport function ThemeProvider({ children }: { children: ReactNode }) {\n  const [theme, setTheme] = useState<Theme>(() => {\n    const stored = localStorage.getItem(\"stove-theme\");\n    return stored === \"light\" || stored === \"dark\" ? stored : \"dark\";\n  });\n\n  useEffect(() => {\n    const root = document.documentElement;\n    root.classList.toggle(\"dark\", theme === \"dark\");\n    localStorage.setItem(\"stove-theme\", theme);\n  }, [theme]);\n\n  const toggle = () => setTheme((t) => (t === \"dark\" ? \"light\" : \"dark\"));\n\n  return <Ctx.Provider value={{ theme, toggle }}>{children}</Ctx.Provider>;\n}\n\nexport function useTheme() {\n  return useContext(Ctx);\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/index.css",
    "content": "@import \"tailwindcss\";\n@import \"@xyflow/react/dist/style.css\";\n\n@theme {\n  --color-stove-base: var(--stove-base);\n  --color-stove-surface: var(--stove-surface);\n  --color-stove-card: var(--stove-card);\n  --color-stove-border: var(--stove-border);\n\n  --font-mono: \"JetBrains Mono\", monospace;\n  --font-sans: \"IBM Plex Sans\", system-ui, sans-serif;\n\n  --animate-fade-in: fade-in 0.2s ease;\n  --animate-pulse-dot: pulse-dot 1.5s ease-in-out infinite;\n}\n\n/* ── Theme variables ─────────────────────────────────────────────── */\n\n:root {\n  --stove-base: #f8f9fa;\n  --stove-surface: #ffffff;\n  --stove-card: #f1f3f5;\n  --stove-border: #dee2e6;\n  --stove-text: #1f2937;\n  --stove-text-secondary: #6b7280;\n  --stove-text-muted: #9ca3af;\n  --stove-text-heading: #111827;\n  --stove-hover: rgba(0, 0, 0, 0.04);\n  --stove-blue: #2563eb;\n  --stove-green: #16a34a;\n  --stove-red: #dc2626;\n  --stove-amber: #d97706;\n  --stove-red-bg: rgba(220, 38, 38, 0.1);\n  --stove-amber-bg: rgba(217, 119, 6, 0.1);\n}\n\n:root.dark {\n  --stove-base: #080b12;\n  --stove-surface: #0a0e17;\n  --stove-card: #0d1117;\n  --stove-border: #1e293b;\n  --stove-text: #d1d5db;\n  --stove-text-secondary: #6b7280;\n  --stove-text-muted: #4b5563;\n  --stove-text-heading: #e5e7eb;\n  --stove-hover: rgba(255, 255, 255, 0.02);\n  --stove-blue: #60a5fa;\n  --stove-green: #4ade80;\n  --stove-red: #f87171;\n  --stove-amber: #fbbf24;\n  --stove-red-bg: rgba(127, 29, 29, 0.3);\n  --stove-amber-bg: rgba(120, 53, 15, 0.2);\n}\n\n/* ── React Flow theme ────────────────────────────────────────────── */\n\n.react-flow {\n  --xy-background-color: var(--stove-base);\n  --xy-node-border-radius: 8px;\n  --xy-edge-stroke: var(--stove-border);\n  --xy-edge-stroke-selected: var(--stove-blue);\n  --xy-controls-button-background-color: var(--stove-card);\n  --xy-controls-button-border-color: var(--stove-border);\n  --xy-controls-button-color: var(--stove-text);\n  --xy-minimap-background-color: var(--stove-surface);\n}\n\n/* ── Animations ──────────────────────────────────────────────────── */\n\n@keyframes fade-in {\n  from {\n    opacity: 0;\n    transform: translateY(4px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes pulse-dot {\n  0%,\n  100% {\n    opacity: 1;\n  }\n  50% {\n    opacity: 0.3;\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/Header.tsx",
    "content": "import { useTheme } from \"../hooks/useTheme\";\n\nexport function Header() {\n  const { theme, toggle } = useTheme();\n\n  return (\n    <header className=\"flex items-center justify-between px-4 py-2 border-b border-stove-border bg-stove-surface\">\n      <div className=\"flex items-center gap-2 text-sm font-medium\">\n        <span className=\"text-orange-400\">&#x1f525;</span>\n        <span className=\"text-[var(--stove-text-heading)]\">Stove</span>\n        <span className=\"text-[var(--stove-text-muted)] text-xs\">v{__STOVE_VERSION__}</span>\n      </div>\n      <div className=\"flex items-center gap-3 text-xs text-[var(--stove-text-secondary)]\">\n        <code\n          className=\"rounded border border-stove-border bg-stove-base px-1.5 py-0.5 font-mono text-[10px] text-[var(--stove-text-secondary)]\"\n          title={`MCP endpoint: ${window.location.origin}/mcp`}\n        >\n          MCP /mcp\n        </code>\n        <a\n          href=\"https://trendyol.github.io/stove/\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"hover:text-[var(--stove-text)] transition-colors flex items-center gap-1\"\n          title=\"Documentation\"\n        >\n          <svg aria-hidden=\"true\" className=\"w-3.5 h-3.5\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n            <path d=\"M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 000 2.5v11a.5.5 0 00.707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 00.78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0016 13.5v-11a.5.5 0 00-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z\" />\n          </svg>\n          Docs\n        </a>\n        <a\n          href=\"https://github.com/Trendyol/stove\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          className=\"hover:text-[var(--stove-text)] transition-colors flex items-center gap-1\"\n          title=\"GitHub\"\n        >\n          <svg aria-hidden=\"true\" className=\"w-3.5 h-3.5\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n            <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\" />\n          </svg>\n          GitHub\n        </a>\n        <button\n          type=\"button\"\n          onClick={toggle}\n          className=\"hover:text-[var(--stove-text)] transition-colors cursor-pointer bg-transparent border-0 text-xs text-[var(--stove-text-secondary)] p-0.5\"\n          title={`Switch to ${theme === \"dark\" ? \"light\" : \"dark\"} mode`}\n        >\n          {theme === \"dark\" ? (\n            <svg aria-hidden=\"true\" className=\"w-3.5 h-3.5\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n              <path d=\"M8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.5.5 0 01.5.5v2a.5.5 0 01-1 0v-2A.5.5 0 018 0zm0 13a.5.5 0 01.5.5v2a.5.5 0 01-1 0v-2A.5.5 0 018 13zm8-5a.5.5 0 01-.5.5h-2a.5.5 0 010-1h2a.5.5 0 01.5.5zM3 8a.5.5 0 01-.5.5h-2a.5.5 0 010-1h2A.5.5 0 013 8zm10.657-5.657a.5.5 0 010 .707l-1.414 1.415a.5.5 0 11-.707-.708l1.414-1.414a.5.5 0 01.707 0zm-9.193 9.193a.5.5 0 010 .707L3.05 13.657a.5.5 0 01-.707-.707l1.414-1.414a.5.5 0 01.707 0zm9.193 2.121a.5.5 0 01-.707 0l-1.414-1.414a.5.5 0 01.707-.707l1.414 1.414a.5.5 0 010 .707zM4.464 4.465a.5.5 0 01-.707 0L2.343 3.05a.5.5 0 11.707-.707l1.414 1.414a.5.5 0 010 .708z\" />\n            </svg>\n          ) : (\n            <svg aria-hidden=\"true\" className=\"w-3.5 h-3.5\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n              <path d=\"M6 .278a.768.768 0 01.08.858 7.208 7.208 0 00-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 01.81.316.733.733 0 01-.031.893A8.349 8.349 0 018.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 016 .278z\" />\n            </svg>\n          )}\n        </button>\n        <span className=\"border-l border-stove-border h-3\" />\n        <span className=\"flex items-center gap-1.5\">\n          <span className=\"w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse-dot\" />\n          Connected\n        </span>\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/Sidebar.tsx",
    "content": "import { useQueryClient } from \"@tanstack/react-query\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { api } from \"../api/client\";\nimport type { AppSummary, Run, Test } from \"../api/types\";\nimport { filterTests } from \"../utils/filters\";\nimport { AppPicker } from \"./sidebar/AppPicker\";\nimport { RunSummary } from \"./sidebar/RunSummary\";\nimport type { FilterValue } from \"./sidebar/TestFilters\";\nimport { TestFilters } from \"./sidebar/TestFilters\";\nimport { TestTree } from \"./sidebar/TestTree\";\n\nconst SIDEBAR_MIN_WIDTH = 240;\nconst SIDEBAR_MAX_WIDTH = 600;\nconst SIDEBAR_DEFAULT_WIDTH = 320;\nconst SIDEBAR_STORAGE_KEY = \"stove-sidebar-width\";\n\nfunction loadSidebarWidth(): number {\n  const stored = localStorage.getItem(SIDEBAR_STORAGE_KEY);\n  if (!stored) return SIDEBAR_DEFAULT_WIDTH;\n  const parsed = Number(stored);\n  return Number.isFinite(parsed)\n    ? Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, parsed))\n    : SIDEBAR_DEFAULT_WIDTH;\n}\n\ninterface SidebarProps {\n  apps: AppSummary[];\n  mismatchedApps: string[];\n  selectedApp: string | null;\n  onSelectApp: (name: string) => void;\n  run: Run | null;\n  tests: Test[];\n  selectedTestId: string | null;\n  onSelectTest: (testId: string) => void;\n}\n\nexport function Sidebar({\n  apps,\n  mismatchedApps,\n  selectedApp,\n  onSelectApp,\n  run,\n  tests,\n  selectedTestId,\n  onSelectTest,\n}: SidebarProps) {\n  const queryClient = useQueryClient();\n  const [filter, setFilter] = useState<FilterValue>(\"all\");\n  const [search, setSearch] = useState(\"\");\n  const [clearing, setClearing] = useState(false);\n  const [width, setWidth] = useState(loadSidebarWidth);\n  const draggingRef = useRef(false);\n\n  const handleClear = async () => {\n    if (!confirm(\"Clear all stored data? This cannot be undone.\")) return;\n    setClearing(true);\n    try {\n      await api.clearAll();\n      queryClient.invalidateQueries();\n    } finally {\n      setClearing(false);\n    }\n  };\n\n  const filteredTests = filterTests(tests, filter, search);\n\n  const handleMouseDown = useCallback((e: React.MouseEvent) => {\n    e.preventDefault();\n    draggingRef.current = true;\n    document.body.style.cursor = \"col-resize\";\n    document.body.style.userSelect = \"none\";\n  }, []);\n\n  useEffect(() => {\n    const handleMouseMove = (e: MouseEvent) => {\n      if (!draggingRef.current) return;\n      const clamped = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, e.clientX));\n      setWidth(clamped);\n    };\n\n    const handleMouseUp = () => {\n      if (!draggingRef.current) return;\n      draggingRef.current = false;\n      document.body.style.cursor = \"\";\n      document.body.style.userSelect = \"\";\n      setWidth((w) => {\n        localStorage.setItem(SIDEBAR_STORAGE_KEY, String(w));\n        return w;\n      });\n    };\n\n    document.addEventListener(\"mousemove\", handleMouseMove);\n    document.addEventListener(\"mouseup\", handleMouseUp);\n    return () => {\n      document.removeEventListener(\"mousemove\", handleMouseMove);\n      document.removeEventListener(\"mouseup\", handleMouseUp);\n    };\n  }, []);\n\n  return (\n    <aside\n      className=\"shrink-0 border-r border-stove-border bg-stove-surface flex flex-col overflow-hidden relative\"\n      style={{ width }}\n    >\n      <AppPicker\n        apps={apps}\n        mismatchedApps={mismatchedApps}\n        selectedApp={selectedApp}\n        onSelectApp={onSelectApp}\n      />\n      {run && <RunSummary run={run} tests={tests} />}\n      <TestFilters\n        filter={filter}\n        onFilterChange={setFilter}\n        search={search}\n        onSearchChange={setSearch}\n      />\n      <div className=\"flex-1 overflow-y-auto\">\n        <TestTree\n          tests={filteredTests}\n          selectedTestId={selectedTestId}\n          onSelectTest={onSelectTest}\n        />\n      </div>\n      <div className=\"border-t border-stove-border px-3 py-2\">\n        <button\n          type=\"button\"\n          onClick={handleClear}\n          disabled={clearing}\n          className=\"w-full flex items-center justify-center gap-1.5 px-2 py-1.5 text-xs text-[var(--stove-text-secondary)] hover:text-red-400 hover:bg-red-400/10 rounded transition-colors cursor-pointer disabled:opacity-50 bg-transparent border border-stove-border\"\n        >\n          <svg aria-hidden=\"true\" className=\"w-3 h-3\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n            <path d=\"M5.5 5.5A.5.5 0 016 6v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm2.5 0a.5.5 0 01.5.5v6a.5.5 0 01-1 0V6a.5.5 0 01.5-.5zm3 .5a.5.5 0 00-1 0v6a.5.5 0 001 0V6z\" />\n            <path\n              fillRule=\"evenodd\"\n              d=\"M14.5 3a1 1 0 01-1 1H13v9a2 2 0 01-2 2H5a2 2 0 01-2-2V4h-.5a1 1 0 010-2H6a1 1 0 011-1h2a1 1 0 011 1h3.5a1 1 0 011 1zM4.118 4L4 4.059V13a1 1 0 001 1h6a1 1 0 001-1V4.059L11.882 4H4.118zM7 1.5a.5.5 0 00-.5.5h3a.5.5 0 00-.5-.5H7z\"\n            />\n          </svg>\n          {clearing ? \"Clearing...\" : \"Clear data\"}\n        </button>\n      </div>\n      {/* biome-ignore lint/a11y/noStaticElementInteractions: resize drag handle */}\n      <div\n        className=\"absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-blue-500/40 transition-colors\"\n        onMouseDown={handleMouseDown}\n      />\n    </aside>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/TestDetail.tsx",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { useMemo, useState } from \"react\";\nimport { api } from \"../api/client\";\nimport type { Test } from \"../api/types\";\nimport { EntryRow } from \"../components/EntryRow\";\nimport { FlowTab } from \"../components/FlowTab\";\nimport { SnapshotCards } from \"../components/SnapshotCards\";\nimport { SpanTree } from \"../components/SpanTree\";\nimport { partitionSnapshotsByDetail } from \"../utils/snapshot-state\";\nimport { isRunning } from \"../utils/status\";\nimport type { Tab } from \"./detail/TabBar\";\nimport { TabBar } from \"./detail/TabBar\";\nimport { TestHeader } from \"./detail/TestHeader\";\n\ninterface TestDetailProps {\n  runId: string;\n  test: Test;\n  liveConnected: boolean;\n}\n\nexport function TestDetail({ runId, test, liveConnected }: TestDetailProps) {\n  const [tab, setTab] = useState<Tab>(\"timeline\");\n  const liveRefetchInterval = isRunning(test.status) && !liveConnected ? 5000 : false;\n\n  const { data: entries = [], error: entriesError } = useQuery({\n    queryKey: [\"entries\", runId, test.id],\n    queryFn: () => api.getEntries(runId, test.id),\n    refetchInterval: liveRefetchInterval,\n    staleTime: liveConnected ? Number.POSITIVE_INFINITY : 0,\n  });\n\n  const {\n    data: spans = [],\n    isLoading: spansLoading,\n    error: spansError,\n  } = useQuery({\n    queryKey: [\"spans\", runId, test.id],\n    queryFn: () => api.getSpans(runId, test.id),\n    refetchInterval: liveRefetchInterval,\n    staleTime: liveConnected ? Number.POSITIVE_INFINITY : 0,\n  });\n\n  const {\n    data: snapshots = [],\n    isLoading: snapshotsLoading,\n    error: snapshotsError,\n  } = useQuery({\n    queryKey: [\"snapshots\", runId, test.id],\n    queryFn: () => api.getSnapshots(runId, test.id),\n    refetchInterval: liveRefetchInterval,\n    staleTime: liveConnected ? Number.POSITIVE_INFINITY : 0,\n  });\n\n  const { detailedSnapshots, hiddenCount: hiddenSnapshotCount } = useMemo(\n    () => partitionSnapshotsByDetail(snapshots),\n    [snapshots],\n  );\n\n  const tabs = [\n    { id: \"timeline\" as Tab, label: `Timeline (${entries.length})`, icon: \"\\u{1f4cb}\" },\n    { id: \"trace\" as Tab, label: `Trace (${spans.length})`, icon: \"\\u{1f50d}\" },\n    {\n      id: \"snapshots\" as Tab,\n      label: `Snapshots (${detailedSnapshots.length})`,\n      icon: \"\\u{1f4f8}\",\n    },\n    { id: \"flow\" as Tab, label: \"Flow\", icon: \"\\u{1f310}\" },\n  ];\n\n  return (\n    <div className=\"flex-1 flex flex-col overflow-hidden\">\n      <div className=\"px-4 py-3 border-b border-stove-border bg-stove-surface sticky top-0 z-10\">\n        <TestHeader test={test} />\n        {test.error && (\n          <div className=\"mt-2 px-3 py-2 bg-red-900/20 border border-red-900/30 rounded text-xs text-red-400 font-mono\">\n            {test.error}\n          </div>\n        )}\n        <TabBar tabs={tabs} active={tab} onSelect={setTab} />\n      </div>\n\n      <div className={`min-h-0 flex-1 ${tab === \"flow\" ? \"overflow-hidden\" : \"overflow-y-auto\"}`}>\n        {tab === \"timeline\" && (\n          <div className=\"p-3 space-y-0.5\">\n            {entriesError && (\n              <QueryErrorMessage error={entriesError} fallback=\"Failed to load entries\" />\n            )}\n            {entries.map((entry) => (\n              <EntryRow key={entry.id} entry={entry} />\n            ))}\n            {entries.length === 0 && (\n              <div className=\"text-[var(--stove-text-secondary)] text-sm p-4\">\n                No entries recorded\n              </div>\n            )}\n          </div>\n        )}\n        {tab === \"trace\" &&\n          (spansLoading ? (\n            <div className=\"text-[var(--stove-text-secondary)] text-sm p-4\">Loading traces...</div>\n          ) : spansError ? (\n            <QueryErrorMessage error={spansError} fallback=\"Failed to load traces\" />\n          ) : (\n            <SpanTree spans={spans} />\n          ))}\n        {tab === \"snapshots\" &&\n          (snapshotsLoading ? (\n            <div className=\"text-[var(--stove-text-secondary)] text-sm p-4\">\n              Loading snapshots...\n            </div>\n          ) : snapshotsError ? (\n            <QueryErrorMessage error={snapshotsError} fallback=\"Failed to load snapshots\" />\n          ) : (\n            <SnapshotCards snapshots={detailedSnapshots} hiddenCount={hiddenSnapshotCount} />\n          ))}\n        {tab === \"flow\" && (\n          <FlowTab\n            entries={entries}\n            spans={spans}\n            snapshots={detailedSnapshots}\n            onOpenTraceTab={() => setTab(\"trace\")}\n          />\n        )}\n      </div>\n    </div>\n  );\n}\n\nfunction QueryErrorMessage({ error, fallback }: { error: unknown; fallback: string }) {\n  const message = error instanceof Error ? error.message : fallback;\n  return <div className=\"text-red-400 text-sm p-4\">{message}</div>;\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/detail/TabBar.tsx",
    "content": "export type Tab = \"timeline\" | \"trace\" | \"snapshots\" | \"flow\";\n\ninterface TabDef {\n  id: Tab;\n  label: string;\n  icon: string;\n}\n\ninterface TabBarProps {\n  tabs: TabDef[];\n  active: Tab;\n  onSelect: (tab: Tab) => void;\n}\n\nexport function TabBar({ tabs, active, onSelect }: TabBarProps) {\n  return (\n    <div className=\"flex gap-1 mt-3\" role=\"tablist\">\n      {tabs.map((t) => (\n        <button\n          type=\"button\"\n          role=\"tab\"\n          key={t.id}\n          aria-selected={active === t.id}\n          className={`px-3 py-1.5 text-xs rounded-t ${\n            active === t.id\n              ? \"bg-stove-card text-[var(--stove-text-heading)] border-b-2 border-amber-500\"\n              : \"text-[var(--stove-text-secondary)] hover:text-[var(--stove-text)]\"\n          }`}\n          onClick={() => onSelect(t.id)}\n        >\n          {t.icon} {t.label}\n        </button>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/detail/TestHeader.tsx",
    "content": "import type { Test } from \"../../api/types\";\nimport { Badge } from \"../../components/Badge\";\nimport { formatDuration } from \"../../utils/format\";\n\ninterface TestHeaderProps {\n  test: Test;\n}\n\nexport function TestHeader({ test }: TestHeaderProps) {\n  return (\n    <div className=\"flex items-center gap-3\">\n      <div className=\"flex-1 min-w-0\">\n        <div className=\"text-xs text-[var(--stove-text-secondary)]\">{test.spec_name}</div>\n        <div className=\"text-sm text-[var(--stove-text-heading)] font-medium truncate\">\n          {test.test_name}\n        </div>\n      </div>\n      <Badge status={test.status} />\n      <span className=\"text-xs text-[var(--stove-text-secondary)] font-mono\">\n        {formatDuration(test.duration_ms)}\n      </span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/sidebar/AppPicker.tsx",
    "content": "import type { AppSummary } from \"../../api/types\";\n\ninterface AppPickerProps {\n  apps: AppSummary[];\n  mismatchedApps: string[];\n  selectedApp: string | null;\n  onSelectApp: (name: string) => void;\n}\n\nexport function AppPicker({ apps, mismatchedApps, selectedApp, onSelectApp }: AppPickerProps) {\n  const mismatchedAppSet = new Set(mismatchedApps);\n\n  return (\n    <div className=\"p-3 border-b border-stove-border\">\n      <select\n        className=\"w-full bg-stove-card border border-stove-border rounded px-2 py-1.5 text-sm text-[var(--stove-text)] focus:outline-none focus:border-blue-500\"\n        value={selectedApp ?? \"\"}\n        onChange={(e) => onSelectApp(e.target.value)}\n      >\n        {apps.map((app) => (\n          <option key={app.app_name} value={app.app_name}>\n            {app.app_name}\n            {mismatchedAppSet.has(app.app_name) ? \" [mismatch]\" : \"\"}\n            {` (${app.total_runs} runs)`}\n          </option>\n        ))}\n      </select>\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/sidebar/RunSummary.tsx",
    "content": "import type { Run, Test } from \"../../api/types\";\nimport { Badge } from \"../../components/Badge\";\nimport { formatDuration } from \"../../utils/format\";\nimport { isFailed, isPassed, isRunning } from \"../../utils/status\";\n\ninterface RunSummaryProps {\n  run: Run;\n  tests: Test[];\n}\n\nexport function RunSummary({ run, tests }: RunSummaryProps) {\n  const hasLiveTests = tests.length > 0;\n  const total = hasLiveTests ? tests.length : run.total_tests;\n  const passed = hasLiveTests ? tests.filter((t) => isPassed(t.status)).length : run.passed;\n  const failed = hasLiveTests ? tests.filter((t) => isFailed(t.status)).length : run.failed;\n  const running = hasLiveTests ? tests.filter((t) => isRunning(t.status)).length : 0;\n\n  return (\n    <div className=\"p-3 border-b border-stove-border\">\n      <div className=\"flex items-center justify-between mb-2\">\n        <Badge status={run.status} />\n        <span className=\"text-xs text-[var(--stove-text-secondary)] font-mono\">\n          {formatDuration(run.duration_ms)}\n        </span>\n      </div>\n      <div className=\"flex gap-4 text-center\">\n        <Stat label=\"Total\" value={total} />\n        <Stat label=\"Running\" value={running} color=\"var(--stove-blue)\" />\n        <Stat label=\"Pass\" value={passed} color=\"var(--stove-green)\" />\n        <Stat label=\"Fail\" value={failed} color=\"var(--stove-red)\" />\n      </div>\n    </div>\n  );\n}\n\nfunction Stat({ label, value, color }: { label: string; value: number; color?: string }) {\n  return (\n    <div>\n      <div className=\"text-lg font-mono font-semibold\" style={{ color }}>\n        {value}\n      </div>\n      <div className=\"text-xs text-[var(--stove-text-secondary)]\">{label}</div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/sidebar/TestFilters.tsx",
    "content": "export type FilterValue = \"all\" | \"pass\" | \"fail\";\n\ninterface TestFiltersProps {\n  filter: FilterValue;\n  onFilterChange: (f: FilterValue) => void;\n  search: string;\n  onSearchChange: (s: string) => void;\n}\n\nexport function TestFilters({ filter, onFilterChange, search, onSearchChange }: TestFiltersProps) {\n  return (\n    <div className=\"p-2 border-b border-stove-border flex gap-1\">\n      {([\"all\", \"pass\", \"fail\"] as const).map((f) => (\n        <button\n          type=\"button\"\n          key={f}\n          className={`px-2 py-1 text-xs rounded ${\n            filter === f\n              ? \"bg-stove-card text-[var(--stove-text-heading)]\"\n              : \"text-[var(--stove-text-secondary)] hover:text-[var(--stove-text)]\"\n          }`}\n          onClick={() => onFilterChange(f)}\n        >\n          {f.charAt(0).toUpperCase() + f.slice(1)}\n        </button>\n      ))}\n      <input\n        className=\"ml-auto bg-stove-card border border-stove-border rounded px-2 py-1 text-xs text-[var(--stove-text)] w-24 focus:outline-none focus:border-blue-500\"\n        placeholder=\"Search...\"\n        value={search}\n        onChange={(e) => onSearchChange(e.target.value)}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/sidebar/TestListItem.tsx",
    "content": "import type { Test } from \"../../api/types\";\nimport { Badge } from \"../../components/Badge\";\nimport { formatDuration } from \"../../utils/format\";\n\ninterface TestListItemProps {\n  test: Test;\n  selected: boolean;\n  onSelect: () => void;\n  hideSpec?: boolean;\n}\n\nexport function TestListItem({ test, selected, onSelect, hideSpec }: TestListItemProps) {\n  return (\n    <button\n      type=\"button\"\n      aria-current={selected ? \"true\" : undefined}\n      className={`w-full text-left px-3 py-2 cursor-pointer border-l-2 hover:bg-[var(--stove-hover)] ${\n        selected ? \"border-l-amber-500 bg-[rgba(245,158,11,0.05)]\" : \"border-l-transparent\"\n      }`}\n      onClick={onSelect}\n    >\n      <div className=\"flex items-center justify-between\">\n        {!hideSpec && (\n          <span className=\"text-xs text-[var(--stove-text-secondary)] truncate\">\n            {test.spec_name}\n          </span>\n        )}\n        <Badge status={test.status} />\n      </div>\n      <div className=\"text-sm text-[var(--stove-text)] truncate mt-0.5\">{test.test_name}</div>\n      <div className=\"text-xs text-[var(--stove-text-muted)] mt-0.5 font-mono\">\n        {formatDuration(test.duration_ms)}\n      </div>\n    </button>\n  );\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/layout/sidebar/TestTree.tsx",
    "content": "import { useCallback, useMemo, useState } from \"react\";\nimport type { Test } from \"../../api/types\";\nimport { aggregateStatus, type Status } from \"../../utils/status\";\nimport { TestListItem } from \"./TestListItem\";\n\ninterface TestTreeProps {\n  tests: Test[];\n  selectedTestId: string | null;\n  onSelectTest: (testId: string) => void;\n}\n\ninterface TreeNode {\n  label: string;\n  tests: Test[];\n  children: Map<string, TreeNode>;\n}\n\nexport function TestTree({ tests, selectedTestId, onSelectTest }: TestTreeProps) {\n  const [collapsed, setCollapsed] = useState<Set<string>>(new Set());\n\n  const tree = useMemo(() => buildTree(tests), [tests]);\n\n  const toggle = useCallback((key: string) => {\n    setCollapsed((prev) =>\n      prev.has(key) ? new Set([...prev].filter((k) => k !== key)) : new Set([...prev, key]),\n    );\n  }, []);\n\n  return <>{renderNodes(tree, collapsed, toggle, selectedTestId, onSelectTest, 0, \"\")}</>;\n}\n\nfunction buildTree(tests: Test[]): Map<string, TreeNode> {\n  const root = new Map<string, TreeNode>();\n\n  for (const test of tests) {\n    const specName = test.spec_name || \"(no spec)\";\n    const path = test.test_path.length > 0 ? test.test_path : [test.test_name];\n\n    if (!root.has(specName)) {\n      root.set(specName, { label: specName, tests: [], children: new Map() });\n    }\n    const specNode = root.get(specName)!;\n\n    if (path.length <= 1) {\n      specNode.tests.push(test);\n      continue;\n    }\n\n    let current = specNode;\n    for (let i = 0; i < path.length - 1; i++) {\n      const segment = path[i];\n      if (!current.children.has(segment)) {\n        current.children.set(segment, { label: segment, tests: [], children: new Map() });\n      }\n      current = current.children.get(segment)!;\n    }\n    current.tests.push(test);\n  }\n\n  return root;\n}\n\nfunction renderNodes(\n  nodes: Map<string, TreeNode>,\n  collapsed: Set<string>,\n  toggle: (key: string) => void,\n  selectedTestId: string | null,\n  onSelectTest: (testId: string) => void,\n  depth: number,\n  parentKey: string,\n): React.ReactNode[] {\n  const result: React.ReactNode[] = [];\n\n  for (const [key, node] of nodes) {\n    const nodeKey = parentKey ? `${parentKey}/${key}` : key;\n    const isCollapsed = collapsed.has(nodeKey);\n    const hasChildren = node.children.size > 0 || node.tests.length > 0;\n    const status = getNodeAggregateStatus(node);\n\n    result.push(\n      <button\n        type=\"button\"\n        key={`group-${nodeKey}`}\n        className=\"w-full text-left flex items-center gap-1 hover:bg-[var(--stove-hover)] cursor-pointer\"\n        style={{ paddingLeft: `${depth * 12 + 8}px`, paddingTop: \"4px\", paddingBottom: \"4px\" }}\n        onClick={() => toggle(nodeKey)}\n      >\n        {hasChildren && (\n          <svg\n            aria-hidden=\"true\"\n            className={`w-3 h-3 shrink-0 text-[var(--stove-text-muted)] transition-transform ${isCollapsed ? \"\" : \"rotate-90\"}`}\n            viewBox=\"0 0 16 16\"\n            fill=\"currentColor\"\n          >\n            <path d=\"M6 4l4 4-4 4z\" />\n          </svg>\n        )}\n        <span className=\"text-xs font-medium text-[var(--stove-text-secondary)] truncate flex-1\">\n          {node.label}\n        </span>\n        <StatusDot status={status} />\n      </button>,\n    );\n\n    if (!isCollapsed) {\n      if (node.children.size > 0) {\n        result.push(\n          ...renderNodes(\n            node.children,\n            collapsed,\n            toggle,\n            selectedTestId,\n            onSelectTest,\n            depth + 1,\n            nodeKey,\n          ),\n        );\n      }\n      for (const test of node.tests) {\n        result.push(\n          <div key={test.id} style={{ paddingLeft: `${depth * 12}px` }}>\n            <TestListItem\n              test={test}\n              selected={selectedTestId === test.id}\n              onSelect={() => onSelectTest(test.id)}\n              hideSpec\n            />\n          </div>,\n        );\n      }\n    }\n  }\n\n  return result;\n}\n\nfunction getNodeAggregateStatus(node: TreeNode): Status {\n  const statuses: Status[] = [];\n  collectNodeStatuses(node, statuses);\n  return aggregateStatus(statuses);\n}\n\nfunction collectNodeStatuses(node: TreeNode, out: Status[]): void {\n  for (const test of node.tests) {\n    out.push(test.status);\n  }\n  for (const child of node.children.values()) {\n    collectNodeStatuses(child, out);\n  }\n}\n\nfunction StatusDot({ status }: { status: Status }) {\n  const color =\n    status === \"FAILED\" || status === \"ERROR\"\n      ? \"bg-red-400\"\n      : status === \"PASSED\"\n        ? \"bg-emerald-400\"\n        : \"bg-blue-400 animate-pulse-dot\";\n\n  return <span className={`w-1.5 h-1.5 rounded-full shrink-0 mr-2 ${color}`} />;\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/main.tsx",
    "content": "import { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport App from \"./App\";\nimport { ThemeProvider } from \"./hooks/useTheme\";\nimport \"./index.css\";\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      refetchOnWindowFocus: false,\n      retry: 1,\n    },\n  },\n});\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <QueryClientProvider client={queryClient}>\n      <ThemeProvider>\n        <App />\n      </ThemeProvider>\n    </QueryClientProvider>\n  </StrictMode>,\n);\n"
  },
  {
    "path": "tools/stove-cli/spa/src/utils/filters.ts",
    "content": "import type { Test } from \"../api/types\";\nimport type { FilterValue } from \"../layout/sidebar/TestFilters\";\nimport { isFailed, isPassed } from \"./status\";\n\nfunction matchesFilter(test: Test, filter: FilterValue): boolean {\n  if (filter === \"pass\") return isPassed(test.status);\n  if (filter === \"fail\") return isFailed(test.status);\n  return true;\n}\n\nfunction matchesSearch(test: Test, query: string): boolean {\n  if (!query) return true;\n  const q = query.toLowerCase();\n  return (\n    test.test_name.toLowerCase().includes(q) ||\n    test.test_path.some((seg) => seg.toLowerCase().includes(q)) ||\n    test.spec_name.toLowerCase().includes(q)\n  );\n}\n\nexport function filterTests(tests: Test[], filter: FilterValue, search: string): Test[] {\n  return tests.filter((t) => matchesFilter(t, filter) && matchesSearch(t, search));\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/utils/flow.ts",
    "content": "import dagre from \"@dagrejs/dagre\";\nimport type { Edge, Node } from \"@xyflow/react\";\nimport { MarkerType } from \"@xyflow/react\";\nimport type { Entry, Span } from \"../api/types\";\nimport { parseAttrs } from \"./json\";\nimport { isFailed } from \"./result\";\n\nconst EXECUTION_GAP_THRESHOLD_MS = 1000;\nconst ADJACENT_MERGE_WINDOW_MS = 250;\nconst STEP_NODE_SIZE = { width: 240, height: 128 };\nconst TRACE_NODE_SIZE = { width: 240, height: 120 };\nconst ARRANGE_NODE_SIZE = { width: 240, height: 128 };\nconst GAP_NODE_SIZE = { width: 208, height: 96 };\n\nexport interface SystemNodeData extends Record<string, unknown> {\n  kind: \"step\" | \"trace\" | \"arrange\";\n  system: string;\n  action: string;\n  result: string;\n  count: number;\n  error: string | null;\n  entries: Entry[];\n  traceId: string | null;\n  startedAt: string | null;\n  endedAt: string | null;\n  durationMs: number | null;\n  inspectable: boolean;\n}\n\nexport interface GapNodeData extends Record<string, unknown> {\n  kind: \"gap\";\n  label: string;\n  durationMs: number;\n  startedAt: string;\n  endedAt: string;\n  inspectable: false;\n}\n\nexport type FlowNodeData = SystemNodeData | GapNodeData;\n\nexport interface DurationEdgeData extends Record<string, unknown> {\n  durationMs: number;\n  label?: string;\n}\n\ninterface TimelineStepGroup {\n  entries: Entry[];\n  startedAtMs: number;\n  endedAtMs: number;\n  kind: \"step\" | \"arrange\";\n  actionLabel: string;\n  displayCount: number;\n}\n\ninterface TimelineStepSpec {\n  kind: \"step\";\n  group: TimelineStepGroup;\n}\n\ninterface TimelineGapSpec {\n  kind: \"gap\";\n  durationMs: number;\n  startedAt: string;\n  endedAt: string;\n  startedAtMs: number;\n  endedAtMs: number;\n}\n\ntype TimelineSpec = TimelineStepSpec | TimelineGapSpec;\n\nexport function entriesToDag(entries: Entry[]): { nodes: Node<FlowNodeData>[]; edges: Edge[] } {\n  if (entries.length === 0) return { nodes: [], edges: [] };\n\n  const sorted = [...entries].sort((a, b) => toMs(a.timestamp) - toMs(b.timestamp));\n  const stepGroups = sorted.length > 0 ? collapseArrangeRuns(groupEntriesIntoSteps(sorted)) : [];\n  const { arrangeGroups, mainGroups } = splitArrangeGroups(stepGroups);\n  const orderedSpecs = expandTimelineSpecs(mainGroups);\n\n  const timelineNodes: Node<FlowNodeData>[] = orderedSpecs.map((spec, index) => {\n    if (spec.kind === \"gap\") {\n      return {\n        id: nodeIdForSpec(spec, index),\n        type: \"gapNode\",\n        position: { x: 0, y: 0 },\n        data: {\n          kind: \"gap\",\n          label: \"Idle gap\",\n          durationMs: spec.durationMs,\n          startedAt: spec.startedAt,\n          endedAt: spec.endedAt,\n          inspectable: false,\n        } satisfies GapNodeData,\n      };\n    }\n\n    const group = spec.group.entries;\n    const first = group[0];\n    const last = group[group.length - 1];\n    const hasFailed = group.some((entry) => isFailed(entry.result));\n    const firstError = group.find((entry) => entry.error)?.error ?? null;\n    const traceId = group.find((entry) => entry.trace_id)?.trace_id ?? null;\n\n    return createSystemNode(nodeIdForSpec(spec, index), {\n      kind: spec.group.kind,\n      system: first.system,\n      action: spec.group.actionLabel,\n      result: hasFailed ? \"FAILED\" : \"PASSED\",\n      count: spec.group.displayCount,\n      error: firstError,\n      entries: group,\n      traceId,\n      startedAt: first.timestamp,\n      endedAt: last.timestamp,\n      durationMs: Math.max(0, spec.group.endedAtMs - spec.group.startedAtMs),\n      inspectable: true,\n    });\n  });\n\n  const edges: Edge[] = [];\n  for (let i = 1; i < orderedSpecs.length; i++) {\n    const previousSpec = orderedSpecs[i - 1];\n    const currentSpec = orderedSpecs[i];\n\n    edges.push({\n      id: `edge-${i - 1}-${i}`,\n      source: nodeIdForSpec(previousSpec, i - 1),\n      target: nodeIdForSpec(currentSpec, i),\n      type: \"durationEdge\",\n      markerEnd: { type: MarkerType.ArrowClosed },\n      data: {\n        durationMs: Math.max(0, getSpecStartedAtMs(currentSpec) - getSpecEndedAtMs(previousSpec)),\n      } satisfies DurationEdgeData,\n    });\n  }\n\n  const nodes = [...timelineNodes];\n\n  if (arrangeGroups.length > 0) {\n    const firstTimelineNodeId = timelineNodes.length > 0 ? timelineNodes[0].id : null;\n\n    arrangeGroups.forEach((group, index) => {\n      const first = group.entries[0];\n      const last = group.entries[group.entries.length - 1];\n      const hasFailed = group.entries.some((entry) => isFailed(entry.result));\n      const firstError = group.entries.find((entry) => entry.error)?.error ?? null;\n      const traceId = group.entries.find((entry) => entry.trace_id)?.trace_id ?? null;\n      const arrangeNodeId = `arrange-step-${index}`;\n\n      nodes.push(\n        createSystemNode(arrangeNodeId, {\n          kind: \"arrange\",\n          system: first.system,\n          action: group.actionLabel,\n          result: hasFailed ? \"FAILED\" : \"PASSED\",\n          count: group.displayCount,\n          error: firstError,\n          entries: group.entries,\n          traceId,\n          startedAt: first.timestamp,\n          endedAt: last.timestamp,\n          durationMs: Math.max(0, group.endedAtMs - group.startedAtMs),\n          inspectable: true,\n        }),\n      );\n\n      if (firstTimelineNodeId) {\n        edges.push({\n          id: `${arrangeNodeId}-${firstTimelineNodeId}`,\n          source: arrangeNodeId,\n          target: firstTimelineNodeId,\n          type: \"durationEdge\",\n          markerEnd: { type: MarkerType.ArrowClosed },\n          data: {\n            durationMs: 0,\n            label: \"ready\",\n          } satisfies DurationEdgeData,\n        });\n      }\n    });\n  }\n\n  return { nodes, edges };\n}\n\nexport function spansToTraceDag(spans: Span[]): { nodes: Node<FlowNodeData>[]; edges: Edge[] } {\n  if (spans.length === 0) return { nodes: [], edges: [] };\n\n  const nodes: Node<FlowNodeData>[] = spans.map((span) => {\n    const system = detectSystemFromSpan(span);\n\n    return {\n      id: span.span_id,\n      type: \"systemNode\",\n      position: { x: 0, y: 0 },\n      data: {\n        kind: \"trace\",\n        system,\n        action: span.operation_name,\n        result: span.status,\n        count: 1,\n        error: span.exception_message ?? null,\n        entries: [],\n        traceId: span.trace_id,\n        startedAt: null,\n        endedAt: null,\n        durationMs: Math.max(0, (span.end_time_nanos - span.start_time_nanos) / 1_000_000),\n        inspectable: true,\n      } satisfies SystemNodeData,\n    };\n  });\n\n  const spanIds = new Set(spans.map((span) => span.span_id));\n  const edges: Edge[] = spans\n    .filter((span) => span.parent_span_id && spanIds.has(span.parent_span_id))\n    .map((span) => ({\n      id: `span-edge-${span.parent_span_id}-${span.span_id}`,\n      source: span.parent_span_id!,\n      target: span.span_id,\n      type: \"durationEdge\",\n      markerEnd: { type: MarkerType.ArrowClosed },\n      data: {\n        durationMs: Math.max(0, (span.end_time_nanos - span.start_time_nanos) / 1_000_000),\n      } satisfies DurationEdgeData,\n    }));\n\n  return { nodes, edges };\n}\n\nexport function applyDagreLayout(\n  nodes: Node<FlowNodeData>[],\n  edges: Edge[],\n  direction: \"LR\" | \"TB\" = \"LR\",\n): Node<FlowNodeData>[] {\n  if (nodes.length === 0) return nodes;\n\n  const g = new dagre.graphlib.Graph();\n  g.setDefaultEdgeLabel(() => ({}));\n  g.setGraph({\n    rankdir: direction,\n    nodesep: 128,\n    ranksep: 176,\n    edgesep: 48,\n    marginx: 24,\n    marginy: 24,\n  });\n\n  for (const node of nodes) {\n    const size = getNodeLayoutSize(node);\n    g.setNode(node.id, size);\n  }\n  for (const edge of edges) {\n    g.setEdge(edge.source, edge.target);\n  }\n\n  dagre.layout(g);\n\n  return nodes.map((node) => {\n    const pos = g.node(node.id);\n    const size = getNodeLayoutSize(node);\n    return {\n      ...node,\n      position: { x: pos.x - size.width / 2, y: pos.y - size.height / 2 },\n    };\n  });\n}\n\nexport function getNodeLayoutSize(node: Node<FlowNodeData>): { width: number; height: number } {\n  switch (node.type) {\n    case \"gapNode\":\n      return cloneLayoutSize(GAP_NODE_SIZE);\n    case \"systemNode\":\n      return getSystemNodeLayoutSize(node.data as SystemNodeData);\n    default:\n      return cloneLayoutSize(STEP_NODE_SIZE);\n  }\n}\n\nconst DB_SYSTEM_MAP: Record<string, string> = {\n  postgresql: \"PostgreSQL\",\n  mysql: \"MySQL\",\n  mssql: \"MSSQL\",\n  mongodb: \"MongoDB\",\n  redis: \"Redis\",\n  couchbase: \"Couchbase\",\n  elasticsearch: \"Elasticsearch\",\n  cassandra: \"Cassandra\",\n};\n\nfunction detectSystemFromSpan(span: Span): string {\n  const attrs = parseAttrs(span.attributes);\n  const keys = Object.keys(attrs);\n\n  if (keys.some((key) => key.startsWith(\"http.\"))) return \"HTTP\";\n  if (keys.some((key) => key.startsWith(\"messaging.\"))) return \"Kafka\";\n  if (keys.some((key) => key.startsWith(\"db.\"))) {\n    const dbSystem = attrs[\"db.system\"];\n    if (dbSystem) return DB_SYSTEM_MAP[dbSystem.toLowerCase()] ?? dbSystem;\n    return \"Database\";\n  }\n  if (keys.some((key) => key.startsWith(\"rpc.\"))) return \"gRPC\";\n\n  return span.service_name || \"Unknown\";\n}\n\nfunction createSystemNode(id: string, data: SystemNodeData): Node<FlowNodeData> {\n  return {\n    id,\n    type: \"systemNode\",\n    position: { x: 0, y: 0 },\n    data,\n  };\n}\n\nfunction getSystemNodeLayoutSize(data: SystemNodeData): { width: number; height: number } {\n  switch (data.kind) {\n    case \"trace\":\n      return cloneLayoutSize(TRACE_NODE_SIZE);\n    case \"arrange\":\n      return cloneLayoutSize(ARRANGE_NODE_SIZE);\n    default:\n      return cloneLayoutSize(STEP_NODE_SIZE);\n  }\n}\n\nfunction cloneLayoutSize(size: { width: number; height: number }): {\n  width: number;\n  height: number;\n} {\n  return { width: size.width, height: size.height };\n}\n\nfunction groupEntriesIntoSteps(entries: Entry[]): TimelineStepGroup[] {\n  const groups: TimelineStepGroup[] = [];\n  let currentEntries: Entry[] = [entries[0]];\n  let currentStartedAtMs = toMs(entries[0].timestamp);\n  let currentEndedAtMs = currentStartedAtMs;\n\n  for (let i = 1; i < entries.length; i++) {\n    const previous = currentEntries[currentEntries.length - 1];\n    const next = entries[i];\n    const nextAtMs = toMs(next.timestamp);\n\n    if (canMergeAdjacentEntries(previous, next, currentEndedAtMs, nextAtMs)) {\n      currentEntries.push(next);\n      currentEndedAtMs = nextAtMs;\n      continue;\n    }\n\n    groups.push({\n      entries: currentEntries,\n      startedAtMs: currentStartedAtMs,\n      endedAtMs: currentEndedAtMs,\n      kind: isArrangeEntryGroup(currentEntries) ? \"arrange\" : \"step\",\n      actionLabel: currentEntries[0].action,\n      displayCount: currentEntries.length,\n    });\n    currentEntries = [next];\n    currentStartedAtMs = nextAtMs;\n    currentEndedAtMs = nextAtMs;\n  }\n\n  groups.push({\n    entries: currentEntries,\n    startedAtMs: currentStartedAtMs,\n    endedAtMs: currentEndedAtMs,\n    kind: isArrangeEntryGroup(currentEntries) ? \"arrange\" : \"step\",\n    actionLabel: currentEntries[0].action,\n    displayCount: currentEntries.length,\n  });\n\n  return groups;\n}\n\nfunction canMergeAdjacentEntries(\n  previous: Entry,\n  next: Entry,\n  previousAtMs: number,\n  nextAtMs: number,\n): boolean {\n  return (\n    next.system === previous.system &&\n    next.action === previous.action &&\n    next.result === previous.result &&\n    (next.error ?? null) === (previous.error ?? null) &&\n    (next.trace_id ?? null) === (previous.trace_id ?? null) &&\n    nextAtMs - previousAtMs <= ADJACENT_MERGE_WINDOW_MS\n  );\n}\n\nfunction expandTimelineSpecs(groups: TimelineStepGroup[]): TimelineSpec[] {\n  const specs: TimelineSpec[] = [];\n\n  for (let i = 0; i < groups.length; i++) {\n    if (i > 0) {\n      const previous = groups[i - 1];\n      const current = groups[i];\n      const gapMs = Math.max(0, current.startedAtMs - previous.endedAtMs);\n\n      if (gapMs >= EXECUTION_GAP_THRESHOLD_MS) {\n        specs.push({\n          kind: \"gap\",\n          durationMs: gapMs,\n          startedAt: previous.entries[previous.entries.length - 1].timestamp,\n          endedAt: current.entries[0].timestamp,\n          startedAtMs: previous.endedAtMs,\n          endedAtMs: current.startedAtMs,\n        });\n      }\n    }\n\n    specs.push({\n      kind: \"step\",\n      group: groups[i],\n    });\n  }\n\n  return specs;\n}\n\nfunction splitArrangeGroups(groups: TimelineStepGroup[]): {\n  arrangeGroups: TimelineStepGroup[];\n  mainGroups: TimelineStepGroup[];\n} {\n  let arrangeCount = 0;\n  while (arrangeCount < groups.length && groups[arrangeCount].kind === \"arrange\") {\n    arrangeCount += 1;\n  }\n\n  return {\n    arrangeGroups: groups.slice(0, arrangeCount),\n    mainGroups: groups.slice(arrangeCount),\n  };\n}\n\nfunction collapseArrangeRuns(groups: TimelineStepGroup[]): TimelineStepGroup[] {\n  const collapsed: TimelineStepGroup[] = [];\n  let index = 0;\n\n  while (index < groups.length) {\n    const current = groups[index];\n    if (current.kind !== \"arrange\") {\n      collapsed.push(current);\n      index += 1;\n      continue;\n    }\n\n    const system = current.entries[0]?.system;\n    const arrangeRun = [current];\n    index += 1;\n\n    while (\n      index < groups.length &&\n      groups[index].kind === \"arrange\" &&\n      groups[index].entries[0]?.system === system\n    ) {\n      arrangeRun.push(groups[index]);\n      index += 1;\n    }\n\n    collapsed.push(combineArrangeRun(arrangeRun));\n  }\n\n  return collapsed;\n}\n\nfunction combineArrangeRun(groups: TimelineStepGroup[]): TimelineStepGroup {\n  const firstGroup = groups[0];\n  if (!firstGroup) {\n    throw new Error(\"arrange run cannot be empty\");\n  }\n\n  if (groups.length === 1) {\n    return firstGroup;\n  }\n\n  const entries = groups.flatMap((group) => group.entries);\n  const system = firstGroup.entries[0]?.system ?? \"Unknown\";\n\n  return {\n    entries,\n    startedAtMs: firstGroup.startedAtMs,\n    endedAtMs: groups[groups.length - 1].endedAtMs,\n    kind: \"arrange\",\n    actionLabel: summarizeArrangeAction(system, groups.length, firstGroup.actionLabel),\n    displayCount: groups.length,\n  };\n}\n\nfunction isArrangeEntryGroup(entries: Entry[]): boolean {\n  const first = entries[0];\n  if (!first) {\n    return false;\n  }\n\n  return isArrangeSystem(first.system) && isArrangeAction(first.action);\n}\n\nfunction isArrangeSystem(system: string): boolean {\n  return ARRANGE_SYSTEMS.has(system);\n}\n\nfunction isArrangeAction(action: string): boolean {\n  return ARRANGE_ACTION_PATTERNS.some((pattern) => pattern.test(action));\n}\n\nfunction nodeIdForSpec(spec: TimelineSpec, index: number): string {\n  return spec.kind === \"gap\" ? `gap-${index}` : `step-${index}`;\n}\n\nfunction getSpecStartedAtMs(spec: TimelineSpec): number {\n  return spec.kind === \"gap\" ? spec.startedAtMs : spec.group.startedAtMs;\n}\n\nfunction getSpecEndedAtMs(spec: TimelineSpec): number {\n  return spec.kind === \"gap\" ? spec.endedAtMs : spec.group.endedAtMs;\n}\n\nfunction summarizeArrangeAction(\n  system: string,\n  registrationCount: number,\n  fallbackAction: string,\n): string {\n  if (registrationCount <= 1) {\n    return fallbackAction;\n  }\n\n  if (system === \"WireMock\" || system === \"gRPC Mock\") {\n    return `Registered ${registrationCount} stubs`;\n  }\n\n  return `${registrationCount} setup actions`;\n}\n\nfunction toMs(timestamp: string): number {\n  return new Date(timestamp).getTime();\n}\n\nconst ARRANGE_SYSTEMS = new Set([\"WireMock\", \"gRPC Mock\"]);\nconst ARRANGE_ACTION_PATTERNS = [/^Register stub:/, /^Register .* stub:/];\n"
  },
  {
    "path": "tools/stove-cli/spa/src/utils/format.ts",
    "content": "export function formatDuration(ms: number | null | undefined): string {\n  if (ms == null) return \"-\";\n  if (ms < 1000) return `${ms}ms`;\n  if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;\n  return `${Math.floor(ms / 60000)}m ${Math.round((ms % 60000) / 1000)}s`;\n}\n\nexport function formatTimestamp(iso: string): string {\n  try {\n    const d = new Date(iso);\n    return d.toLocaleTimeString(\"en-US\", {\n      hour12: false,\n      hour: \"2-digit\",\n      minute: \"2-digit\",\n      second: \"2-digit\",\n      fractionalSecondDigits: 3,\n    } as Intl.DateTimeFormatOptions);\n  } catch {\n    return iso;\n  }\n}\n\nexport function formatNanosDuration(startNanos: number, endNanos: number): string {\n  const ms = (endNanos - startNanos) / 1_000_000;\n  return formatDuration(ms);\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/utils/json.ts",
    "content": "export function tryFormatJson(s: string): string {\n  try {\n    return JSON.stringify(JSON.parse(s), null, 2);\n  } catch {\n    return s;\n  }\n}\n\nexport function tryFormatJsonDeep(s: string): string {\n  const parsed = parseJsonDeep(s);\n  if (parsed === null) {\n    return s;\n  }\n  return JSON.stringify(parsed, null, 2);\n}\n\nexport function parseJsonDeep(s: string): unknown | null {\n  try {\n    return normalizeEmbeddedJson(JSON.parse(s));\n  } catch {\n    return null;\n  }\n}\n\nexport interface JsonSearchResult {\n  filteredValue: unknown | null;\n  matchCount: number;\n}\n\nexport function filterJsonByQuery(value: unknown, query: string): JsonSearchResult {\n  const normalizedQuery = query.trim().toLowerCase();\n  if (!normalizedQuery) {\n    return {\n      filteredValue: value,\n      matchCount: 0,\n    };\n  }\n\n  return filterJsonValue(value, normalizedQuery);\n}\n\nexport function describeJsonValue(value: unknown): string {\n  if (Array.isArray(value)) {\n    return `${value.length} item${value.length === 1 ? \"\" : \"s\"}`;\n  }\n\n  if (typeof value === \"object\" && value !== null) {\n    const keys = Object.keys(value);\n    return `${keys.length} key${keys.length === 1 ? \"\" : \"s\"}`;\n  }\n\n  if (value === null) {\n    return \"null\";\n  }\n\n  return typeof value;\n}\n\nexport function getJsonPreviewKeys(value: unknown, limit = 4): string[] {\n  if (Array.isArray(value)) {\n    return value.slice(0, limit).map((_, index) => `[${index}]`);\n  }\n\n  if (typeof value === \"object\" && value !== null) {\n    return Object.keys(value).slice(0, limit);\n  }\n\n  return [];\n}\n\nfunction normalizeEmbeddedJson(value: unknown): unknown {\n  if (typeof value === \"string\") {\n    const trimmed = value.trim();\n    if (\n      (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) ||\n      (trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\"))\n    ) {\n      try {\n        return normalizeEmbeddedJson(JSON.parse(trimmed));\n      } catch {\n        return value;\n      }\n    }\n    return value;\n  }\n\n  if (Array.isArray(value)) {\n    return value.map(normalizeEmbeddedJson);\n  }\n\n  if (typeof value === \"object\" && value !== null) {\n    return Object.fromEntries(\n      Object.entries(value).map(([key, nested]) => [key, normalizeEmbeddedJson(nested)]),\n    );\n  }\n\n  return value;\n}\n\nfunction filterJsonValue(value: unknown, normalizedQuery: string): JsonSearchResult {\n  if (Array.isArray(value)) {\n    const filteredItems: unknown[] = [];\n    let matchCount = 0;\n\n    value.forEach((item) => {\n      const nested = filterJsonValue(item, normalizedQuery);\n      if (nested.filteredValue !== null) {\n        filteredItems.push(nested.filteredValue);\n        matchCount += nested.matchCount;\n      }\n    });\n\n    return filteredItems.length > 0\n      ? { filteredValue: filteredItems, matchCount }\n      : { filteredValue: null, matchCount: 0 };\n  }\n\n  if (typeof value === \"object\" && value !== null) {\n    const filteredEntries: Array<[string, unknown]> = [];\n    let matchCount = 0;\n\n    for (const [key, nestedValue] of Object.entries(value)) {\n      if (key.toLowerCase().includes(normalizedQuery)) {\n        filteredEntries.push([key, nestedValue]);\n        matchCount += 1;\n        continue;\n      }\n\n      const nested = filterJsonValue(nestedValue, normalizedQuery);\n      if (nested.filteredValue !== null) {\n        filteredEntries.push([key, nested.filteredValue]);\n        matchCount += nested.matchCount;\n      }\n    }\n\n    return filteredEntries.length > 0\n      ? { filteredValue: Object.fromEntries(filteredEntries), matchCount }\n      : { filteredValue: null, matchCount: 0 };\n  }\n\n  return primitiveIncludes(value, normalizedQuery)\n    ? { filteredValue: value, matchCount: 1 }\n    : { filteredValue: null, matchCount: 0 };\n}\n\nfunction primitiveIncludes(value: unknown, normalizedQuery: string): boolean {\n  if (value == null) {\n    return \"null\".includes(normalizedQuery);\n  }\n\n  return String(value).toLowerCase().includes(normalizedQuery);\n}\n\nexport function parseAttrs(json: string | null): Record<string, string> {\n  if (!json) return {};\n  try {\n    const parsed: unknown = JSON.parse(json);\n    if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) return {};\n    const result: Record<string, string> = {};\n    for (const [k, v] of Object.entries(parsed)) {\n      result[k] = String(v);\n    }\n    return result;\n  } catch {\n    return {};\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/utils/result.ts",
    "content": "export function isFailed(result: string): boolean {\n  const upper = result.toUpperCase();\n  return upper === \"FAILED\" || upper === \"ERROR\";\n}\n\nexport function isSuccessful(result: string): boolean {\n  const upper = result.toUpperCase();\n  return upper === \"PASSED\" || upper === \"OK\";\n}\n\nexport function getResultTone(result: string): \"failed\" | \"success\" | \"neutral\" {\n  if (isFailed(result)) return \"failed\";\n  if (isSuccessful(result)) return \"success\";\n  return \"neutral\";\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/utils/snapshot-state.ts",
    "content": "import type { Snapshot } from \"../api/types\";\nimport { parseJsonDeep } from \"./json\";\n\nexport interface SnapshotMetric {\n  key: string;\n  label: string;\n  value: number;\n  tone: \"info\" | \"success\" | \"warning\" | \"danger\" | \"neutral\";\n}\n\nexport interface SnapshotPartition<TSnapshot> {\n  detailedSnapshots: TSnapshot[];\n  hiddenCount: number;\n}\n\nexport function hasDetailedSnapshotState(\n  snapshot: Pick<Snapshot, \"state_json\">,\n  parsedState: unknown | null = parseJsonDeep(snapshot.state_json),\n): boolean {\n  if (parsedState !== null) {\n    return hasInspectableValue(parsedState);\n  }\n\n  const rawState = snapshot.state_json.trim();\n  return rawState.length > 0 && rawState !== \"{}\" && rawState !== \"[]\";\n}\n\nexport function getKafkaSnapshotMetrics(\n  snapshot: Pick<Snapshot, \"state_json\">,\n  parsedState: unknown | null = parseJsonDeep(snapshot.state_json),\n): SnapshotMetric[] {\n  if (typeof parsedState !== \"object\" || parsedState === null || Array.isArray(parsedState)) {\n    return [];\n  }\n\n  const state = parsedState as Record<string, unknown>;\n  const metricDefs = [\n    { key: \"consumed\", label: \"Consumed\" },\n    { key: \"published\", label: \"Published\" },\n    { key: \"produced\", label: \"Produced\" },\n    { key: \"committed\", label: \"Committed\" },\n    { key: \"failed\", label: \"Failed\" },\n  ];\n\n  return metricDefs.flatMap(({ key, label }) => {\n    if (!(key in state)) {\n      return [];\n    }\n\n    const value = countMetricValue(state[key]);\n    if (value === null) {\n      return [];\n    }\n\n    return [\n      {\n        key,\n        label,\n        value,\n        tone: metricTone(key, value),\n      } satisfies SnapshotMetric,\n    ];\n  });\n}\n\nexport function partitionSnapshotsByDetail<TSnapshot extends Pick<Snapshot, \"state_json\">>(\n  snapshots: TSnapshot[],\n): SnapshotPartition<TSnapshot> {\n  const detailedSnapshots = snapshots.filter((snapshot) => hasDetailedSnapshotState(snapshot));\n\n  return {\n    detailedSnapshots,\n    hiddenCount: snapshots.length - detailedSnapshots.length,\n  };\n}\n\nfunction hasInspectableValue(value: unknown): boolean {\n  if (Array.isArray(value)) {\n    return value.some(hasInspectableValue);\n  }\n\n  if (typeof value === \"object\" && value !== null) {\n    const entries = Object.values(value);\n    return entries.length > 0 && entries.some(hasInspectableValue);\n  }\n\n  if (typeof value === \"string\") {\n    return value.trim().length > 0;\n  }\n\n  return typeof value === \"number\" || typeof value === \"boolean\";\n}\n\nfunction countMetricValue(value: unknown): number | null {\n  if (typeof value === \"number\" && Number.isFinite(value)) {\n    return value;\n  }\n\n  if (Array.isArray(value)) {\n    return value.length;\n  }\n\n  if (typeof value === \"object\" && value !== null) {\n    return Object.keys(value).length;\n  }\n\n  return null;\n}\n\nfunction metricTone(key: string, value: number): SnapshotMetric[\"tone\"] {\n  if (key === \"failed\") {\n    return value > 0 ? \"danger\" : \"neutral\";\n  }\n\n  if (key === \"published\" || key === \"produced\") {\n    return value > 0 ? \"success\" : \"neutral\";\n  }\n\n  if (key === \"consumed\" || key === \"committed\") {\n    return value > 0 ? \"info\" : \"neutral\";\n  }\n\n  return \"warning\";\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/utils/status.ts",
    "content": "export type Status = \"RUNNING\" | \"PASSED\" | \"FAILED\" | \"ERROR\";\n\nexport const isFailed = (s: Status): boolean => s === \"FAILED\" || s === \"ERROR\";\nexport const isRunning = (s: Status): boolean => s === \"RUNNING\";\nexport const isPassed = (s: Status): boolean => s === \"PASSED\";\n\nexport function aggregateStatus(statuses: Iterable<Status>): Status {\n  let hasPassed = false;\n  for (const s of statuses) {\n    if (isFailed(s)) return \"FAILED\";\n    if (isRunning(s)) return \"RUNNING\";\n    if (isPassed(s)) hasPassed = true;\n  }\n  return hasPassed ? \"PASSED\" : \"RUNNING\";\n}\n\nexport function collectStatuses<T>(items: T[], getStatus: (item: T) => Status): Status {\n  const statuses = items.map(getStatus);\n  return aggregateStatus(statuses);\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/utils/systems.ts",
    "content": "interface SystemInfo {\n  color: string;\n  icon: string;\n}\n\nconst SYSTEM_MAP: Record<string, SystemInfo> = {\n  HTTP: { color: \"#60a5fa\", icon: \"\\u21c4\" },\n  Kafka: { color: \"#f59e0b\", icon: \"\\u26a1\" },\n  PostgreSQL: { color: \"#34d399\", icon: \"\\u229e\" },\n  Postgres: { color: \"#34d399\", icon: \"\\u229e\" },\n  WireMock: { color: \"#a78bfa\", icon: \"\\u25ce\" },\n  gRPC: { color: \"#fb923c\", icon: \"\\u25c8\" },\n  \"gRPC Mock\": { color: \"#fb923c\", icon: \"\\u25c8\" },\n  Redis: { color: \"#f87171\", icon: \"\\u25c6\" },\n  MongoDB: { color: \"#4ade80\", icon: \"\\u2291\" },\n  Mongo: { color: \"#4ade80\", icon: \"\\u2291\" },\n  Couchbase: { color: \"#06b6d4\", icon: \"\\u2261\" },\n  Elasticsearch: { color: \"#fbbf24\", icon: \"\\u2315\" },\n  MySQL: { color: \"#0ea5e9\", icon: \"\\u229e\" },\n  MSSQL: { color: \"#8b5cf6\", icon: \"\\u229e\" },\n  Cassandra: { color: \"#d946ef\", icon: \"\\u2609\" },\n};\n\nconst DEFAULT_SYSTEM: SystemInfo = { color: \"#94a3b8\", icon: \"\\u2022\" };\n\nexport function getSystemInfo(name: string): SystemInfo {\n  return SYSTEM_MAP[name] ?? DEFAULT_SYSTEM;\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/utils/version-mismatch.ts",
    "content": "import type { AppSummary } from \"../api/types\";\n\nconst RELEASE_VERSION_PATTERN = /^(\\d+)\\.(\\d+)\\.(\\d+)$/;\nconst SWITCH_HINT = \"Switch to a mismatched app to see exact remediation.\";\nconst CLI_UPGRADE_COMMAND = \"brew upgrade Trendyol/trendyol-tap/stove\";\n\nexport type VersionMismatchKind = \"runtime_older\" | \"cli_older\" | \"unknown\";\n\nexport interface VersionMismatch {\n  appName: string;\n  cliVersion: string;\n  runtimeVersion: string | null;\n  kind: VersionMismatchKind;\n}\n\nexport interface VersionMismatchSummary {\n  cliVersion: string;\n  mismatches: VersionMismatch[];\n  affectedAppNames: string[];\n  selectedAppMismatch: VersionMismatch | null;\n}\n\nexport interface VersionMismatchRemediationStep {\n  kind: \"text\" | \"command\";\n  value: string;\n}\n\nexport interface VersionMismatchBannerModel {\n  title: string;\n  affectedApps: string[];\n  switchHint: string | null;\n  selectedAppName: string | null;\n  runtimeVersion: string | null;\n  cliVersion: string;\n  remediationSteps: VersionMismatchRemediationStep[];\n}\n\nexport function compareVersions(\n  runtimeVersion: string | null | undefined,\n  cliVersion: string,\n): VersionMismatchKind | null {\n  const normalizedRuntime = normalizeVersion(runtimeVersion);\n  if (normalizedRuntime === cliVersion) {\n    return null;\n  }\n\n  if (!normalizedRuntime) {\n    return \"unknown\";\n  }\n\n  const runtimeTriplet = parseReleaseVersion(normalizedRuntime);\n  const cliTriplet = parseReleaseVersion(cliVersion);\n  if (!runtimeTriplet || !cliTriplet) {\n    return \"unknown\";\n  }\n\n  for (let index = 0; index < runtimeTriplet.length; index += 1) {\n    if (runtimeTriplet[index] < cliTriplet[index]) {\n      return \"runtime_older\";\n    }\n    if (runtimeTriplet[index] > cliTriplet[index]) {\n      return \"cli_older\";\n    }\n  }\n\n  return \"unknown\";\n}\n\nexport function summarizeVersionMismatches(\n  apps: AppSummary[],\n  cliVersion: string | null,\n  selectedApp: string | null,\n): VersionMismatchSummary | null {\n  if (!cliVersion) {\n    return null;\n  }\n\n  const mismatches = apps\n    .map((app) => createVersionMismatch(app, cliVersion))\n    .filter((mismatch): mismatch is VersionMismatch => mismatch !== null);\n\n  if (mismatches.length === 0) {\n    return null;\n  }\n\n  const affectedAppNames = mismatches.map((mismatch) => mismatch.appName);\n\n  return {\n    cliVersion,\n    mismatches,\n    affectedAppNames,\n    selectedAppMismatch: mismatches.find((mismatch) => mismatch.appName === selectedApp) ?? null,\n  };\n}\n\nexport function buildVersionMismatchBannerModel(\n  summary: VersionMismatchSummary,\n): VersionMismatchBannerModel {\n  const mismatchCount = summary.mismatches.length;\n  const selectedAppMismatch = summary.selectedAppMismatch;\n\n  return {\n    title: bannerTitle(mismatchCount),\n    affectedApps: summary.affectedAppNames,\n    switchHint: selectedAppMismatch ? null : SWITCH_HINT,\n    selectedAppName: selectedAppMismatch?.appName ?? null,\n    runtimeVersion: selectedAppMismatch?.runtimeVersion ?? null,\n    cliVersion: summary.cliVersion,\n    remediationSteps: selectedAppMismatch ? remediationStepsForMismatch(selectedAppMismatch) : [],\n  };\n}\n\nfunction createVersionMismatch(app: AppSummary, cliVersion: string): VersionMismatch | null {\n  const kind = compareVersions(app.stove_version, cliVersion);\n  if (!kind) {\n    return null;\n  }\n\n  return {\n    appName: app.app_name,\n    cliVersion,\n    runtimeVersion: normalizeVersion(app.stove_version),\n    kind,\n  };\n}\n\nfunction bannerTitle(mismatchCount: number): string {\n  return mismatchCount === 1\n    ? \"A version mismatch was detected in the latest Stove dashboard run.\"\n    : `${mismatchCount} apps have a version mismatch in their latest Stove dashboard runs.`;\n}\n\nfunction normalizeVersion(version: string | null | undefined): string | null {\n  const normalized = version?.trim();\n  return normalized ? normalized : null;\n}\n\nfunction parseReleaseVersion(version: string): number[] | null {\n  const match = version.match(RELEASE_VERSION_PATTERN);\n  if (!match) {\n    return null;\n  }\n\n  return match.slice(1).map(Number);\n}\n\nfunction remediationStepsForMismatch(mismatch: VersionMismatch): VersionMismatchRemediationStep[] {\n  if (mismatch.kind === \"runtime_older\") {\n    return [textStep(dependencyAlignmentMessage(mismatch.cliVersion))];\n  }\n\n  if (mismatch.kind === \"cli_older\") {\n    return [\n      textStep(\"Update stove-cli to match the runtime version:\"),\n      commandStep(CLI_UPGRADE_COMMAND),\n      commandStep(installScriptCommand(mismatch.runtimeVersion!)),\n    ];\n  }\n\n  return [\n    textStep(\n      `This run comes from an older or non-standard Stove runtime. ${dependencyAlignmentMessage(mismatch.cliVersion)}`,\n    ),\n  ];\n}\n\nfunction dependencyAlignmentMessage(cliVersion: string): string {\n  return `Align the Stove BOM or all Stove test dependencies to ${cliVersion}.`;\n}\n\nfunction installScriptCommand(runtimeVersion: string): string {\n  return `curl -fsSL https://raw.githubusercontent.com/Trendyol/stove/main/tools/stove-cli/install.sh | sh -s -- --version ${runtimeVersion}`;\n}\n\nfunction textStep(value: string): VersionMismatchRemediationStep {\n  return { kind: \"text\", value };\n}\n\nfunction commandStep(value: string): VersionMismatchRemediationStep {\n  return { kind: \"command\", value };\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ndeclare const __STOVE_VERSION__: string;\n"
  },
  {
    "path": "tools/stove-cli/spa/test/api-client.test.mjs",
    "content": "import assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport createJiti from \"jiti\";\n\nconst jiti = createJiti(import.meta.url);\nconst { api } = await jiti.import(\"../src/api/client.ts\");\n\ntest(\"getSnapshots URL-encodes run and test ids before requesting snapshot data\", async () => {\n  const originalFetch = globalThis.fetch;\n  const seen = [];\n\n  globalThis.fetch = async (input) => {\n    seen.push(String(input));\n    return new Response(\"[]\", {\n      status: 200,\n      headers: { \"content-type\": \"application/json\" },\n    });\n  };\n\n  try {\n    await api.getSnapshots(\n      \"run:1\",\n      \"AuditHeadersValidationTests::should not require audit headers for get endpoint\",\n    );\n  } finally {\n    globalThis.fetch = originalFetch;\n  }\n\n  assert.equal(\n    seen[0],\n    \"/api/v1/runs/run%3A1/tests/AuditHeadersValidationTests%3A%3Ashould%20not%20require%20audit%20headers%20for%20get%20endpoint/snapshots\",\n  );\n});\n"
  },
  {
    "path": "tools/stove-cli/spa/test/flow.test.mjs",
    "content": "import assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport createJiti from \"jiti\";\n\nconst jiti = createJiti(import.meta.url);\nconst { applyDagreLayout, entriesToDag, getNodeLayoutSize, spansToTraceDag } = await jiti.import(\n  \"../src/utils/flow.ts\",\n);\n\ntest(\"spansToTraceDag preserves UNSET span status instead of marking it as passed\", () => {\n  const { nodes } = spansToTraceDag([\n    {\n      id: 1,\n      run_id: \"run-1\",\n      trace_id: \"trace-1\",\n      span_id: \"span-1\",\n      parent_span_id: null,\n      operation_name: \"GET /health\",\n      service_name: \"my-api\",\n      start_time_nanos: 0,\n      end_time_nanos: 1_000_000,\n      status: \"UNSET\",\n      attributes: null,\n      exception_type: null,\n      exception_message: null,\n      exception_stack_trace: null,\n    },\n  ]);\n\n  assert.equal(nodes.length, 1);\n  assert.equal(nodes[0].data.result, \"UNSET\");\n});\n\ntest(\"entriesToDag keeps distinct actions separate even when they belong to the same system\", () => {\n  const { nodes } = entriesToDag([\n    {\n      id: 1,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.000Z\",\n      system: \"HTTP\",\n      action: \"POST /products\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: \"trace-1\",\n    },\n    {\n      id: 2,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.050Z\",\n      system: \"HTTP\",\n      action: \"GET /products/42\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: \"trace-2\",\n    },\n  ]);\n\n  assert.equal(nodes.length, 2);\n  assert.equal(nodes[0].data.action, \"POST /products\");\n  assert.equal(nodes[1].data.action, \"GET /products/42\");\n});\n\ntest(\"entriesToDag inserts explicit gap nodes for long idle periods\", () => {\n  const { nodes, edges } = entriesToDag([\n    {\n      id: 1,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.000Z\",\n      system: \"HTTP\",\n      action: \"POST /products\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: \"trace-1\",\n    },\n    {\n      id: 2,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:03.250Z\",\n      system: \"Kafka\",\n      action: \"consume ProductCreated\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n  ]);\n\n  assert.equal(nodes.length, 3);\n  assert.equal(nodes[1].type, \"gapNode\");\n  assert.equal(nodes[1].data.kind, \"gap\");\n  assert.equal(nodes[1].data.durationMs, 3250);\n  assert.equal(edges.length, 2);\n});\n\ntest(\"entriesToDag keeps captured snapshots out of the execution graph layout\", () => {\n  const { nodes, edges } = entriesToDag([\n    {\n      id: 1,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.000Z\",\n      system: \"HTTP\",\n      action: \"POST /products\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: \"trace-1\",\n    },\n  ]);\n\n  assert.equal(nodes.length, 1);\n  assert.equal(nodes[0].type, \"systemNode\");\n  assert.equal(edges.length, 0);\n});\n\ntest(\"entriesToDag lifts leading mock registration steps into an arrange branch\", () => {\n  const { nodes, edges } = entriesToDag([\n    {\n      id: 1,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.000Z\",\n      system: \"WireMock\",\n      action: \"Register stub: GET /inventory\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 2,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.100Z\",\n      system: \"gRPC Mock\",\n      action: \"Register unary stub: inventory.StockService/GetStock\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 3,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.250Z\",\n      system: \"HTTP\",\n      action: \"POST /products\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: \"trace-1\",\n    },\n  ]);\n\n  assert.equal(nodes.length, 3);\n  const arrangeNodes = nodes.filter(\n    (node) => node.type === \"systemNode\" && node.data.kind === \"arrange\",\n  );\n  assert.equal(arrangeNodes.length, 2);\n  const executionNode = nodes.find(\n    (node) => node.type === \"systemNode\" && node.data.kind === \"step\",\n  );\n  assert.ok(executionNode);\n  assert.equal(executionNode.data.system, \"HTTP\");\n  assert.equal(edges.filter((edge) => edge.data.label === \"ready\").length, 2);\n});\n\ntest(\"entriesToDag groups leading registrations by mock system\", () => {\n  const { nodes } = entriesToDag([\n    {\n      id: 1,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.000Z\",\n      system: \"WireMock\",\n      action: \"Register stub: GET /inventory\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 2,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.100Z\",\n      system: \"WireMock\",\n      action: \"Register stub: GET /prices\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 3,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.150Z\",\n      system: \"gRPC Mock\",\n      action: \"Register unary stub: inventory.StockService/GetStock\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 4,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.250Z\",\n      system: \"HTTP\",\n      action: \"POST /products\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: \"trace-1\",\n    },\n  ]);\n\n  const arrangeNodes = nodes.filter(\n    (node) => node.type === \"systemNode\" && node.data.kind === \"arrange\",\n  );\n\n  assert.equal(arrangeNodes.length, 2);\n  const wireMockNode = arrangeNodes.find((node) => node.data.system === \"WireMock\");\n  assert.ok(wireMockNode);\n  assert.equal(wireMockNode.data.count, 2);\n  assert.equal(wireMockNode.data.action, \"Registered 2 stubs\");\n});\n\ntest(\"entriesToDag groups consecutive WireMock registrations in the middle of the timeline\", () => {\n  const { nodes } = entriesToDag([\n    {\n      id: 1,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-31T09:00:00.000Z\",\n      system: \"SpringJdbc\",\n      action: \"Select business unit\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 2,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-31T09:00:00.050Z\",\n      system: \"WireMock\",\n      action: \"Register stub: GET /first\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 3,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-31T09:00:00.100Z\",\n      system: \"WireMock\",\n      action: \"Register stub: GET /second\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 4,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-31T09:00:00.150Z\",\n      system: \"Kafka\",\n      action: \"Publish order result\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n  ]);\n\n  const flowNodes = nodes.filter((node) => node.type === \"systemNode\");\n  assert.equal(flowNodes.length, 3);\n  assert.equal(flowNodes[1].data.system, \"WireMock\");\n  assert.equal(flowNodes[1].data.kind, \"arrange\");\n  assert.equal(flowNodes[1].data.count, 2);\n  assert.equal(flowNodes[1].data.action, \"Registered 2 stubs\");\n});\n\ntest(\"applyDagreLayout keeps arrange siblings on distinct coordinates\", () => {\n  const dag = entriesToDag([\n    {\n      id: 1,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.000Z\",\n      system: \"WireMock\",\n      action: \"Register stub: GET /inventory\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 2,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.050Z\",\n      system: \"WireMock\",\n      action: \"Register stub: GET /prices\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 3,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.100Z\",\n      system: \"gRPC Mock\",\n      action: \"Register unary stub: inventory.StockService/GetStock\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: null,\n    },\n    {\n      id: 4,\n      run_id: \"run-1\",\n      test_id: \"test-1\",\n      timestamp: \"2026-03-30T10:00:00.300Z\",\n      system: \"HTTP\",\n      action: \"POST /products\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: null,\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: \"trace-1\",\n    },\n  ]);\n\n  const laidOut = applyDagreLayout(dag.nodes, dag.edges);\n  const arrangeNodes = laidOut.filter(\n    (node) => node.type === \"systemNode\" && node.data.kind === \"arrange\",\n  );\n\n  assert.equal(arrangeNodes.length, 2);\n  assert.notDeepEqual(\n    arrangeNodes.map((node) => node.position),\n    [{ x: arrangeNodes[0].position.x, y: arrangeNodes[0].position.y }, { x: arrangeNodes[0].position.x, y: arrangeNodes[0].position.y }],\n  );\n});\n\ntest(\"applyDagreLayout keeps trace siblings on distinct coordinates\", () => {\n  const dag = spansToTraceDag([\n    {\n      id: 1,\n      run_id: \"run-1\",\n      trace_id: \"trace-1\",\n      span_id: \"root\",\n      parent_span_id: null,\n      operation_name: \"request\",\n      service_name: \"api\",\n      start_time_nanos: 0,\n      end_time_nanos: 4_000_000,\n      status: \"OK\",\n      attributes: null,\n      exception_type: null,\n      exception_message: null,\n      exception_stack_trace: null,\n    },\n    {\n      id: 2,\n      run_id: \"run-1\",\n      trace_id: \"trace-1\",\n      span_id: \"child-a\",\n      parent_span_id: \"root\",\n      operation_name: \"db query\",\n      service_name: \"postgres\",\n      start_time_nanos: 500_000,\n      end_time_nanos: 2_000_000,\n      status: \"OK\",\n      attributes: '{\"db.system\":\"postgresql\"}',\n      exception_type: null,\n      exception_message: null,\n      exception_stack_trace: null,\n    },\n    {\n      id: 3,\n      run_id: \"run-1\",\n      trace_id: \"trace-1\",\n      span_id: \"child-b\",\n      parent_span_id: \"root\",\n      operation_name: \"http call\",\n      service_name: \"inventory\",\n      start_time_nanos: 1_000_000,\n      end_time_nanos: 3_000_000,\n      status: \"OK\",\n      attributes: '{\"http.method\":\"GET\"}',\n      exception_type: null,\n      exception_message: null,\n      exception_stack_trace: null,\n    },\n  ]);\n\n  const laidOut = applyDagreLayout(dag.nodes, dag.edges);\n  const traceChildren = laidOut.filter((node) => node.id === \"child-a\" || node.id === \"child-b\");\n\n  assert.equal(traceChildren.length, 2);\n  assert.notDeepEqual(traceChildren[0].position, traceChildren[1].position);\n});\n\ntest(\"getNodeLayoutSize keeps stable spacing for regular graph nodes\", () => {\n  const stepNodeSize = getNodeLayoutSize({\n    id: \"step-1\",\n    type: \"systemNode\",\n    position: { x: 0, y: 0 },\n    data: {\n      kind: \"step\",\n      system: \"HTTP\",\n      action: \"POST /products\",\n      result: \"PASSED\",\n      count: 1,\n      error: null,\n      entries: [],\n      traceId: null,\n      startedAt: \"2026-03-30T10:00:00.000Z\",\n      endedAt: \"2026-03-30T10:00:00.200Z\",\n      durationMs: 200,\n      inspectable: true,\n    },\n  });\n  const gapNodeSize = getNodeLayoutSize({\n    id: \"gap-1\",\n    type: \"gapNode\",\n    position: { x: 0, y: 0 },\n    data: {\n      kind: \"gap\",\n      label: \"Idle gap\",\n      durationMs: 1200,\n      startedAt: \"2026-03-30T10:00:00.000Z\",\n      endedAt: \"2026-03-30T10:00:01.200Z\",\n      inspectable: false,\n    },\n  });\n\n  assert.equal(stepNodeSize.width, 240);\n  assert.equal(gapNodeSize.width, 208);\n  assert.ok(stepNodeSize.height > gapNodeSize.height);\n});\n"
  },
  {
    "path": "tools/stove-cli/spa/test/json.test.mjs",
    "content": "import assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport createJiti from \"jiti\";\n\nconst jiti = createJiti(import.meta.url);\nconst {\n  tryFormatJsonDeep,\n  parseJsonDeep,\n  filterJsonByQuery,\n  describeJsonValue,\n  getJsonPreviewKeys,\n} = await jiti.import(\"../src/utils/json.ts\");\n\ntest(\"tryFormatJsonDeep expands embedded JSON strings inside structured snapshot payloads\", () => {\n  const formatted = tryFormatJsonDeep(\n    JSON.stringify({\n      outboxEvents: [\n        JSON.stringify({\n          type: \"ProductCreated\",\n          payload: {\n            productId: 42,\n            sellerId: 99,\n          },\n        }),\n      ],\n      metadata: {\n        count: 1,\n      },\n    }),\n  );\n\n  assert.match(formatted, /\"outboxEvents\": \\[/);\n  assert.match(formatted, /\"type\": \"ProductCreated\"/);\n  assert.match(formatted, /\"productId\": 42/);\n  assert.doesNotMatch(formatted, /\\\\\"type\\\\\"/);\n});\n\ntest(\"parseJsonDeep returns structured nested values for snapshot state rendering\", () => {\n  const parsed = parseJsonDeep(\n    JSON.stringify({\n      counts: {\n        succeeded: 4,\n      },\n      outboxEvents: [\n        JSON.stringify({\n          eventType: \"ProductUpdated\",\n        }),\n      ],\n    }),\n  );\n\n  assert.deepEqual(parsed, {\n    counts: {\n      succeeded: 4,\n    },\n    outboxEvents: [\n      {\n        eventType: \"ProductUpdated\",\n      },\n    ],\n  });\n});\n\ntest(\"snapshot json helpers describe and preview object roots for compact cards\", () => {\n  const parsed = parseJsonDeep(\n    JSON.stringify({\n      registeredStubs: [],\n      servedRequests: [],\n      unmatchedRequests: [],\n      metadata: {\n        matched: 0,\n      },\n    }),\n  );\n\n  assert.equal(describeJsonValue(parsed), \"4 keys\");\n  assert.deepEqual(getJsonPreviewKeys(parsed), [\n    \"registeredStubs\",\n    \"servedRequests\",\n    \"unmatchedRequests\",\n    \"metadata\",\n  ]);\n});\n\ntest(\"filterJsonByQuery narrows state by matching property names while preserving subtree context\", () => {\n  const parsed = parseJsonDeep(\n    JSON.stringify({\n      servedRequests: [\n        {\n          method: \"GET\",\n          url: \"/inventory\",\n          matched: true,\n        },\n      ],\n      unmatchedRequests: [],\n    }),\n  );\n\n  const filtered = filterJsonByQuery(parsed, \"servedRequests\");\n\n  assert.equal(filtered.matchCount, 1);\n  assert.deepEqual(filtered.filteredValue, {\n    servedRequests: [\n      {\n        method: \"GET\",\n        url: \"/inventory\",\n        matched: true,\n      },\n    ],\n  });\n});\n\ntest(\"filterJsonByQuery narrows state by matching primitive values\", () => {\n  const parsed = parseJsonDeep(\n    JSON.stringify({\n      servedRequests: [\n        {\n          method: \"GET\",\n          url: \"/inventory\",\n          matched: true,\n        },\n        {\n          method: \"POST\",\n          url: \"/orders\",\n          matched: false,\n        },\n      ],\n    }),\n  );\n\n  const filtered = filterJsonByQuery(parsed, \"/orders\");\n\n  assert.equal(filtered.matchCount, 1);\n  assert.deepEqual(filtered.filteredValue, {\n    servedRequests: [\n      {\n        url: \"/orders\",\n      },\n    ],\n  });\n});\n\ntest(\"filterJsonByQuery returns no matches when the query is absent from the state\", () => {\n  const parsed = parseJsonDeep(\n    JSON.stringify({\n      servedRequests: [],\n      unmatchedRequests: [],\n    }),\n  );\n\n  const filtered = filterJsonByQuery(parsed, \"kafka\");\n\n  assert.equal(filtered.matchCount, 0);\n  assert.equal(filtered.filteredValue, null);\n});\n"
  },
  {
    "path": "tools/stove-cli/spa/test/live-cache.test.mjs",
    "content": "import assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport createJiti from \"jiti\";\nimport { QueryClient } from \"@tanstack/react-query\";\n\nconst jiti = createJiti(import.meta.url);\nconst { applyLiveDashboardEvent } = await jiti.import(\"../src/api/live-cache.ts\");\n\ntest(\"applyLiveDashboardEvent updates run, test, and detail caches from live SSE payloads\", () => {\n  const queryClient = new QueryClient();\n\n  applyLiveDashboardEvent(queryClient, {\n    seq: 1,\n    run_id: \"run-live\",\n    event_type: \"run_started\",\n    payload: {\n      app_name: \"live-app\",\n      started_at: \"2024-06-01T10:00:00Z\",\n      stove_version: \"0.23.2\",\n      systems: [\"HTTP\"],\n    },\n  });\n\n  applyLiveDashboardEvent(queryClient, {\n    seq: 2,\n    run_id: \"run-live\",\n    event_type: \"test_started\",\n    payload: {\n      test_id: \"test-1\",\n      test_name: \"streams immediately\",\n      spec_name: \"LiveSpec\",\n      started_at: \"2024-06-01T10:00:01Z\",\n      status: \"RUNNING\",\n    },\n  });\n\n  applyLiveDashboardEvent(queryClient, {\n    seq: 3,\n    run_id: \"run-live\",\n    event_type: \"entry_recorded\",\n    payload: {\n      id: -3,\n      test_id: \"test-1\",\n      timestamp: \"2024-06-01T10:00:02Z\",\n      system: \"HTTP\",\n      action: \"GET /health\",\n      result: \"PASSED\",\n      input: null,\n      output: null,\n      metadata: \"{}\",\n      expected: null,\n      actual: null,\n      error: null,\n      trace_id: \"trace-1\",\n    },\n  });\n\n  applyLiveDashboardEvent(queryClient, {\n    seq: 4,\n    run_id: \"run-live\",\n    event_type: \"span_recorded\",\n    payload: {\n      id: -4,\n      test_id: null,\n      trace_id: \"trace-1\",\n      span_id: \"span-1\",\n      parent_span_id: null,\n      operation_name: \"GET /health\",\n      service_name: \"live-app\",\n      start_time_nanos: 1_000_000,\n      end_time_nanos: 2_000_000,\n      status: \"OK\",\n      attributes: \"{}\",\n      exception_type: null,\n      exception_message: null,\n      exception_stack_trace: null,\n    },\n  });\n\n  applyLiveDashboardEvent(queryClient, {\n    seq: 5,\n    run_id: \"run-live\",\n    event_type: \"test_ended\",\n    payload: {\n      test_id: \"test-1\",\n      status: \"PASSED\",\n      duration_ms: 1200,\n      error: null,\n      ended_at: \"2024-06-01T10:00:03Z\",\n    },\n  });\n\n  const apps = queryClient.getQueryData([\"apps\"]);\n  const runs = queryClient.getQueryData([\"runs\", \"live-app\"]);\n  const tests = queryClient.getQueryData([\"tests\", \"run-live\"]);\n  const entries = queryClient.getQueryData([\"entries\", \"run-live\", \"test-1\"]);\n  const spans = queryClient.getQueryData([\"spans\", \"run-live\", \"test-1\"]);\n\n  assert.equal(apps.length, 1);\n  assert.equal(apps[0].latest_run_id, \"run-live\");\n  assert.equal(apps[0].stove_version, \"0.23.2\");\n\n  assert.equal(runs.length, 1);\n  assert.equal(runs[0].status, \"RUNNING\");\n  assert.equal(runs[0].stove_version, \"0.23.2\");\n\n  assert.equal(tests.length, 1);\n  assert.equal(tests[0].status, \"PASSED\");\n  assert.equal(tests[0].duration_ms, 1200);\n\n  assert.equal(entries.length, 1);\n  assert.equal(entries[0].action, \"GET /health\");\n\n  assert.equal(spans.length, 1);\n  assert.equal(spans[0].span_id, \"span-1\");\n});\n"
  },
  {
    "path": "tools/stove-cli/spa/test/snapshot-state.test.mjs",
    "content": "import assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport createJiti from \"jiti\";\n\nconst jiti = createJiti(import.meta.url);\nconst {\n  getKafkaSnapshotMetrics,\n  hasDetailedSnapshotState,\n  partitionSnapshotsByDetail,\n} = await jiti.import(\"../src/utils/snapshot-state.ts\");\n\ntest(\"hasDetailedSnapshotState returns false for empty object payloads\", () => {\n  const snapshot = {\n    state_json: \"{}\",\n  };\n\n  assert.equal(hasDetailedSnapshotState(snapshot), false);\n});\n\ntest(\"hasDetailedSnapshotState returns true when structured state exists\", () => {\n  const snapshot = {\n    state_json: JSON.stringify({\n      consumed: [{ topic: \"orders\" }, { topic: \"payments\" }, { topic: \"orders-retry\" }],\n      published: [{ topic: \"events\" }],\n      failed: [],\n    }),\n  };\n\n  assert.equal(hasDetailedSnapshotState(snapshot), true);\n});\n\ntest(\"hasDetailedSnapshotState returns false when nested collections are all empty\", () => {\n  const snapshot = {\n    state_json: JSON.stringify({\n      registeredStubs: [],\n      servedRequests: [],\n      unmatchedRequests: [],\n    }),\n  };\n\n  assert.equal(hasDetailedSnapshotState(snapshot), false);\n});\n\ntest(\"hasDetailedSnapshotState treats scalar counters as meaningful values\", () => {\n  const snapshot = {\n    state_json: JSON.stringify({\n      consumed: 0,\n      published: 0,\n      failed: 0,\n    }),\n  };\n\n  assert.equal(hasDetailedSnapshotState(snapshot), true);\n});\n\ntest(\"getKafkaSnapshotMetrics derives counts from kafka snapshot payloads\", () => {\n  const snapshot = {\n    state_json: JSON.stringify({\n      consumed: [{ topic: \"orders\" }, { topic: \"payments\" }],\n      published: [{ topic: \"events\" }],\n      committed: [],\n      failed: [{ topic: \"orders.failed\" }],\n    }),\n  };\n\n  assert.deepEqual(getKafkaSnapshotMetrics(snapshot), [\n    { key: \"consumed\", label: \"Consumed\", value: 2, tone: \"info\" },\n    { key: \"published\", label: \"Published\", value: 1, tone: \"success\" },\n    { key: \"committed\", label: \"Committed\", value: 0, tone: \"neutral\" },\n    { key: \"failed\", label: \"Failed\", value: 1, tone: \"danger\" },\n  ]);\n});\n\ntest(\"partitionSnapshotsByDetail separates hidden summary-only snapshots from detailed ones\", () => {\n  const snapshots = [\n    {\n      id: \"http\",\n      state_json: \"{}\",\n    },\n    {\n      id: \"wiremock\",\n      state_json: JSON.stringify({\n        registeredStubs: [],\n        servedRequests: [],\n        unmatchedRequests: [],\n      }),\n    },\n    {\n      id: \"kafka\",\n      state_json: JSON.stringify({\n        consumed: 0,\n        published: 1,\n      }),\n    },\n  ];\n\n  const result = partitionSnapshotsByDetail(snapshots);\n\n  assert.deepEqual(result.detailedSnapshots, [snapshots[2]]);\n  assert.equal(result.hiddenCount, 2);\n});\n"
  },
  {
    "path": "tools/stove-cli/spa/test/version-mismatch.test.mjs",
    "content": "import assert from \"node:assert/strict\";\nimport test from \"node:test\";\nimport createJiti from \"jiti\";\n\nconst jiti = createJiti(import.meta.url);\nconst {\n  buildVersionMismatchBannerModel,\n  compareVersions,\n  summarizeVersionMismatches,\n} = await jiti.import(\n  \"../src/utils/version-mismatch.ts\",\n);\n\ntest(\"compareVersions detects exact matches, directional mismatches, and unknown cases\", () => {\n  assert.equal(compareVersions(\"0.23.2\", \"0.23.2\"), null);\n  assert.equal(compareVersions(\"0.23.0\", \"0.23.2\"), \"runtime_older\");\n  assert.equal(compareVersions(\"0.23.3\", \"0.23.2\"), \"cli_older\");\n  assert.equal(compareVersions(null, \"0.23.2\"), \"unknown\");\n  assert.equal(compareVersions(\"0.23.2-SNAPSHOT\", \"0.23.2\"), \"unknown\");\n});\n\ntest(\"summarizeVersionMismatches returns null when every latest app matches the CLI\", () => {\n  const summary = summarizeVersionMismatches(\n    [\n      {\n        app_name: \"alpha-api\",\n        latest_run_id: \"run-1\",\n        latest_status: \"PASSED\",\n        stove_version: \"0.23.2\",\n        total_runs: 1,\n      },\n    ],\n    \"0.23.2\",\n    \"alpha-api\",\n  );\n\n  assert.equal(summary, null);\n});\n\ntest(\"summarizeVersionMismatches captures selected-app mismatch and all affected apps\", () => {\n  const summary = summarizeVersionMismatches(\n    [\n      {\n        app_name: \"alpha-api\",\n        latest_run_id: \"run-1\",\n        latest_status: \"PASSED\",\n        stove_version: \"0.23.0\",\n        total_runs: 1,\n      },\n      {\n        app_name: \"beta-api\",\n        latest_run_id: \"run-2\",\n        latest_status: \"FAILED\",\n        stove_version: \"0.23.5\",\n        total_runs: 1,\n      },\n    ],\n    \"0.23.2\",\n    \"alpha-api\",\n  );\n\n  assert.equal(summary.cliVersion, \"0.23.2\");\n  assert.equal(summary.mismatches.length, 2);\n  assert.deepEqual(summary.affectedAppNames, [\"alpha-api\", \"beta-api\"]);\n  assert.equal(summary.selectedAppMismatch.appName, \"alpha-api\");\n  assert.equal(summary.selectedAppMismatch.kind, \"runtime_older\");\n});\n\ntest(\"buildVersionMismatchBannerModel returns dependency alignment guidance for older runtimes\", () => {\n  const model = buildVersionMismatchBannerModel({\n    cliVersion: \"0.23.2\",\n    mismatches: [\n      {\n        appName: \"alpha-api\",\n        cliVersion: \"0.23.2\",\n        runtimeVersion: \"0.23.0\",\n        kind: \"runtime_older\",\n      },\n    ],\n    affectedAppNames: [\"alpha-api\"],\n    selectedAppMismatch: {\n      appName: \"alpha-api\",\n      cliVersion: \"0.23.2\",\n      runtimeVersion: \"0.23.0\",\n      kind: \"runtime_older\",\n    },\n  });\n\n  assert.equal(model.selectedAppName, \"alpha-api\");\n  assert.deepEqual(model.remediationSteps, [\n    {\n      kind: \"text\",\n      value: \"Align the Stove BOM or all Stove test dependencies to 0.23.2.\",\n    },\n  ]);\n});\n\ntest(\"buildVersionMismatchBannerModel returns CLI upgrade commands when the runtime is newer\", () => {\n  const model = buildVersionMismatchBannerModel({\n    cliVersion: \"0.23.2\",\n    mismatches: [\n      {\n        appName: \"beta-api\",\n        cliVersion: \"0.23.2\",\n        runtimeVersion: \"0.23.5\",\n        kind: \"cli_older\",\n      },\n    ],\n    affectedAppNames: [\"beta-api\"],\n    selectedAppMismatch: {\n      appName: \"beta-api\",\n      cliVersion: \"0.23.2\",\n      runtimeVersion: \"0.23.5\",\n      kind: \"cli_older\",\n    },\n  });\n\n  assert.equal(model.remediationSteps[0].kind, \"text\");\n  assert.equal(model.remediationSteps[1].kind, \"command\");\n  assert.equal(model.remediationSteps[1].value, \"brew upgrade Trendyol/trendyol-tap/stove\");\n  assert.equal(\n    model.remediationSteps[2].value,\n    \"curl -fsSL https://raw.githubusercontent.com/Trendyol/stove/main/tools/stove-cli/install.sh | sh -s -- --version 0.23.5\",\n  );\n});\n\ntest(\"buildVersionMismatchBannerModel stays summary-only when another app mismatches\", () => {\n  const model = buildVersionMismatchBannerModel({\n    cliVersion: \"0.23.2\",\n    mismatches: [\n      {\n        appName: \"alpha-api\",\n        cliVersion: \"0.23.2\",\n        runtimeVersion: \"0.23.0\",\n        kind: \"runtime_older\",\n      },\n      {\n        appName: \"beta-api\",\n        cliVersion: \"0.23.2\",\n        runtimeVersion: \"0.23.5\",\n        kind: \"cli_older\",\n      },\n    ],\n    affectedAppNames: [\"alpha-api\", \"beta-api\"],\n    selectedAppMismatch: null,\n  });\n\n  assert.deepEqual(model.affectedApps, [\"alpha-api\", \"beta-api\"]);\n  assert.equal(model.switchHint, \"Switch to a mismatched app to see exact remediation.\");\n  assert.deepEqual(model.remediationSteps, []);\n});\n\ntest(\"buildVersionMismatchBannerModel returns legacy guidance for missing runtime versions\", () => {\n  const model = buildVersionMismatchBannerModel({\n    cliVersion: \"0.23.2\",\n    mismatches: [\n      {\n        appName: \"legacy-api\",\n        cliVersion: \"0.23.2\",\n        runtimeVersion: null,\n        kind: \"unknown\",\n      },\n    ],\n    affectedAppNames: [\"legacy-api\"],\n    selectedAppMismatch: {\n      appName: \"legacy-api\",\n      cliVersion: \"0.23.2\",\n      runtimeVersion: null,\n      kind: \"unknown\",\n    },\n  });\n\n  assert.equal(model.runtimeVersion, null);\n  assert.equal(model.remediationSteps[0].kind, \"text\");\n  assert.match(model.remediationSteps[0].value, /older or non-standard Stove runtime/);\n});\n"
  },
  {
    "path": "tools/stove-cli/spa/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedSideEffectImports\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "tools/stove-cli/spa/vite.config.ts",
    "content": "import react from \"@vitejs/plugin-react\";\nimport { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { defineConfig } from \"vite\";\n\nconst propsPath = resolve(import.meta.dirname, \"../../../gradle.properties\");\nconst version = readFileSync(propsPath, \"utf-8\").match(/^version=(.+)$/m)?.[1] ?? \"dev\";\n\nexport default defineConfig({\n  plugins: [react()],\n  define: {\n    __STOVE_VERSION__: JSON.stringify(version),\n  },\n  server: {\n    proxy: {\n      \"/api\": \"http://localhost:4040\",\n    },\n  },\n});\n"
  },
  {
    "path": "tools/stove-cli/src/config.rs",
    "content": "use std::path::Path;\n\nuse clap::{Parser, Subcommand};\n\n/// CLI configuration parsed from command-line arguments.\n#[derive(Parser, Debug)]\n#[command(\n  name = \"stove\",\n  about = \"Stove CLI \\u{2014} local e2e test observability\",\n  version = env!(\"STOVE_VERSION\")\n)]\n#[allow(clippy::struct_excessive_bools)] // CLI flags are naturally bool-heavy\npub struct Config {\n  /// HTTP port for the web UI and REST API\n  #[arg(long, default_value_t = 4040)]\n  pub port: u16,\n\n  /// gRPC port for receiving events from Stove test process\n  #[arg(long, default_value_t = 4041)]\n  pub grpc_port: u16,\n\n  /// Path to `SQLite` database file\n  #[arg(long, default_value_t = default_db_path())]\n  pub db: String,\n\n  /// Clear all stored runs and exit\n  #[arg(long)]\n  pub clear: bool,\n\n  /// Drop and recreate the database from scratch (backs up existing file first)\n  #[arg(long)]\n  pub fresh_start: bool,\n\n  /// Fetch and apply Stove agent skills from GitHub on startup without prompting.\n  /// Useful for automation inside repositories.\n  #[arg(long)]\n  pub update_skills: bool,\n\n  /// Skip the startup Stove agent skills check entirely.\n  #[arg(long)]\n  pub no_skills_check: bool,\n\n  /// Optional subcommand. When omitted, the CLI runs the dashboard.\n  #[command(subcommand)]\n  pub command: Option<StoveCommand>,\n}\n\n/// Top-level subcommands for the Stove CLI.\n#[derive(Subcommand, Debug)]\npub enum StoveCommand {\n  /// Manage Stove agent skills under the current project.\n  Skills {\n    #[command(subcommand)]\n    command: SkillsCommand,\n  },\n}\n\n/// `stove skills <...>` subcommands.\n#[derive(Subcommand, Debug)]\npub enum SkillsCommand {\n  /// Install or update Stove agent skills from GitHub.\n  Install {\n    /// Skip git repository detection and overwrite without prompting.\n    /// Installs into the resolved skill target relative to the current directory.\n    #[arg(long)]\n    force: bool,\n  },\n}\n\n/// If `--fresh-start` is set, backs up the existing database file and deletes the original.\n/// Returns `Ok(Some(backup_path))` if a backup was created, `Ok(None)` if no file existed.\n/// Skips in-memory databases.\npub fn handle_fresh_start(db_path: &str) -> std::io::Result<Option<String>> {\n  if db_path == \":memory:\" {\n    return Ok(None);\n  }\n\n  let path = Path::new(db_path);\n  if !path.exists() {\n    return Ok(None);\n  }\n\n  let timestamp = chrono::Local::now().format(\"%Y%m%d-%H%M%S\");\n  let backup_path = format!(\"{db_path}.backup-{timestamp}\");\n  std::fs::copy(path, &backup_path)?;\n  std::fs::remove_file(path)?;\n  Ok(Some(backup_path))\n}\n\n/// Returns the default database path in the user's home directory.\nfn default_db_path() -> String {\n  dirs_fallback()\n    .join(\".stove-dashboard.db\")\n    .to_string_lossy()\n    .to_string()\n}\n\n/// Best-effort home directory lookup without pulling in the `dirs` crate.\nfn dirs_fallback() -> std::path::PathBuf {\n  std::env::var(\"HOME\")\n    .or_else(|_| std::env::var(\"USERPROFILE\"))\n    .map_or_else(\n      |_| std::env::current_dir().unwrap_or_else(|_| \".\".into()),\n      std::path::PathBuf::from,\n    )\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n  use std::fs;\n  use tempfile::TempDir;\n\n  #[test]\n  fn fresh_start_backs_up_and_deletes_existing_db() {\n    let dir = TempDir::new().unwrap();\n    let db_path = dir.path().join(\"test.db\");\n    fs::write(&db_path, b\"some data\").unwrap();\n\n    let result = handle_fresh_start(db_path.to_str().unwrap()).unwrap();\n\n    assert!(result.is_some(), \"should return backup path\");\n    let backup_path = result.unwrap();\n    assert!(Path::new(&backup_path).exists(), \"backup file should exist\");\n    assert!(!db_path.exists(), \"original file should be deleted\");\n    assert_eq!(fs::read(&backup_path).unwrap(), b\"some data\");\n  }\n\n  #[test]\n  fn fresh_start_returns_none_when_file_does_not_exist() {\n    let dir = TempDir::new().unwrap();\n    let db_path = dir.path().join(\"nonexistent.db\");\n\n    let result = handle_fresh_start(db_path.to_str().unwrap()).unwrap();\n\n    assert!(result.is_none());\n  }\n\n  #[test]\n  fn fresh_start_skips_in_memory_database() {\n    let result = handle_fresh_start(\":memory:\").unwrap();\n\n    assert!(result.is_none());\n  }\n\n  #[test]\n  fn cli_parses_default_values() {\n    let config = Config::try_parse_from([\"stove\"]).unwrap();\n\n    assert_eq!(config.port, 4040);\n    assert_eq!(config.grpc_port, 4041);\n    assert!(!config.clear);\n    assert!(!config.fresh_start);\n  }\n\n  #[test]\n  fn cli_parses_custom_ports() {\n    let config =\n      Config::try_parse_from([\"stove\", \"--port\", \"8080\", \"--grpc-port\", \"9090\"]).unwrap();\n\n    assert_eq!(config.port, 8080);\n    assert_eq!(config.grpc_port, 9090);\n  }\n\n  #[test]\n  fn cli_parses_clear_flag() {\n    let config = Config::try_parse_from([\"stove\", \"--clear\"]).unwrap();\n\n    assert!(config.clear);\n  }\n\n  #[test]\n  fn cli_parses_fresh_start_flag() {\n    let config = Config::try_parse_from([\"stove\", \"--fresh-start\"]).unwrap();\n\n    assert!(config.fresh_start);\n  }\n\n  #[test]\n  fn cli_parses_custom_db_path() {\n    let config = Config::try_parse_from([\"stove\", \"--db\", \"/tmp/my.db\"]).unwrap();\n\n    assert_eq!(config.db, \"/tmp/my.db\");\n  }\n\n  #[test]\n  fn cli_defaults_skills_flags_off() {\n    let config = Config::try_parse_from([\"stove\"]).unwrap();\n    assert!(!config.update_skills);\n    assert!(!config.no_skills_check);\n    assert!(config.command.is_none());\n  }\n\n  #[test]\n  fn cli_parses_update_skills_flag() {\n    let config = Config::try_parse_from([\"stove\", \"--update-skills\"]).unwrap();\n    assert!(config.update_skills);\n  }\n\n  #[test]\n  fn cli_parses_no_skills_check_flag() {\n    let config = Config::try_parse_from([\"stove\", \"--no-skills-check\"]).unwrap();\n    assert!(config.no_skills_check);\n  }\n\n  #[test]\n  fn cli_parses_skills_install_subcommand() {\n    let config = Config::try_parse_from([\"stove\", \"skills\", \"install\"]).unwrap();\n    let Some(StoveCommand::Skills { command }) = config.command else {\n      panic!(\"expected skills subcommand\");\n    };\n    let SkillsCommand::Install { force } = command;\n    assert!(!force);\n  }\n\n  #[test]\n  fn cli_parses_skills_install_force() {\n    let config = Config::try_parse_from([\"stove\", \"skills\", \"install\", \"--force\"]).unwrap();\n    let Some(StoveCommand::Skills { command }) = config.command else {\n      panic!(\"expected skills subcommand\");\n    };\n    let SkillsCommand::Install { force } = command;\n    assert!(force);\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/error.rs",
    "content": "use thiserror::Error;\n\n/// Application-level error types.\n///\n/// Uses `thiserror` for typed, displayable errors in library-like code.\n/// `anyhow` is used only at the top level (`main.rs`) for ergonomic `?` usage.\n#[derive(Error, Debug)]\npub enum AppError {\n  #[error(\"Database error: {0}\")]\n  Database(#[from] rusqlite::Error),\n\n  #[error(\"gRPC transport error: {0}\")]\n  GrpcTransport(#[from] tonic::transport::Error),\n\n  #[error(\"Serialization error: {0}\")]\n  Serialization(#[from] serde_json::Error),\n\n  #[error(\"Invalid dashboard event: {0}\")]\n  InvalidEvent(String),\n\n  #[error(\"Server startup failed: {0}\")]\n  #[allow(dead_code)]\n  Startup(String),\n}\n\npub type Result<T> = std::result::Result<T, AppError>;\n\n/// Convert `AppError` into an axum-compatible HTTP response.\nimpl axum::response::IntoResponse for AppError {\n  fn into_response(self) -> axum::response::Response {\n    let status = match &self {\n      AppError::GrpcTransport(_) => axum::http::StatusCode::BAD_GATEWAY,\n      AppError::Serialization(_) | AppError::InvalidEvent(_) => axum::http::StatusCode::BAD_REQUEST,\n      AppError::Database(_) | AppError::Startup(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR,\n    };\n    let body = axum::Json(serde_json::json!({ \"error\": self.to_string() }));\n    (status, body).into_response()\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/grpc/mod.rs",
    "content": "pub mod service;\n"
  },
  {
    "path": "tools/stove-cli/src/grpc/service.rs",
    "content": "use std::collections::{HashMap, HashSet};\nuse std::sync::Arc;\nuse std::sync::Mutex;\nuse std::sync::atomic::{AtomicU64, Ordering};\nuse std::time::Duration;\n\nuse tonic::{Request, Response, Status, Streaming};\nuse tracing::warn;\n\nuse crate::error::{AppError, Result as AppResult};\nuse crate::ingest::{\n  DEFAULT_MAX_BATCH_DELAY, DEFAULT_MAX_BATCH_SIZE, EventIngestor, LiveDashboardEvent,\n  LiveDashboardPayload, LiveEntryRecordedPayload, LiveRunEndedPayload, LiveRunStartedPayload,\n  LiveSnapshotPayload, LiveSpanRecordedPayload, LiveTestEndedPayload, LiveTestStartedPayload,\n  PersistedDashboardEvent,\n};\nuse crate::proto;\nuse crate::sse::manager::SseManager;\nuse crate::storage::models::{NewEntry, NewSpan, RunStatus};\nuse crate::storage::repository::Repository;\n\nmod event_type {\n  pub const RUN_STARTED: &str = \"run_started\";\n  pub const RUN_ENDED: &str = \"run_ended\";\n  pub const TEST_STARTED: &str = \"test_started\";\n  pub const TEST_ENDED: &str = \"test_ended\";\n  pub const ENTRY_RECORDED: &str = \"entry_recorded\";\n  pub const SPAN_RECORDED: &str = \"span_recorded\";\n  pub const SNAPSHOT: &str = \"snapshot\";\n}\n\n/// gRPC service implementation that receives events from Stove test processes.\npub struct DashboardEventServiceImpl {\n  #[allow(dead_code)]\n  repository: Arc<Repository>,\n  ingestor: EventIngestor,\n  sse_manager: Arc<SseManager>,\n  next_live_seq: AtomicU64,\n  state: Mutex<LiveState>,\n}\n\nimpl DashboardEventServiceImpl {\n  #[must_use]\n  pub fn new(repository: Arc<Repository>, sse_manager: Arc<SseManager>) -> Self {\n    Self::new_with_ingest_config(\n      repository,\n      sse_manager,\n      DEFAULT_MAX_BATCH_SIZE,\n      DEFAULT_MAX_BATCH_DELAY,\n    )\n  }\n\n  #[must_use]\n  pub fn new_with_ingest_config(\n    repository: Arc<Repository>,\n    sse_manager: Arc<SseManager>,\n    max_batch_size: usize,\n    max_batch_delay: Duration,\n  ) -> Self {\n    let ingestor = EventIngestor::with_config(repository.clone(), max_batch_size, max_batch_delay);\n    Self::new_with_ingestor(repository, sse_manager, ingestor)\n  }\n\n  #[must_use]\n  pub fn new_with_ingestor(\n    repository: Arc<Repository>,\n    sse_manager: Arc<SseManager>,\n    ingestor: EventIngestor,\n  ) -> Self {\n    Self {\n      repository,\n      ingestor,\n      sse_manager,\n      next_live_seq: AtomicU64::new(0),\n      state: Mutex::new(LiveState::default()),\n    }\n  }\n\n  pub async fn flush_pending(&self) -> AppResult<()> {\n    self.ingestor.flush_pending().await\n  }\n\n  /// Queue persistence work and immediately broadcast the full event to SSE.\n  fn process_event(&self, event: &proto::DashboardEvent) -> std::result::Result<(), Status> {\n    let Some(prepared) = self.prepare_event(event).map_err(to_status)? else {\n      return Ok(());\n    };\n\n    self\n      .ingestor\n      .enqueue(prepared.persisted, prepared.flush_immediately)\n      .map_err(to_status)?;\n\n    let seq = self.next_live_seq.fetch_add(1, Ordering::Relaxed) + 1;\n    let live_event = prepared.live.with_seq(seq);\n    match serde_json::to_string(&live_event) {\n      Ok(json) => self.sse_manager.broadcast(&json),\n      Err(error) => warn!(%error, \"Failed to serialize live SSE event\"),\n    }\n\n    Ok(())\n  }\n\n  fn prepare_event(\n    &self,\n    event: &proto::DashboardEvent,\n  ) -> AppResult<Option<PreparedDashboardEvent>> {\n    let Some(inner_event) = &event.event else {\n      warn!(\"Received DashboardEvent with no event payload\");\n      return Ok(None);\n    };\n\n    let mut state = self\n      .state\n      .lock()\n      .expect(\"dashboard live state lock poisoned\");\n    let prepared = match inner_event {\n      proto::dashboard_event::Event::RunStarted(inner) => {\n        Ok(Self::prepare_run_started(&mut state, &event.run_id, inner))\n      }\n      proto::dashboard_event::Event::RunEnded(inner) => {\n        Self::prepare_run_ended(&mut state, &event.run_id, inner)\n      }\n      proto::dashboard_event::Event::TestStarted(inner) => {\n        Self::prepare_test_started(&mut state, &event.run_id, inner)\n      }\n      proto::dashboard_event::Event::TestEnded(inner) => {\n        Self::prepare_test_ended(&mut state, &event.run_id, inner)\n      }\n      proto::dashboard_event::Event::EntryRecorded(inner) => {\n        Self::prepare_entry_recorded(&mut state, &event.run_id, inner)\n      }\n      proto::dashboard_event::Event::SpanRecorded(inner) => {\n        Self::prepare_span_recorded(&mut state, &event.run_id, inner)\n      }\n      proto::dashboard_event::Event::Snapshot(inner) => {\n        Self::prepare_snapshot(&mut state, &event.run_id, inner)\n      }\n    }?;\n\n    Ok(Some(prepared))\n  }\n\n  fn prepare_run_started(\n    state: &mut LiveState,\n    run_id: &str,\n    event: &proto::RunStartedEvent,\n  ) -> PreparedDashboardEvent {\n    let started_at = format_timestamp(event.timestamp.as_ref());\n    state.runs.insert(run_id.to_string());\n    PreparedDashboardEvent {\n      live: Self::live_event(\n        run_id,\n        event_type::RUN_STARTED,\n        LiveDashboardPayload::RunStarted(LiveRunStartedPayload {\n          app_name: event.app_name.clone(),\n          started_at: started_at.clone(),\n          stove_version: non_empty(&event.stove_version),\n          systems: event.systems.clone(),\n        }),\n      ),\n      persisted: PersistedDashboardEvent::RunStarted {\n        run_id: run_id.to_string(),\n        app_name: event.app_name.clone(),\n        started_at,\n        stove_version: non_empty(&event.stove_version),\n        systems: event.systems.clone(),\n      },\n      flush_immediately: false,\n    }\n  }\n\n  fn prepare_run_ended(\n    state: &mut LiveState,\n    run_id: &str,\n    event: &proto::RunEndedEvent,\n  ) -> AppResult<PreparedDashboardEvent> {\n    ensure_run_known(state, run_id)?;\n    let ended_at = format_timestamp(event.timestamp.as_ref());\n    let status = run_status(event.failed).to_string();\n    state.clear_run(run_id);\n    Ok(PreparedDashboardEvent {\n      live: Self::live_event(\n        run_id,\n        event_type::RUN_ENDED,\n        LiveDashboardPayload::RunEnded(LiveRunEndedPayload {\n          ended_at: ended_at.clone(),\n          status,\n          total_tests: event.total_tests,\n          passed: event.passed,\n          failed: event.failed,\n          duration_ms: event.duration_ms,\n        }),\n      ),\n      persisted: PersistedDashboardEvent::RunEnded {\n        run_id: run_id.to_string(),\n        ended_at,\n        total_tests: event.total_tests,\n        passed: event.passed,\n        failed: event.failed,\n        duration_ms: event.duration_ms,\n      },\n      flush_immediately: true,\n    })\n  }\n\n  fn prepare_test_started(\n    state: &mut LiveState,\n    run_id: &str,\n    event: &proto::TestStartedEvent,\n  ) -> AppResult<PreparedDashboardEvent> {\n    ensure_run_known(state, run_id)?;\n    let started_at = format_timestamp(event.timestamp.as_ref());\n    state\n      .tests\n      .insert((run_id.to_string(), event.test_id.clone()));\n    Ok(PreparedDashboardEvent {\n      live: Self::live_event(\n        run_id,\n        event_type::TEST_STARTED,\n        LiveDashboardPayload::TestStarted(LiveTestStartedPayload {\n          test_id: event.test_id.clone(),\n          test_name: event.test_name.clone(),\n          spec_name: event.spec_name.clone(),\n          test_path: event.test_path.clone(),\n          started_at: started_at.clone(),\n          status: \"RUNNING\".to_string(),\n        }),\n      ),\n      persisted: PersistedDashboardEvent::TestStarted {\n        run_id: run_id.to_string(),\n        test_id: event.test_id.clone(),\n        test_name: event.test_name.clone(),\n        spec_name: event.spec_name.clone(),\n        test_path: event.test_path.clone(),\n        started_at,\n      },\n      flush_immediately: false,\n    })\n  }\n\n  fn prepare_test_ended(\n    state: &mut LiveState,\n    run_id: &str,\n    event: &proto::TestEndedEvent,\n  ) -> AppResult<PreparedDashboardEvent> {\n    ensure_test_known(state, run_id, &event.test_id)?;\n    let ended_at = format_timestamp(event.timestamp.as_ref());\n    Ok(PreparedDashboardEvent {\n      live: Self::live_event(\n        run_id,\n        event_type::TEST_ENDED,\n        LiveDashboardPayload::TestEnded(LiveTestEndedPayload {\n          test_id: event.test_id.clone(),\n          status: event.status.clone(),\n          duration_ms: event.duration_ms,\n          error: non_empty(&event.error),\n          ended_at: ended_at.clone(),\n        }),\n      ),\n      persisted: PersistedDashboardEvent::TestEnded {\n        run_id: run_id.to_string(),\n        test_id: event.test_id.clone(),\n        status: event.status.clone(),\n        duration_ms: event.duration_ms,\n        error: non_empty(&event.error),\n        ended_at,\n      },\n      flush_immediately: false,\n    })\n  }\n\n  fn prepare_entry_recorded(\n    state: &mut LiveState,\n    run_id: &str,\n    event: &proto::EntryRecordedEvent,\n  ) -> AppResult<PreparedDashboardEvent> {\n    ensure_test_known(state, run_id, &event.test_id)?;\n    if !event.trace_id.is_empty() {\n      state.traces.insert(\n        (run_id.to_string(), event.trace_id.clone()),\n        event.test_id.clone(),\n      );\n    }\n\n    let metadata = serde_json::to_string(&event.metadata)?;\n    let timestamp = format_timestamp(event.timestamp.as_ref());\n    let entry = NewEntry {\n      run_id: run_id.to_string(),\n      test_id: event.test_id.clone(),\n      timestamp: timestamp.clone(),\n      system: event.system.clone(),\n      action: event.action.clone(),\n      result: event.result.clone(),\n      input: event.input.clone(),\n      output: event.output.clone(),\n      metadata: metadata.clone(),\n      expected: event.expected.clone(),\n      actual: event.actual.clone(),\n      error: event.error.clone(),\n      trace_id: event.trace_id.clone(),\n    };\n\n    Ok(PreparedDashboardEvent {\n      live: Self::live_event(\n        run_id,\n        event_type::ENTRY_RECORDED,\n        LiveDashboardPayload::EntryRecorded(LiveEntryRecordedPayload {\n          id: 0,\n          test_id: event.test_id.clone(),\n          timestamp,\n          system: event.system.clone(),\n          action: event.action.clone(),\n          result: event.result.clone(),\n          input: non_empty(&event.input),\n          output: non_empty(&event.output),\n          metadata: non_empty(&metadata),\n          expected: non_empty(&event.expected),\n          actual: non_empty(&event.actual),\n          error: non_empty(&event.error),\n          trace_id: non_empty(&event.trace_id),\n        }),\n      ),\n      persisted: PersistedDashboardEvent::EntryRecorded(entry),\n      flush_immediately: false,\n    })\n  }\n\n  fn prepare_span_recorded(\n    state: &mut LiveState,\n    run_id: &str,\n    event: &proto::SpanRecordedEvent,\n  ) -> AppResult<PreparedDashboardEvent> {\n    ensure_run_known(state, run_id)?;\n    let test_id = extract_test_id(&event.attributes).or_else(|| {\n      state\n        .traces\n        .get(&(run_id.to_string(), event.trace_id.clone()))\n        .cloned()\n    });\n\n    let attributes = serde_json::to_string(&event.attributes)?;\n    let (exception_type, exception_message, exception_stack_trace) = event\n      .exception\n      .as_ref()\n      .map(|exception| {\n        (\n          exception.r#type.clone(),\n          exception.message.clone(),\n          exception.stack_trace.join(\"\\n\"),\n        )\n      })\n      .unwrap_or_default();\n\n    let span = NewSpan {\n      run_id: run_id.to_string(),\n      trace_id: event.trace_id.clone(),\n      span_id: event.span_id.clone(),\n      parent_span_id: event.parent_span_id.clone(),\n      operation_name: event.operation_name.clone(),\n      service_name: event.service_name.clone(),\n      start_time_nanos: event.start_time_nanos,\n      end_time_nanos: event.end_time_nanos,\n      status: event.status.clone(),\n      attributes: attributes.clone(),\n      exception_type: exception_type.clone(),\n      exception_message: exception_message.clone(),\n      exception_stack_trace: exception_stack_trace.clone(),\n    };\n\n    Ok(PreparedDashboardEvent {\n      live: Self::live_event(\n        run_id,\n        event_type::SPAN_RECORDED,\n        LiveDashboardPayload::SpanRecorded(LiveSpanRecordedPayload {\n          id: 0,\n          test_id,\n          trace_id: event.trace_id.clone(),\n          span_id: event.span_id.clone(),\n          parent_span_id: non_empty(&event.parent_span_id),\n          operation_name: event.operation_name.clone(),\n          service_name: event.service_name.clone(),\n          start_time_nanos: event.start_time_nanos,\n          end_time_nanos: event.end_time_nanos,\n          status: event.status.clone(),\n          attributes: non_empty(&attributes),\n          exception_type: non_empty(&exception_type),\n          exception_message: non_empty(&exception_message),\n          exception_stack_trace: non_empty(&exception_stack_trace),\n        }),\n      ),\n      persisted: PersistedDashboardEvent::SpanRecorded(span),\n      flush_immediately: false,\n    })\n  }\n\n  fn prepare_snapshot(\n    state: &mut LiveState,\n    run_id: &str,\n    event: &proto::SnapshotEvent,\n  ) -> AppResult<PreparedDashboardEvent> {\n    ensure_test_known(state, run_id, &event.test_id)?;\n    Ok(PreparedDashboardEvent {\n      live: Self::live_event(\n        run_id,\n        event_type::SNAPSHOT,\n        LiveDashboardPayload::Snapshot(LiveSnapshotPayload {\n          id: 0,\n          test_id: event.test_id.clone(),\n          system: event.system.clone(),\n          state_json: event.state_json.clone(),\n          summary: event.summary.clone(),\n        }),\n      ),\n      persisted: PersistedDashboardEvent::Snapshot {\n        run_id: run_id.to_string(),\n        test_id: event.test_id.clone(),\n        system: event.system.clone(),\n        state_json: event.state_json.clone(),\n        summary: event.summary.clone(),\n      },\n      flush_immediately: false,\n    })\n  }\n\n  fn live_event(\n    run_id: &str,\n    event_type: &str,\n    payload: LiveDashboardPayload,\n  ) -> LiveDashboardEvent {\n    LiveDashboardEvent {\n      seq: 0,\n      run_id: run_id.to_string(),\n      event_type: event_type.to_string(),\n      payload,\n    }\n  }\n}\n\n#[tonic::async_trait]\nimpl proto::dashboard_event_service_server::DashboardEventService for DashboardEventServiceImpl {\n  async fn stream_events(\n    &self,\n    request: Request<Streaming<proto::DashboardEvent>>,\n  ) -> std::result::Result<Response<proto::EventAck>, Status> {\n    let mut stream = request.into_inner();\n    while let Some(event) = stream.message().await? {\n      self.process_event(&event)?;\n    }\n    Ok(Response::new(proto::EventAck { accepted: true }))\n  }\n\n  async fn send_event(\n    &self,\n    request: Request<proto::DashboardEvent>,\n  ) -> std::result::Result<Response<proto::EventAck>, Status> {\n    self.process_event(&request.into_inner())?;\n    Ok(Response::new(proto::EventAck { accepted: true }))\n  }\n}\n\nstruct PreparedDashboardEvent {\n  live: LiveDashboardEvent,\n  persisted: PersistedDashboardEvent,\n  flush_immediately: bool,\n}\n\n#[derive(Default)]\nstruct LiveState {\n  runs: HashSet<String>,\n  tests: HashSet<(String, String)>,\n  traces: HashMap<(String, String), String>,\n}\n\nimpl LiveState {\n  fn clear_run(&mut self, run_id: &str) {\n    self.runs.remove(run_id);\n    self\n      .tests\n      .retain(|(known_run_id, _)| known_run_id != run_id);\n    self\n      .traces\n      .retain(|(known_run_id, _), _| known_run_id != run_id);\n  }\n}\n\nfn ensure_run_known(state: &LiveState, run_id: &str) -> AppResult<()> {\n  if state.runs.contains(run_id) {\n    Ok(())\n  } else {\n    Err(AppError::InvalidEvent(format!(\n      \"received event for unknown run `{run_id}`\"\n    )))\n  }\n}\n\nfn ensure_test_known(state: &LiveState, run_id: &str, test_id: &str) -> AppResult<()> {\n  ensure_run_known(state, run_id)?;\n  if state\n    .tests\n    .contains(&(run_id.to_string(), test_id.to_string()))\n  {\n    Ok(())\n  } else {\n    Err(AppError::InvalidEvent(format!(\n      \"received event for unknown test `{test_id}` in run `{run_id}`\"\n    )))\n  }\n}\n\nfn extract_test_id(attributes: &HashMap<String, String>) -> Option<String> {\n  [\n    \"x-stove-test-id\",\n    \"X-Stove-Test-Id\",\n    \"stove.test.id\",\n    \"stove_test_id\",\n  ]\n  .iter()\n  .find_map(|key| attributes.get(*key))\n  .cloned()\n}\n\nfn run_status(failed: i32) -> RunStatus {\n  if failed > 0 {\n    RunStatus::Failed\n  } else {\n    RunStatus::Passed\n  }\n}\n\nfn format_timestamp(ts: Option<&prost_types::Timestamp>) -> String {\n  ts.map(|timestamp| {\n    #[allow(clippy::cast_sign_loss)]\n    chrono::DateTime::from_timestamp(timestamp.seconds, timestamp.nanos as u32)\n      .map(|datetime| datetime.to_rfc3339())\n      .unwrap_or_default()\n  })\n  .unwrap_or_default()\n}\n\nfn non_empty(value: &str) -> Option<String> {\n  if value.is_empty() {\n    None\n  } else {\n    Some(value.to_string())\n  }\n}\n\n#[allow(clippy::needless_pass_by_value)]\nfn to_status(error: AppError) -> Status {\n  match error {\n    AppError::InvalidEvent(message) => Status::invalid_argument(message),\n    other => Status::internal(other.to_string()),\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n  use crate::storage::database::Database;\n\n  fn test_service() -> DashboardEventServiceImpl {\n    let db = Database::open(\":memory:\").unwrap();\n    let repo = Arc::new(Repository::new(db));\n    let sse = Arc::new(SseManager::new());\n    DashboardEventServiceImpl::new_with_ingest_config(repo, sse, 50, Duration::from_secs(60))\n  }\n\n  fn ts(seconds: i64) -> Option<prost_types::Timestamp> {\n    Some(prost_types::Timestamp { seconds, nanos: 0 })\n  }\n\n  #[tokio::test]\n  async fn no_broadcast_on_invalid_event_order() {\n    let svc = test_service();\n    let mut rx = svc.sse_manager.subscribe();\n\n    let result = svc.process_event(&proto::DashboardEvent {\n      run_id: \"nonexistent-run\".to_string(),\n      event: Some(proto::dashboard_event::Event::TestStarted(\n        proto::TestStartedEvent {\n          test_id: \"t-1\".to_string(),\n          test_name: \"orphan test\".to_string(),\n          spec_name: \"Spec\".to_string(),\n          timestamp: ts(1_704_067_200),\n          test_path: vec![],\n        },\n      )),\n    });\n\n    assert!(result.is_err(), \"invalid event ordering should be rejected\");\n    assert!(\n      rx.try_recv().is_err(),\n      \"invalid events must not be broadcast\"\n    );\n    assert!(svc.repository.get_runs(None).unwrap().is_empty());\n    svc.flush_pending().await.unwrap();\n    assert!(svc.repository.get_runs(None).unwrap().is_empty());\n  }\n\n  #[tokio::test]\n  async fn broadcast_fires_before_batch_flush() {\n    let svc = test_service();\n    let mut rx = svc.sse_manager.subscribe();\n\n    svc\n      .process_event(&proto::DashboardEvent {\n        run_id: \"run-1\".to_string(),\n        event: Some(proto::dashboard_event::Event::RunStarted(\n          proto::RunStartedEvent {\n            timestamp: ts(1_704_067_200),\n            app_name: \"my-api\".to_string(),\n            systems: vec![\"HTTP\".to_string()],\n            stove_version: \"0.23.1\".to_string(),\n          },\n        )),\n      })\n      .unwrap();\n\n    let msg = rx.try_recv().expect(\"broadcast should be sent on success\");\n    assert!(msg.contains(\"run_started\"));\n    assert!(\n      svc.repository.get_runs(None).unwrap().is_empty(),\n      \"run should not be visible in SQLite before an explicit flush\"\n    );\n\n    svc.flush_pending().await.unwrap();\n\n    let runs = svc.repository.get_runs(None).unwrap();\n    assert_eq!(runs.len(), 1);\n  }\n\n  #[tokio::test]\n  async fn process_run_started_event() {\n    let svc = test_service();\n    let event = proto::DashboardEvent {\n      run_id: \"run-1\".to_string(),\n      event: Some(proto::dashboard_event::Event::RunStarted(\n        proto::RunStartedEvent {\n          timestamp: Some(prost_types::Timestamp {\n            seconds: 1_704_067_200,\n            nanos: 0,\n          }),\n          app_name: \"product-api\".to_string(),\n          systems: vec![\"HTTP\".to_string(), \"Kafka\".to_string()],\n          stove_version: \"0.23.2\".to_string(),\n        },\n      )),\n    };\n\n    svc.process_event(&event).unwrap();\n    svc.flush_pending().await.unwrap();\n\n    let runs = svc.repository.get_runs(None).unwrap();\n    assert_eq!(runs.len(), 1);\n    assert_eq!(runs[0].app_name, \"product-api\");\n    assert_eq!(runs[0].stove_version.as_deref(), Some(\"0.23.2\"));\n  }\n\n  #[tokio::test]\n  async fn process_full_lifecycle() {\n    let svc = test_service();\n\n    svc\n      .process_event(&proto::DashboardEvent {\n        run_id: \"run-1\".to_string(),\n        event: Some(proto::dashboard_event::Event::RunStarted(\n          proto::RunStartedEvent {\n            timestamp: Some(prost_types::Timestamp {\n              seconds: 1_704_067_200,\n              nanos: 0,\n            }),\n            app_name: \"test-app\".to_string(),\n            stove_version: String::new(),\n            systems: vec![],\n          },\n        )),\n      })\n      .unwrap();\n\n    svc\n      .process_event(&proto::DashboardEvent {\n        run_id: \"run-1\".to_string(),\n        event: Some(proto::dashboard_event::Event::TestStarted(\n          proto::TestStartedEvent {\n            test_id: \"test-1\".to_string(),\n            test_name: \"my test\".to_string(),\n            spec_name: \"MySpec\".to_string(),\n            timestamp: Some(prost_types::Timestamp {\n              seconds: 1_704_067_201,\n              nanos: 0,\n            }),\n            test_path: vec![],\n          },\n        )),\n      })\n      .unwrap();\n\n    svc\n      .process_event(&proto::DashboardEvent {\n        run_id: \"run-1\".to_string(),\n        event: Some(proto::dashboard_event::Event::EntryRecorded(\n          proto::EntryRecordedEvent {\n            test_id: \"test-1\".to_string(),\n            timestamp: Some(prost_types::Timestamp {\n              seconds: 1_704_067_202,\n              nanos: 0,\n            }),\n            system: \"HTTP\".to_string(),\n            action: \"GET /api\".to_string(),\n            result: \"PASSED\".to_string(),\n            input: String::new(),\n            output: String::new(),\n            metadata: std::collections::HashMap::default(),\n            expected: String::new(),\n            actual: String::new(),\n            error: String::new(),\n            trace_id: String::new(),\n          },\n        )),\n      })\n      .unwrap();\n\n    svc\n      .process_event(&proto::DashboardEvent {\n        run_id: \"run-1\".to_string(),\n        event: Some(proto::dashboard_event::Event::TestEnded(\n          proto::TestEndedEvent {\n            test_id: \"test-1\".to_string(),\n            status: \"PASSED\".to_string(),\n            duration_ms: 500,\n            error: String::new(),\n            timestamp: Some(prost_types::Timestamp {\n              seconds: 1_704_067_203,\n              nanos: 0,\n            }),\n          },\n        )),\n      })\n      .unwrap();\n\n    svc\n      .process_event(&proto::DashboardEvent {\n        run_id: \"run-1\".to_string(),\n        event: Some(proto::dashboard_event::Event::RunEnded(\n          proto::RunEndedEvent {\n            timestamp: Some(prost_types::Timestamp {\n              seconds: 1_704_067_210,\n              nanos: 0,\n            }),\n            total_tests: 1,\n            passed: 1,\n            failed: 0,\n            duration_ms: 10000,\n          },\n        )),\n      })\n      .unwrap();\n\n    svc.flush_pending().await.unwrap();\n\n    let runs = svc.repository.get_runs(None).unwrap();\n    assert_eq!(runs.len(), 1);\n    assert_eq!(runs[0].status, crate::storage::models::RunStatus::Passed);\n\n    let tests = svc.repository.get_tests_for_run(\"run-1\").unwrap();\n    assert_eq!(tests.len(), 1);\n    assert_eq!(tests[0].status, crate::storage::models::TestStatus::Passed);\n\n    let entries = svc.repository.get_entries(\"run-1\", \"test-1\").unwrap();\n    assert_eq!(entries.len(), 1);\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/http/mod.rs",
    "content": "pub mod routes;\npub mod server;\n"
  },
  {
    "path": "tools/stove-cli/src/http/routes/meta.rs",
    "content": "use axum::Json;\nuse axum::http::HeaderMap;\nuse axum::http::header::HOST;\nuse serde::Serialize;\n\nuse crate::STOVE_CLI_VERSION;\n\n#[derive(Serialize)]\npub struct MetaResponse {\n  pub stove_cli_version: &'static str,\n  pub mcp: McpMeta,\n}\n\n#[derive(Serialize)]\npub struct McpMeta {\n  pub enabled: bool,\n  pub transport: &'static str,\n  pub endpoint: String,\n  pub scope: &'static str,\n}\n\npub async fn get_meta(headers: HeaderMap) -> Json<MetaResponse> {\n  Json(MetaResponse {\n    stove_cli_version: STOVE_CLI_VERSION,\n    mcp: McpMeta {\n      enabled: true,\n      transport: \"streamable-http\",\n      endpoint: mcp_endpoint(&headers),\n      scope: \"read-only-test-observability\",\n    },\n  })\n}\n\nfn mcp_endpoint(headers: &HeaderMap) -> String {\n  headers\n    .get(HOST)\n    .and_then(|value| value.to_str().ok())\n    .filter(|host| !host.trim().is_empty())\n    .map_or_else(|| \"/mcp\".to_string(), |host| format!(\"http://{host}/mcp\"))\n}\n"
  },
  {
    "path": "tools/stove-cli/src/http/routes/mod.rs",
    "content": "mod meta;\nmod runs;\nmod sse;\nmod static_files;\nmod tests;\nmod traces;\n\npub use meta::*;\npub use runs::*;\npub use sse::*;\npub use static_files::*;\npub use tests::*;\npub use traces::*;\n"
  },
  {
    "path": "tools/stove-cli/src/http/routes/runs.rs",
    "content": "use axum::Json;\nuse axum::extract::{Path, Query, State};\nuse serde::Deserialize;\n\nuse crate::http::server::AppState;\nuse crate::storage::models::{AppSummary, Run};\n\n#[derive(Deserialize)]\npub struct RunsQuery {\n  pub app: Option<String>,\n}\n\npub async fn get_apps(\n  State(state): State<AppState>,\n) -> Result<Json<Vec<AppSummary>>, crate::error::AppError> {\n  let apps = state.repository.get_apps()?;\n  Ok(Json(apps))\n}\n\npub async fn get_runs(\n  State(state): State<AppState>,\n  Query(query): Query<RunsQuery>,\n) -> Result<Json<Vec<Run>>, crate::error::AppError> {\n  let runs = state.repository.get_runs(query.app.as_deref())?;\n  Ok(Json(runs))\n}\n\npub async fn get_run(\n  State(state): State<AppState>,\n  Path(run_id): Path<String>,\n) -> Result<Json<Option<Run>>, crate::error::AppError> {\n  let run = state.repository.get_run(&run_id)?;\n  Ok(Json(run))\n}\n\npub async fn clear_all(\n  State(state): State<AppState>,\n) -> Result<Json<serde_json::Value>, crate::error::AppError> {\n  state.repository.clear_all()?;\n  Ok(Json(serde_json::json!({ \"cleared\": true })))\n}\n"
  },
  {
    "path": "tools/stove-cli/src/http/routes/sse.rs",
    "content": "use std::convert::Infallible;\nuse std::time::Duration;\n\nuse axum::extract::State;\nuse axum::response::sse::{Event, KeepAlive, Sse};\nuse tokio_stream::StreamExt;\nuse tokio_stream::wrappers::BroadcastStream;\n\nuse crate::http::server::AppState;\n\n/// SSE endpoint that streams dashboard events to connected browser clients.\n///\n/// Sends a keep-alive comment every 15 seconds to prevent proxies and browsers\n/// from closing the connection during long-running tests.\npub async fn sse_handler(\n  State(state): State<AppState>,\n) -> Sse<impl tokio_stream::Stream<Item = Result<Event, Infallible>>> {\n  let rx = state.sse_manager.subscribe();\n  let stream = BroadcastStream::new(rx)\n    .filter_map(|result| result.ok().map(|data| Ok(Event::default().data(data))));\n  Sse::new(stream).keep_alive(\n    KeepAlive::new()\n      .interval(Duration::from_secs(15))\n      .text(\"keep-alive\"),\n  )\n}\n"
  },
  {
    "path": "tools/stove-cli/src/http/routes/static_files.rs",
    "content": "use axum::http::{StatusCode, Uri, header};\nuse axum::response::{IntoResponse, Response};\nuse rust_embed::Embed;\n\n/// Embedded SPA assets, baked into the binary at compile time.\n///\n/// In development, this folder may be empty — the SPA is served by Vite's dev server instead.\n#[derive(Embed)]\n#[folder = \"spa/dist/\"]\n#[allow(dead_code)]\nstruct SpaAssets;\n\n/// Serve embedded SPA files, falling back to `index.html` for client-side routing.\npub async fn static_handler(uri: Uri) -> Response {\n  let path = uri.path().trim_start_matches('/');\n\n  // Try exact file match first\n  if let Some(file) = SpaAssets::get(path) {\n    let mime = mime_guess::from_path(path).first_or_octet_stream();\n    return ([(header::CONTENT_TYPE, mime.as_ref())], file.data).into_response();\n  }\n\n  if is_asset_like_path(path) {\n    return (StatusCode::NOT_FOUND, \"Asset not found\").into_response();\n  }\n\n  // Fallback to index.html for SPA client-side routing\n  match SpaAssets::get(\"index.html\") {\n    Some(file) => ([(header::CONTENT_TYPE, \"text/html\")], file.data).into_response(),\n    None => (\n      StatusCode::NOT_FOUND,\n      \"SPA not built. Run: cd spa && npm run build\",\n    )\n      .into_response(),\n  }\n}\n\nfn is_asset_like_path(path: &str) -> bool {\n  !path.is_empty() && std::path::Path::new(path).extension().is_some()\n}\n\n#[cfg(test)]\nmod tests {\n  use super::is_asset_like_path;\n\n  #[test]\n  fn detects_asset_like_paths_by_extension() {\n    assert!(is_asset_like_path(\"assets/app.js\"));\n    assert!(is_asset_like_path(\"styles/main.css\"));\n    assert!(!is_asset_like_path(\"\"));\n    assert!(!is_asset_like_path(\"runs/run-1\"));\n    assert!(!is_asset_like_path(\"dashboard/settings\"));\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/http/routes/tests.rs",
    "content": "use axum::Json;\nuse axum::extract::{Path, State};\n\nuse crate::http::server::AppState;\nuse crate::storage::models::{Entry, Snapshot, Test};\n\npub async fn get_tests(\n  State(state): State<AppState>,\n  Path(run_id): Path<String>,\n) -> Result<Json<Vec<Test>>, crate::error::AppError> {\n  let tests = state.repository.get_tests_for_run(&run_id)?;\n  Ok(Json(tests))\n}\n\npub async fn get_entries(\n  State(state): State<AppState>,\n  Path((run_id, test_id)): Path<(String, String)>,\n) -> Result<Json<Vec<Entry>>, crate::error::AppError> {\n  let entries = state.repository.get_entries(&run_id, &test_id)?;\n  Ok(Json(entries))\n}\n\npub async fn get_snapshots(\n  State(state): State<AppState>,\n  Path((run_id, test_id)): Path<(String, String)>,\n) -> Result<Json<Vec<Snapshot>>, crate::error::AppError> {\n  let snapshots = state.repository.get_snapshots(&run_id, &test_id)?;\n  Ok(Json(snapshots))\n}\n\npub async fn get_test_spans(\n  State(state): State<AppState>,\n  Path((run_id, test_id)): Path<(String, String)>,\n) -> Result<Json<Vec<crate::storage::models::Span>>, crate::error::AppError> {\n  let spans = state.repository.get_spans_for_test(&run_id, &test_id)?;\n  Ok(Json(spans))\n}\n"
  },
  {
    "path": "tools/stove-cli/src/http/routes/traces.rs",
    "content": "use axum::Json;\nuse axum::extract::{Path, State};\n\nuse crate::http::server::AppState;\nuse crate::storage::models::Span;\n\npub async fn get_trace(\n  State(state): State<AppState>,\n  Path(trace_id): Path<String>,\n) -> Result<Json<Vec<Span>>, crate::error::AppError> {\n  let spans = state.repository.get_trace(&trace_id)?;\n  Ok(Json(spans))\n}\n"
  },
  {
    "path": "tools/stove-cli/src/http/server.rs",
    "content": "use std::sync::Arc;\n\nuse axum::Router;\nuse axum::routing::{delete, get};\nuse tower_http::cors::CorsLayer;\n\nuse crate::ingest::EventIngestor;\nuse crate::sse::manager::SseManager;\nuse crate::storage::repository::Repository;\n\n/// Shared application state passed to all HTTP handlers.\n#[derive(Clone)]\npub struct AppState {\n  pub repository: Arc<Repository>,\n  pub sse_manager: Arc<SseManager>,\n  pub ingestor: Option<EventIngestor>,\n}\n\n/// Create the axum router with all API routes, SSE, and embedded SPA.\npub fn create_router(repository: Arc<Repository>, sse_manager: Arc<SseManager>) -> Router {\n  create_router_with_ingestor(repository, sse_manager, None)\n}\n\n/// Create the axum router with an optional ingest flush handle for MCP reads.\npub fn create_router_with_ingestor(\n  repository: Arc<Repository>,\n  sse_manager: Arc<SseManager>,\n  ingestor: Option<EventIngestor>,\n) -> Router {\n  let state = AppState {\n    repository,\n    sse_manager,\n    ingestor,\n  };\n\n  let api = Router::new()\n    .route(\"/meta\", get(super::routes::get_meta))\n    .route(\"/apps\", get(super::routes::get_apps))\n    .route(\"/runs\", get(super::routes::get_runs))\n    .route(\"/runs/{run_id}\", get(super::routes::get_run))\n    .route(\"/runs/{run_id}/tests\", get(super::routes::get_tests))\n    .route(\n      \"/runs/{run_id}/tests/{test_id}/entries\",\n      get(super::routes::get_entries),\n    )\n    .route(\n      \"/runs/{run_id}/tests/{test_id}/spans\",\n      get(super::routes::get_test_spans),\n    )\n    .route(\n      \"/runs/{run_id}/tests/{test_id}/snapshots\",\n      get(super::routes::get_snapshots),\n    )\n    .route(\"/traces/{trace_id}\", get(super::routes::get_trace))\n    .route(\"/events/stream\", get(super::routes::sse_handler))\n    .route(\"/data\", delete(super::routes::clear_all));\n\n  Router::new()\n    .route(\n      \"/mcp\",\n      get(crate::mcp::handle_get).post(crate::mcp::handle_post),\n    )\n    .nest(\"/api/v1\", api)\n    .fallback(super::routes::static_handler)\n    .layer(CorsLayer::permissive())\n    .with_state(state)\n}\n"
  },
  {
    "path": "tools/stove-cli/src/ingest.rs",
    "content": "use std::sync::Arc;\nuse std::time::Duration;\n\nuse serde::Serialize;\nuse tokio::sync::{mpsc, oneshot};\nuse tracing::warn;\n\nuse crate::error::{AppError, Result};\nuse crate::storage::models::{NewEntry, NewSpan};\nuse crate::storage::repository::Repository;\n\npub const DEFAULT_MAX_BATCH_SIZE: usize = 20;\npub const DEFAULT_MAX_BATCH_DELAY: Duration = Duration::from_secs(5);\n\n#[derive(Clone, Debug)]\npub enum PersistedDashboardEvent {\n  RunStarted {\n    run_id: String,\n    app_name: String,\n    started_at: String,\n    stove_version: Option<String>,\n    systems: Vec<String>,\n  },\n  RunEnded {\n    run_id: String,\n    ended_at: String,\n    total_tests: i32,\n    passed: i32,\n    failed: i32,\n    duration_ms: i64,\n  },\n  TestStarted {\n    run_id: String,\n    test_id: String,\n    test_name: String,\n    spec_name: String,\n    test_path: Vec<String>,\n    started_at: String,\n  },\n  TestEnded {\n    run_id: String,\n    test_id: String,\n    status: String,\n    duration_ms: i64,\n    error: Option<String>,\n    ended_at: String,\n  },\n  EntryRecorded(NewEntry),\n  SpanRecorded(NewSpan),\n  Snapshot {\n    run_id: String,\n    test_id: String,\n    system: String,\n    state_json: String,\n    summary: String,\n  },\n}\n\n#[derive(Clone, Debug, Serialize)]\npub struct LiveDashboardEvent {\n  pub seq: u64,\n  pub run_id: String,\n  pub event_type: String,\n  pub payload: LiveDashboardPayload,\n}\n\nimpl LiveDashboardEvent {\n  #[must_use]\n  pub fn with_seq(mut self, seq: u64) -> Self {\n    self.seq = seq;\n    let temp_id = live_record_id(seq);\n    match &mut self.payload {\n      LiveDashboardPayload::EntryRecorded(payload) => payload.id = temp_id,\n      LiveDashboardPayload::SpanRecorded(payload) => payload.id = temp_id,\n      LiveDashboardPayload::Snapshot(payload) => payload.id = temp_id,\n      LiveDashboardPayload::RunStarted(_)\n      | LiveDashboardPayload::RunEnded(_)\n      | LiveDashboardPayload::TestStarted(_)\n      | LiveDashboardPayload::TestEnded(_) => {}\n    }\n    self\n  }\n}\n\n#[derive(Clone, Debug, Serialize)]\n#[serde(untagged)]\npub enum LiveDashboardPayload {\n  RunStarted(LiveRunStartedPayload),\n  RunEnded(LiveRunEndedPayload),\n  TestStarted(LiveTestStartedPayload),\n  TestEnded(LiveTestEndedPayload),\n  EntryRecorded(LiveEntryRecordedPayload),\n  SpanRecorded(LiveSpanRecordedPayload),\n  Snapshot(LiveSnapshotPayload),\n}\n\n#[derive(Clone, Debug, Serialize)]\npub struct LiveRunStartedPayload {\n  pub app_name: String,\n  pub started_at: String,\n  pub stove_version: Option<String>,\n  pub systems: Vec<String>,\n}\n\n#[derive(Clone, Debug, Serialize)]\npub struct LiveRunEndedPayload {\n  pub ended_at: String,\n  pub status: String,\n  pub total_tests: i32,\n  pub passed: i32,\n  pub failed: i32,\n  pub duration_ms: i64,\n}\n\n#[derive(Clone, Debug, Serialize)]\npub struct LiveTestStartedPayload {\n  pub test_id: String,\n  pub test_name: String,\n  pub spec_name: String,\n  pub test_path: Vec<String>,\n  pub started_at: String,\n  pub status: String,\n}\n\n#[derive(Clone, Debug, Serialize)]\npub struct LiveTestEndedPayload {\n  pub test_id: String,\n  pub status: String,\n  pub duration_ms: i64,\n  pub error: Option<String>,\n  pub ended_at: String,\n}\n\n#[derive(Clone, Debug, Serialize)]\npub struct LiveEntryRecordedPayload {\n  pub id: i64,\n  pub test_id: String,\n  pub timestamp: String,\n  pub system: String,\n  pub action: String,\n  pub result: String,\n  pub input: Option<String>,\n  pub output: Option<String>,\n  pub metadata: Option<String>,\n  pub expected: Option<String>,\n  pub actual: Option<String>,\n  pub error: Option<String>,\n  pub trace_id: Option<String>,\n}\n\n#[derive(Clone, Debug, Serialize)]\npub struct LiveSpanRecordedPayload {\n  pub id: i64,\n  pub test_id: Option<String>,\n  pub trace_id: String,\n  pub span_id: String,\n  pub parent_span_id: Option<String>,\n  pub operation_name: String,\n  pub service_name: String,\n  pub start_time_nanos: i64,\n  pub end_time_nanos: i64,\n  pub status: String,\n  pub attributes: Option<String>,\n  pub exception_type: Option<String>,\n  pub exception_message: Option<String>,\n  pub exception_stack_trace: Option<String>,\n}\n\n#[derive(Clone, Debug, Serialize)]\npub struct LiveSnapshotPayload {\n  pub id: i64,\n  pub test_id: String,\n  pub system: String,\n  pub state_json: String,\n  pub summary: String,\n}\n\n#[derive(Clone)]\npub struct EventIngestor {\n  sender: mpsc::UnboundedSender<IngestCommand>,\n}\n\nimpl EventIngestor {\n  #[must_use]\n  pub fn new(repository: Arc<Repository>) -> Self {\n    Self::with_config(repository, DEFAULT_MAX_BATCH_SIZE, DEFAULT_MAX_BATCH_DELAY)\n  }\n\n  #[must_use]\n  pub fn with_config(\n    repository: Arc<Repository>,\n    max_batch_size: usize,\n    max_batch_delay: Duration,\n  ) -> Self {\n    let (sender, receiver) = mpsc::unbounded_channel();\n    tokio::spawn(run_ingest_loop(\n      repository,\n      receiver,\n      max_batch_size,\n      max_batch_delay,\n    ));\n    Self { sender }\n  }\n\n  pub fn enqueue(&self, event: PersistedDashboardEvent, flush_immediately: bool) -> Result<()> {\n    self\n      .sender\n      .send(IngestCommand::Persist {\n        event: Box::new(event),\n        flush_immediately,\n      })\n      .map_err(|_| AppError::Startup(\"persistence worker is not running\".to_string()))\n  }\n\n  pub async fn flush_pending(&self) -> Result<()> {\n    let (reply_tx, reply_rx) = oneshot::channel();\n    self\n      .sender\n      .send(IngestCommand::Flush { reply: reply_tx })\n      .map_err(|_| AppError::Startup(\"persistence worker is not running\".to_string()))?;\n    reply_rx\n      .await\n      .map_err(|_| AppError::Startup(\"persistence worker stopped before flushing\".to_string()))?\n  }\n}\n\nenum IngestCommand {\n  Persist {\n    event: Box<PersistedDashboardEvent>,\n    flush_immediately: bool,\n  },\n  Flush {\n    reply: oneshot::Sender<Result<()>>,\n  },\n}\n\nasync fn run_ingest_loop(\n  repository: Arc<Repository>,\n  mut receiver: mpsc::UnboundedReceiver<IngestCommand>,\n  max_batch_size: usize,\n  max_batch_delay: Duration,\n) {\n  let mut pending = Vec::with_capacity(max_batch_size.max(1));\n\n  loop {\n    if pending.is_empty() {\n      let Some(command) = receiver.recv().await else {\n        break;\n      };\n      handle_command(\n        command,\n        repository.as_ref(),\n        &mut pending,\n        max_batch_size.max(1),\n      );\n      continue;\n    }\n\n    let delay = tokio::time::sleep(max_batch_delay);\n    tokio::pin!(delay);\n\n    tokio::select! {\n      maybe_command = receiver.recv() => {\n        if let Some(command) = maybe_command {\n          handle_command(\n            command,\n            repository.as_ref(),\n            &mut pending,\n            max_batch_size.max(1),\n          );\n        } else {\n          if let Err(error) = persist_pending(repository.as_ref(), &mut pending) {\n            warn!(%error, \"Failed to flush pending dashboard events during shutdown\");\n          }\n          break;\n        }\n      }\n      () = &mut delay => {\n        if let Err(error) = persist_pending(repository.as_ref(), &mut pending) {\n          warn!(%error, \"Failed to persist a batched dashboard event flush\");\n        }\n      }\n    }\n  }\n}\n\nfn handle_command(\n  command: IngestCommand,\n  repository: &Repository,\n  pending: &mut Vec<PersistedDashboardEvent>,\n  max_batch_size: usize,\n) {\n  match command {\n    IngestCommand::Persist {\n      event,\n      flush_immediately,\n    } => {\n      pending.push(*event);\n      if (flush_immediately || pending.len() >= max_batch_size)\n        && let Err(error) = persist_pending(repository, pending)\n      {\n        warn!(%error, \"Failed to persist dashboard events after batch threshold\");\n      }\n    }\n    IngestCommand::Flush { reply } => {\n      let _ = reply.send(persist_pending(repository, pending));\n    }\n  }\n}\n\nfn persist_pending(\n  repository: &Repository,\n  pending: &mut Vec<PersistedDashboardEvent>,\n) -> Result<()> {\n  if pending.is_empty() {\n    return Ok(());\n  }\n\n  let batch = std::mem::take(pending);\n  match repository.apply_persisted_events(&batch) {\n    Ok(()) => Ok(()),\n    Err(batch_error) => {\n      warn!(\n        %batch_error,\n        batch_size = batch.len(),\n        \"Batch persistence failed, retrying events individually\"\n      );\n\n      let mut first_individual_error = None;\n      for event in &batch {\n        if let Err(error) = repository.apply_persisted_events(std::slice::from_ref(event)) {\n          warn!(%error, \"Failed to persist dashboard event after individual retry\");\n          if first_individual_error.is_none() {\n            first_individual_error = Some(error);\n          }\n        }\n      }\n\n      if let Some(error) = first_individual_error {\n        Err(error)\n      } else {\n        Ok(())\n      }\n    }\n  }\n}\n\nfn live_record_id(seq: u64) -> i64 {\n  let bounded = seq.min(i64::MAX as u64);\n  -bounded.cast_signed()\n}\n"
  },
  {
    "path": "tools/stove-cli/src/lib.rs",
    "content": "#![deny(clippy::all)]\n#![warn(clippy::pedantic)]\n// Allow these pedantic lints project-wide — they conflict with our conventions.\n#![allow(clippy::module_name_repetitions)]\n#![allow(clippy::missing_errors_doc)]\n#![allow(clippy::missing_panics_doc)]\n#![allow(clippy::redundant_closure_for_method_calls)]\n\npub const STOVE_CLI_VERSION: &str = env!(\"STOVE_VERSION\");\n\npub mod config;\npub mod error;\npub mod grpc;\npub mod http;\npub mod ingest;\npub mod mcp;\npub mod skills;\npub mod sse;\npub mod storage;\n\n/// Generated protobuf types from shared `.proto` contract.\n#[allow(clippy::pedantic)]\npub mod proto {\n  tonic::include_proto!(\"stove.dashboard.v1\");\n}\n"
  },
  {
    "path": "tools/stove-cli/src/main.rs",
    "content": "use std::net::SocketAddr;\nuse std::sync::Arc;\n\nuse clap::Parser;\nuse tracing::info;\n\nuse stove::config;\nuse stove::grpc;\nuse stove::http;\nuse stove::ingest;\nuse stove::proto;\nuse stove::skills;\nuse stove::sse;\nuse stove::storage;\n\n#[tokio::main]\nasync fn main() -> anyhow::Result<()> {\n  tracing_subscriber::fmt()\n    .with_env_filter(\n      tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| \"info\".into()),\n    )\n    .init();\n\n  let config = config::Config::parse();\n\n  // Handle a `skills` subcommand if requested. Returns true when handled.\n  if skills::handle_skills_command(&config).await? {\n    return Ok(());\n  }\n\n  // Handle --fresh-start: back up and delete the existing database\n  if config.fresh_start {\n    if let Some(backup_path) = config::handle_fresh_start(&config.db)? {\n      info!(\"Backed up database to {}\", backup_path);\n      println!(\"  Backed up database to {backup_path}\");\n    }\n    println!(\"  Starting fresh — database will be recreated.\");\n  }\n\n  // Initialize database\n  let db = storage::database::Database::open(&config.db)?;\n  let repository = Arc::new(storage::repository::Repository::new(db));\n\n  // Handle --clear flag\n  if config.clear {\n    repository.clear_all()?;\n    info!(\"Cleared all stored runs.\");\n    return Ok(());\n  }\n\n  // Suggest or apply Stove agent skills update before serving.\n  // Network/IO errors are swallowed inside; never blocks startup.\n  skills::maybe_update_skills(&config).await;\n\n  let sse_manager = Arc::new(sse::manager::SseManager::new());\n  let ingestor = ingest::EventIngestor::new(repository.clone());\n\n  // Start gRPC server\n  let grpc_addr: SocketAddr = format!(\"0.0.0.0:{}\", config.grpc_port).parse()?;\n  let grpc_service = grpc::service::DashboardEventServiceImpl::new_with_ingestor(\n    repository.clone(),\n    sse_manager.clone(),\n    ingestor.clone(),\n  );\n  let grpc_handle = tokio::spawn(async move {\n    info!(\"gRPC server listening on {}\", grpc_addr);\n    tonic::transport::Server::builder()\n      .add_service(\n        proto::dashboard_event_service_server::DashboardEventServiceServer::new(grpc_service),\n      )\n      .serve(grpc_addr)\n      .await\n  });\n\n  // Start HTTP server\n  let http_addr: SocketAddr = format!(\"0.0.0.0:{}\", config.port).parse()?;\n  let router = http::server::create_router_with_ingestor(repository, sse_manager, Some(ingestor));\n  let http_handle = tokio::spawn(async move {\n    info!(\"HTTP server listening on {}\", http_addr);\n    let listener = tokio::net::TcpListener::bind(http_addr).await?;\n    axum::serve(\n      listener,\n      router.into_make_service_with_connect_info::<SocketAddr>(),\n    )\n    .await\n  });\n\n  println!(\n    \"\\n  Stove CLI v{} running\\n  UI:   http://localhost:{}\\n  REST: http://localhost:{}/api/v1\\n  MCP:  http://localhost:{}/mcp\\n  gRPC: localhost:{}\\n\",\n    env!(\"STOVE_VERSION\"),\n    config.port,\n    config.port,\n    config.port,\n    config.grpc_port\n  );\n\n  // Wait for either server to finish (or error)\n  tokio::select! {\n      result = grpc_handle => {\n          result??;\n      }\n      result = http_handle => {\n          result??;\n      }\n  }\n\n  Ok(())\n}\n"
  },
  {
    "path": "tools/stove-cli/src/mcp/analysis/evidence.rs",
    "content": "use serde_json::{Value, json};\n\nuse super::super::contract::{ArgName, RawEvidenceKind, ToolName};\nuse crate::storage::models::{Entry, Snapshot, Span};\n\npub(super) fn entry_preview(entry: &Entry, max_chars: usize) -> Value {\n  json!({\n    \"id\": entry.id,\n    \"timestamp\": entry.timestamp,\n    \"system\": entry.system,\n    \"action\": entry.action,\n    \"result\": entry.result,\n    \"input\": preview_field(entry.input.as_deref(), max_chars),\n    \"output\": preview_field(entry.output.as_deref(), max_chars),\n    \"metadata\": preview_field(entry.metadata.as_deref(), max_chars),\n    \"expected\": preview_field(entry.expected.as_deref(), max_chars),\n    \"actual\": preview_field(entry.actual.as_deref(), max_chars),\n    \"error\": clip_opt(entry.error.as_deref(), max_chars),\n    \"trace_id\": entry.trace_id,\n    \"raw_tool_call\": super::tool_call(ToolName::RawEvidence, super::tool_args([\n      (ArgName::Kind, json!(RawEvidenceKind::Entry.as_str())),\n      (ArgName::Id, json!(entry.id)),\n      (ArgName::RunId, json!(&entry.run_id)),\n      (ArgName::TestId, json!(&entry.test_id)),\n    ])),\n  })\n}\n\npub(super) fn span_preview(span: &Span, max_chars: usize) -> Value {\n  json!({\n    \"id\": span.id,\n    \"trace_id\": span.trace_id,\n    \"span_id\": span.span_id,\n    \"parent_span_id\": span.parent_span_id,\n    \"operation_name\": span.operation_name,\n    \"service_name\": span.service_name,\n    \"duration_ms\": nanos_to_millis(span.end_time_nanos - span.start_time_nanos),\n    \"status\": span.status,\n    \"attributes\": preview_field(span.attributes.as_deref(), max_chars),\n    \"exception_type\": span.exception_type,\n    \"exception_message\": clip_opt(span.exception_message.as_deref(), max_chars),\n    \"exception_stack_trace\": clip_opt(span.exception_stack_trace.as_deref(), max_chars),\n  })\n}\n\npub(super) fn snapshot_summary(snapshot: &Snapshot, max_chars: usize) -> Value {\n  let state = parse_state(&snapshot.state_json, max_chars);\n  json!({\n    \"id\": snapshot.id,\n    \"system\": snapshot.system,\n    \"summary\": clip_string(&snapshot.summary, max_chars),\n    \"state_overview\": state_overview(&state),\n    \"snapshot_tool_call\": super::tool_call(ToolName::Snapshot, super::tool_args([\n      (ArgName::RunId, json!(&snapshot.run_id)),\n      (ArgName::TestId, json!(&snapshot.test_id)),\n      (ArgName::System, json!(&snapshot.system)),\n    ])),\n  })\n}\n\npub(super) fn snapshot_detail(\n  snapshot: &Snapshot,\n  pointer: Option<&str>,\n  max_chars: usize,\n) -> Value {\n  let parsed = parse_state(&snapshot.state_json, max_chars);\n  let selected_state = pointer.map_or_else(\n    || parsed.clone(),\n    |pointer| {\n      parsed\n        .get(\"value\")\n        .and_then(|value| value.pointer(pointer))\n        .map_or_else(\n          || json!({ \"parse_status\": \"pointer_not_found\", \"json_pointer\": pointer }),\n          |value| json!({ \"parse_status\": \"ok\", \"json_pointer\": pointer, \"value\": redact_value(value, max_chars) }),\n        )\n    },\n  );\n\n  json!({\n    \"id\": snapshot.id,\n    \"system\": snapshot.system,\n    \"summary\": clip_string(&snapshot.summary, max_chars),\n    \"state\": selected_state,\n    \"raw_tool_call\": super::tool_call(ToolName::RawEvidence, super::tool_args([\n      (ArgName::Kind, json!(RawEvidenceKind::Snapshot.as_str())),\n      (ArgName::Id, json!(snapshot.id)),\n      (ArgName::RunId, json!(&snapshot.run_id)),\n      (ArgName::TestId, json!(&snapshot.test_id)),\n    ])),\n  })\n}\n\nfn state_overview(parsed: &Value) -> Value {\n  let Some(value) = parsed.get(\"value\") else {\n    return parsed.clone();\n  };\n  match value {\n    Value::Object(map) => json!({\n      \"type\": \"object\",\n      \"keys\": map.keys().take(20).collect::<Vec<_>>(),\n      \"key_count\": map.len(),\n    }),\n    Value::Array(items) => json!({ \"type\": \"array\", \"item_count\": items.len() }),\n    _ => json!({ \"type\": value_type(value), \"value\": value }),\n  }\n}\n\nfn parse_state(raw: &str, max_chars: usize) -> Value {\n  match serde_json::from_str::<Value>(raw) {\n    Ok(value) => json!({ \"parse_status\": \"ok\", \"value\": redact_value(&value, max_chars) }),\n    Err(error) => json!({\n      \"parse_status\": \"malformed_json\",\n      \"parse_error\": error.to_string(),\n      \"raw_preview\": clip_string(raw, max_chars),\n    }),\n  }\n}\n\nfn preview_field(raw: Option<&str>, max_chars: usize) -> Value {\n  let Some(raw) = raw.filter(|value| !value.is_empty()) else {\n    return Value::Null;\n  };\n\n  match serde_json::from_str::<Value>(raw) {\n    Ok(value) => redact_value(&value, max_chars),\n    Err(error) => json!({\n      \"parse_status\": \"plain_or_malformed\",\n      \"parse_error\": error.to_string(),\n      \"preview\": clip_string(raw, max_chars),\n    }),\n  }\n}\n\nfn redact_value(value: &Value, max_chars: usize) -> Value {\n  match value {\n    Value::Object(map) => Value::Object(\n      map\n        .iter()\n        .map(|(key, value)| {\n          if is_sensitive_key(key) {\n            (key.clone(), Value::String(\"[REDACTED]\".to_string()))\n          } else {\n            (key.clone(), redact_value(value, max_chars))\n          }\n        })\n        .collect(),\n    ),\n    Value::Array(items) => Value::Array(\n      items\n        .iter()\n        .take(50)\n        .map(|item| redact_value(item, max_chars))\n        .collect(),\n    ),\n    Value::String(value) => json!(clip_string(value, max_chars)),\n    _ => value.clone(),\n  }\n}\n\nfn is_sensitive_key(key: &str) -> bool {\n  let lower = key.to_ascii_lowercase();\n  [\n    \"authorization\",\n    \"cookie\",\n    \"password\",\n    \"secret\",\n    \"token\",\n    \"apikey\",\n    \"api_key\",\n    \"credential\",\n  ]\n  .iter()\n  .any(|needle| lower.contains(needle))\n}\n\npub(super) fn clip_opt(value: Option<&str>, max_chars: usize) -> Value {\n  value\n    .filter(|value| !value.is_empty())\n    .map_or(Value::Null, |value| json!(clip_string(value, max_chars)))\n}\n\nfn clip_string(value: &str, max_chars: usize) -> String {\n  let chars = value.chars().count();\n  if chars <= max_chars {\n    return value.to_string();\n  }\n\n  let prefix: String = value.chars().take(max_chars).collect();\n  format!(\n    \"{prefix}...<truncated {} chars>\",\n    chars.saturating_sub(max_chars)\n  )\n}\n\n#[allow(clippy::cast_precision_loss)]\nfn nanos_to_millis(nanos: i64) -> f64 {\n  nanos as f64 / 1_000_000.0\n}\n\nfn value_type(value: &Value) -> &'static str {\n  match value {\n    Value::Null => \"null\",\n    Value::Bool(_) => \"boolean\",\n    Value::Number(_) => \"number\",\n    Value::String(_) => \"string\",\n    Value::Array(_) => \"array\",\n    Value::Object(_) => \"object\",\n  }\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn redacts_sensitive_keys_recursively() {\n    let value = json!({\n      \"Authorization\": \"Bearer secret\",\n      \"nested\": { \"apiKey\": \"abc\", \"safe\": \"value\" },\n      \"items\": [{ \"password\": \"pw\" }]\n    });\n\n    let redacted = redact_value(&value, 100);\n\n    assert_eq!(redacted[\"Authorization\"], \"[REDACTED]\");\n    assert_eq!(redacted[\"nested\"][\"apiKey\"], \"[REDACTED]\");\n    assert_eq!(redacted[\"nested\"][\"safe\"], \"value\");\n    assert_eq!(redacted[\"items\"][0][\"password\"], \"[REDACTED]\");\n  }\n\n  #[test]\n  fn clips_long_strings_deterministically() {\n    let clipped = clip_string(\"abcdef\", 3);\n\n    assert_eq!(clipped, \"abc...<truncated 3 chars>\");\n  }\n\n  #[test]\n  fn malformed_json_preview_keeps_parse_error() {\n    let preview = preview_field(Some(\"{bad\"), 20);\n\n    assert_eq!(preview[\"parse_status\"], \"plain_or_malformed\");\n    assert!(preview[\"parse_error\"].as_str().unwrap().contains(\"key\"));\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/mcp/analysis.rs",
    "content": "mod evidence;\n\nuse std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};\nuse std::sync::Arc;\nuse std::time::Duration;\n\nuse serde_json::{Value, json};\n\nuse self::evidence::{clip_opt, entry_preview, snapshot_detail, snapshot_summary, span_preview};\nuse super::args::{\n  Budget, ExactTestArgs, FailuresArgs, ListArgs, RawEvidenceArgs, RunsArgs, SnapshotArgs,\n  TimelineArgs, TraceArgs, parse,\n};\nuse super::contract::{\n  ArgName, RawEvidenceKind, RunStatusValue, STATUS_ERROR, TimelineFocus, ToolName,\n};\nuse crate::ingest::EventIngestor;\nuse crate::storage::models::{Entry, Run, RunStatus, Span, Test, TestStatus};\nuse crate::storage::repository::Repository;\n\nconst FLUSH_TIMEOUT: Duration = Duration::from_millis(500);\n\n#[derive(Debug, Clone)]\npub struct ToolOutput {\n  pub structured: Value,\n  pub text: String,\n}\n\n#[derive(Clone)]\npub struct Analyzer {\n  repository: Arc<Repository>,\n  ingestor: Option<EventIngestor>,\n}\n\nimpl Analyzer {\n  #[must_use]\n  pub fn new(repository: Arc<Repository>, ingestor: Option<EventIngestor>) -> Self {\n    Self {\n      repository,\n      ingestor,\n    }\n  }\n\n  pub async fn call_tool(&self, name: &str, arguments: Value) -> Result<ToolOutput, String> {\n    self.flush_pending().await;\n    match ToolName::from_str(name) {\n      Some(ToolName::Apps) => self.apps(arguments),\n      Some(ToolName::Runs) => self.runs(arguments),\n      Some(ToolName::Failures) => self.failures(arguments),\n      Some(ToolName::FailureDetail) => self.failure_detail(arguments),\n      Some(ToolName::Timeline) => self.timeline(arguments),\n      Some(ToolName::Trace) => self.trace(arguments),\n      Some(ToolName::Snapshot) => self.snapshot(arguments),\n      Some(ToolName::RawEvidence) => self.raw_evidence(arguments),\n      None => Err(format!(\"unknown Stove MCP tool: {name}\")),\n    }\n  }\n\n  async fn flush_pending(&self) {\n    let Some(ingestor) = &self.ingestor else {\n      return;\n    };\n\n    let _ = tokio::time::timeout(FLUSH_TIMEOUT, ingestor.flush_pending()).await;\n  }\n\n  fn apps(&self, arguments: Value) -> Result<ToolOutput, String> {\n    let args: ListArgs = parse(arguments)?;\n    let limit = args.limit();\n    let apps = self.repository.get_apps().map_err(display_error)?;\n    let total_apps = apps.len();\n    let runs = self.repository.get_runs(None).map_err(display_error)?;\n    let mut failed_runs_by_app: HashMap<String, usize> = HashMap::new();\n    for run in &runs {\n      if run.status == RunStatus::Failed {\n        *failed_runs_by_app.entry(run.app_name.clone()).or_insert(0) += 1;\n      }\n    }\n\n    let items: Vec<Value> = apps\n      .into_iter()\n      .take(limit)\n      .map(|app| {\n        json!({\n          \"app_name\": app.app_name,\n          \"latest_run_id\": app.latest_run_id,\n          \"latest_status\": app.latest_status,\n          \"stove_version\": app.stove_version,\n          \"total_runs\": app.total_runs,\n          \"failed_runs\": failed_runs_by_app.get(&app.app_name).copied().unwrap_or_default(),\n          \"runs_tool_call\": tool_call(ToolName::Runs, tool_args([(ArgName::AppName, json!(&app.app_name))])),\n          \"failures_tool_call\": tool_call(ToolName::Failures, tool_args([(ArgName::AppName, json!(&app.app_name))])),\n        })\n      })\n      .collect();\n\n    let structured = json!({\n      \"apps\": items,\n      \"count\": items.len(),\n      \"total_apps\": total_apps,\n      \"omitted_apps\": total_apps.saturating_sub(items.len()),\n      \"fallback\": fallback_message(),\n    });\n    Ok(output(structured, \"Known Stove apps\"))\n  }\n\n  fn runs(&self, arguments: Value) -> Result<ToolOutput, String> {\n    let args: RunsArgs = parse(arguments)?;\n    let limit = args.common.limit();\n    let status_filter = args.status.as_deref().map(str::to_ascii_uppercase);\n    let mut runs = self\n      .repository\n      .get_runs(args.app_name.as_deref())\n      .map_err(display_error)?;\n\n    if let Some(status) = &status_filter {\n      runs.retain(|run| run.status.to_string() == *status);\n    }\n    let total_runs = runs.len();\n\n    let items: Vec<Value> = runs\n      .into_iter()\n      .take(limit)\n      .map(|run| {\n        json!({\n          \"app_name\": run.app_name,\n          \"run_id\": run.id,\n          \"status\": run.status,\n          \"started_at\": run.started_at,\n          \"ended_at\": run.ended_at,\n          \"total_tests\": run.total_tests,\n          \"passed\": run.passed,\n          \"failed\": run.failed,\n          \"duration_ms\": run.duration_ms,\n          \"stove_version\": run.stove_version,\n          \"systems\": run.systems,\n          \"failures_tool_call\": tool_call(ToolName::Failures, tool_args([(ArgName::RunId, json!(&run.id))])),\n        })\n      })\n      .collect();\n\n    let structured = json!({\n      \"runs\": items,\n      \"count\": items.len(),\n      \"total_runs\": total_runs,\n      \"omitted_runs\": total_runs.saturating_sub(items.len()),\n      \"selector_rules\": selector_rules(),\n      \"fallback\": fallback_message(),\n    });\n    Ok(output(structured, \"Stove runs\"))\n  }\n\n  fn failures(&self, arguments: Value) -> Result<ToolOutput, String> {\n    let args: FailuresArgs = parse(arguments)?;\n    let limit = args.common.limit();\n    let runs = selected_runs(\n      &self.repository,\n      args.app_name.as_deref(),\n      args.run_id.as_deref(),\n    )?;\n\n    let mut groups = Vec::new();\n    let mut selected_failures = 0_usize;\n    let mut total_failures = 0_usize;\n    for run in runs {\n      let tests = self\n        .repository\n        .get_tests_for_run(&run.id)\n        .map_err(display_error)?;\n      let failed_tests: Vec<Test> = tests.into_iter().filter(is_failed_test).collect();\n      total_failures += failed_tests.len();\n\n      let remaining = limit.saturating_sub(selected_failures);\n      let failures: Vec<Value> = failed_tests\n        .into_iter()\n        .take(remaining)\n        .map(|test| failure_item(&run, &test))\n        .collect();\n\n      if failures.is_empty() {\n        continue;\n      }\n      selected_failures += failures.len();\n      groups.push(json!({\n        \"app_name\": run.app_name,\n        \"run_id\": run.id,\n        \"run_status\": run.status,\n        \"started_at\": run.started_at,\n        \"ended_at\": run.ended_at,\n        \"stove_version\": run.stove_version,\n        \"failures\": failures,\n      }));\n    }\n\n    let structured = json!({\n      \"groups\": groups,\n      \"failure_count\": selected_failures,\n      \"total_failure_count\": total_failures,\n      \"omitted_failures\": total_failures.saturating_sub(selected_failures),\n      \"data_freshness\": if groups_have_running_runs(&groups) { \"partial\" } else { \"complete_or_idle\" },\n      \"selector_rules\": selector_rules(),\n      \"fallback\": fallback_message(),\n    });\n    Ok(output(\n      structured,\n      \"Stove failed tests grouped by app and run\",\n    ))\n  }\n\n  fn failure_detail(&self, arguments: Value) -> Result<ToolOutput, String> {\n    let args: ExactTestArgs = parse(arguments)?;\n    let budget = Budget::from_args(args.common.budget.as_deref(), args.common.max_chars);\n    let (run, test) = self.resolve_test(&args.run_id, &args.test_id)?;\n    let entries = self\n      .repository\n      .get_entries(&args.run_id, &args.test_id)\n      .map_err(display_error)?;\n    let snapshots = self\n      .repository\n      .get_snapshots(&args.run_id, &args.test_id)\n      .map_err(display_error)?;\n    let spans = self\n      .repository\n      .get_spans_for_test(&args.run_id, &args.test_id)\n      .map_err(display_error)?;\n\n    let failed_entries: Vec<&Entry> = entries\n      .iter()\n      .filter(|entry| is_failed_status(&entry.result))\n      .collect();\n    let timeline_summary = timeline_summary(\n      &entries,\n      &args.run_id,\n      &args.test_id,\n      budget.timeline_events,\n    );\n    let trace_summary = trace_summary(\n      &spans,\n      &entries,\n      &args.run_id,\n      &args.test_id,\n      budget.trace_spans,\n    );\n    let snapshot_summaries: Vec<Value> = snapshots\n      .iter()\n      .take(budget.snapshots)\n      .map(|snapshot| snapshot_summary(snapshot, budget.string_chars))\n      .collect();\n\n    let structured = json!({\n      \"app_name\": run.app_name,\n      \"run_id\": run.id,\n      \"run_status\": run.status,\n      \"test\": test_json(&test),\n      \"data_freshness\": if test.status == TestStatus::Running || run.status == RunStatus::Running { \"partial\" } else { \"complete_or_idle\" },\n      \"error_summary\": clip_opt(test.error.as_deref(), budget.string_chars),\n      \"failed_entries\": failed_entries\n        .iter()\n        .take(budget.failed_entries)\n        .map(|entry| entry_preview(entry, budget.string_chars))\n        .collect::<Vec<_>>(),\n      \"omitted\": {\n        \"entries\": entries.len().saturating_sub(budget.timeline_events),\n        \"failed_entries\": failed_entries.len().saturating_sub(budget.failed_entries),\n        \"spans\": spans.len().saturating_sub(budget.trace_spans),\n        \"snapshots\": snapshots.len().saturating_sub(snapshot_summaries.len()),\n      },\n      \"timeline_summary\": timeline_summary,\n      \"trace_summary\": trace_summary,\n      \"snapshot_summaries\": snapshot_summaries,\n      \"timeline_tool_call\": exact_test_tool_call(ToolName::Timeline, &args.run_id, &args.test_id),\n      \"trace_tool_call\": exact_test_tool_call(ToolName::Trace, &args.run_id, &args.test_id),\n      \"snapshot_tool_call\": exact_test_tool_call(ToolName::Snapshot, &args.run_id, &args.test_id),\n      \"fallback\": fallback_message(),\n    });\n    Ok(output(structured, \"Stove failure detail\"))\n  }\n\n  fn timeline(&self, arguments: Value) -> Result<ToolOutput, String> {\n    let args: TimelineArgs = parse(arguments)?;\n    let budget = Budget::from_args(\n      args.exact.common.budget.as_deref(),\n      args.exact.common.max_chars,\n    );\n    let (run, test) = self.resolve_test(&args.exact.run_id, &args.exact.test_id)?;\n    let entries = self\n      .repository\n      .get_entries(&args.exact.run_id, &args.exact.test_id)\n      .map_err(display_error)?;\n    let focus = args\n      .focus\n      .unwrap_or_else(|| TimelineFocus::Failure.as_str().to_string());\n    let selected = if focus == TimelineFocus::All.as_str() {\n      entries.iter().take(budget.timeline_events).collect()\n    } else {\n      failure_window(&entries, budget.timeline_events)\n    };\n\n    let structured = json!({\n      \"app_name\": run.app_name,\n      \"run_id\": run.id,\n      \"test\": test_json(&test),\n      \"focus\": focus,\n      \"events\": selected\n        .iter()\n        .map(|entry| entry_preview(entry, budget.string_chars))\n        .collect::<Vec<_>>(),\n      \"total_events\": entries.len(),\n      \"omitted_events\": entries.len().saturating_sub(selected.len()),\n      \"fallback\": fallback_message(),\n    });\n    Ok(output(structured, \"Stove test timeline\"))\n  }\n\n  fn trace(&self, arguments: Value) -> Result<ToolOutput, String> {\n    let args: TraceArgs = parse(arguments)?;\n    let budget = Budget::from_args(args.common.budget.as_deref(), args.common.max_chars);\n    let (run, test, entries, spans) = if let Some(trace_id) = args.trace_id.as_deref() {\n      let spans = self.repository.get_trace(trace_id).map_err(display_error)?;\n      let run = spans\n        .first()\n        .and_then(|span| self.repository.get_run(&span.run_id).ok().flatten());\n      let test = run\n        .as_ref()\n        .and_then(|run| correlated_test_for_trace(&self.repository, run, trace_id));\n      let entries = test.as_ref().map_or_else(Vec::new, |test| {\n        self\n          .repository\n          .get_entries(&test.run_id, &test.id)\n          .unwrap_or_default()\n      });\n      (run, test, entries, spans)\n    } else {\n      let run_id = args\n        .run_id\n        .as_deref()\n        .ok_or_else(|| \"stove_trace requires run_id + test_id or trace_id\".to_string())?;\n      let test_id = args\n        .test_id\n        .as_deref()\n        .ok_or_else(|| \"stove_trace requires run_id + test_id or trace_id\".to_string())?;\n      let (run, test) = self.resolve_test(run_id, test_id)?;\n      let entries = self\n        .repository\n        .get_entries(run_id, test_id)\n        .map_err(display_error)?;\n      let spans = self\n        .repository\n        .get_spans_for_test(run_id, test_id)\n        .map_err(display_error)?;\n      (Some(run), Some(test), entries, spans)\n    };\n\n    let view = args.view.unwrap_or_else(|| \"critical_path\".to_string());\n    let structured = json!({\n      \"app_name\": run.as_ref().map(|run| run.app_name.as_str()),\n      \"run_id\": run.as_ref().map(|run| run.id.as_str()),\n      \"test\": test.as_ref().map(test_json),\n      \"view\": view,\n      \"trace\": trace_summary(&spans, &entries, run.as_ref().map_or(\"\", |run| &run.id), test.as_ref().map_or(\"\", |test| &test.id), budget.trace_spans),\n      \"fallback\": fallback_message(),\n    });\n    Ok(output(structured, \"Stove trace evidence\"))\n  }\n\n  fn snapshot(&self, arguments: Value) -> Result<ToolOutput, String> {\n    let args: SnapshotArgs = parse(arguments)?;\n    let budget = Budget::from_args(\n      args.exact.common.budget.as_deref(),\n      args.exact.common.max_chars,\n    );\n    let (run, test) = self.resolve_test(&args.exact.run_id, &args.exact.test_id)?;\n    let mut snapshots = self\n      .repository\n      .get_snapshots(&args.exact.run_id, &args.exact.test_id)\n      .map_err(display_error)?;\n    if let Some(system) = &args.system {\n      snapshots.retain(|snapshot| snapshot.system == *system);\n    }\n\n    let items = snapshots\n      .iter()\n      .take(budget.snapshots)\n      .map(|snapshot| snapshot_detail(snapshot, args.json_pointer.as_deref(), budget.string_chars))\n      .collect::<Vec<_>>();\n    let structured = json!({\n      \"app_name\": run.app_name,\n      \"run_id\": run.id,\n      \"test\": test_json(&test),\n      \"snapshots\": items,\n      \"total_snapshots\": snapshots.len(),\n      \"omitted_snapshots\": snapshots.len().saturating_sub(items.len()),\n      \"fallback\": fallback_message(),\n    });\n    Ok(output(structured, \"Stove snapshots\"))\n  }\n\n  fn raw_evidence(&self, arguments: Value) -> Result<ToolOutput, String> {\n    let args: RawEvidenceArgs = parse(arguments)?;\n    let budget = Budget::from_args(args.common.budget.as_deref(), args.common.max_chars);\n    let kind = args.kind.to_ascii_lowercase();\n    let evidence = match RawEvidenceKind::from_str(&kind) {\n      Some(RawEvidenceKind::Entry) => {\n        let run_id = args\n          .run_id\n          .as_deref()\n          .ok_or_else(|| \"raw entry lookup requires run_id and test_id\".to_string())?;\n        let test_id = args\n          .test_id\n          .as_deref()\n          .ok_or_else(|| \"raw entry lookup requires run_id and test_id\".to_string())?;\n        let entry = self\n          .repository\n          .get_entries(run_id, test_id)\n          .map_err(display_error)?\n          .into_iter()\n          .find(|entry| entry.id == args.id)\n          .ok_or_else(|| format!(\"entry {} was not found in {run_id}/{test_id}\", args.id))?;\n        json!({ \"kind\": RawEvidenceKind::Entry.as_str(), \"evidence\": entry_preview(&entry, budget.raw_string_chars) })\n      }\n      Some(RawEvidenceKind::Span) => {\n        let spans = if let Some(trace_id) = args.trace_id.as_deref() {\n          self.repository.get_trace(trace_id).map_err(display_error)?\n        } else {\n          let run_id = args\n            .run_id\n            .as_deref()\n            .ok_or_else(|| \"raw span lookup requires trace_id or run_id + test_id\".to_string())?;\n          let test_id = args\n            .test_id\n            .as_deref()\n            .ok_or_else(|| \"raw span lookup requires trace_id or run_id + test_id\".to_string())?;\n          self\n            .repository\n            .get_spans_for_test(run_id, test_id)\n            .map_err(display_error)?\n        };\n        let span = spans\n          .into_iter()\n          .find(|span| span.id == args.id)\n          .ok_or_else(|| format!(\"span {} was not found\", args.id))?;\n        json!({ \"kind\": RawEvidenceKind::Span.as_str(), \"evidence\": span_preview(&span, budget.raw_string_chars) })\n      }\n      Some(RawEvidenceKind::Snapshot) => {\n        let run_id = args\n          .run_id\n          .as_deref()\n          .ok_or_else(|| \"raw snapshot lookup requires run_id and test_id\".to_string())?;\n        let test_id = args\n          .test_id\n          .as_deref()\n          .ok_or_else(|| \"raw snapshot lookup requires run_id and test_id\".to_string())?;\n        let snapshot = self\n          .repository\n          .get_snapshots(run_id, test_id)\n          .map_err(display_error)?\n          .into_iter()\n          .find(|snapshot| snapshot.id == args.id)\n          .ok_or_else(|| format!(\"snapshot {} was not found in {run_id}/{test_id}\", args.id))?;\n        json!({ \"kind\": RawEvidenceKind::Snapshot.as_str(), \"evidence\": snapshot_detail(&snapshot, None, budget.raw_string_chars) })\n      }\n      None => {\n        return Err(format!(\n          \"kind must be one of: {}, {}, {}\",\n          RawEvidenceKind::Entry.as_str(),\n          RawEvidenceKind::Span.as_str(),\n          RawEvidenceKind::Snapshot.as_str()\n        ));\n      }\n    };\n\n    Ok(output(\n      json!({ \"raw_evidence\": evidence, \"fallback\": fallback_message() }),\n      \"Raw Stove evidence\",\n    ))\n  }\n\n  fn resolve_test(&self, run_id: &str, test_id: &str) -> Result<(Run, Test), String> {\n    let run = self\n      .repository\n      .get_run(run_id)\n      .map_err(display_error)?\n      .ok_or_else(|| format!(\"run `{run_id}` was not found\"))?;\n    let test = self\n      .repository\n      .get_tests_for_run(run_id)\n      .map_err(display_error)?\n      .into_iter()\n      .find(|test| test.id == test_id)\n      .ok_or_else(|| format!(\"test `{test_id}` was not found in run `{run_id}`\"))?;\n    Ok((run, test))\n  }\n}\n\nfn selected_runs(\n  repository: &Repository,\n  app_name: Option<&str>,\n  run_id: Option<&str>,\n) -> Result<Vec<Run>, String> {\n  if let Some(run_id) = run_id {\n    return repository\n      .get_run(run_id)\n      .map_err(display_error)?\n      .map_or_else(|| Ok(Vec::new()), |run| Ok(vec![run]));\n  }\n\n  let mut runs = repository.get_runs(app_name).map_err(display_error)?;\n  runs.retain(|run| run.status == RunStatus::Failed || run.status == RunStatus::Running);\n  Ok(runs)\n}\n\nfn failure_item(run: &Run, test: &Test) -> Value {\n  json!({\n    \"app_name\": run.app_name,\n    \"run_id\": run.id,\n    \"test_id\": test.id,\n    \"spec_name\": test.spec_name,\n    \"test_path\": test.test_path,\n    \"test_name\": test.test_name,\n    \"status\": test.status,\n    \"duration_ms\": test.duration_ms,\n    \"error_summary\": clip_opt(test.error.as_deref(), 600),\n    \"detail_tool_call\": exact_test_tool_call(ToolName::FailureDetail, &run.id, &test.id),\n    \"timeline_tool_call\": exact_test_tool_call(ToolName::Timeline, &run.id, &test.id),\n    \"trace_tool_call\": exact_test_tool_call(ToolName::Trace, &run.id, &test.id),\n  })\n}\n\nfn test_json(test: &Test) -> Value {\n  json!({\n    \"test_id\": test.id,\n    \"test_name\": test.test_name,\n    \"spec_name\": test.spec_name,\n    \"test_path\": test.test_path,\n    \"status\": test.status,\n    \"started_at\": test.started_at,\n    \"ended_at\": test.ended_at,\n    \"duration_ms\": test.duration_ms,\n  })\n}\n\nfn timeline_summary(entries: &[Entry], run_id: &str, test_id: &str, max_events: usize) -> Value {\n  let selected = failure_window(entries, max_events);\n  json!({\n    \"total_events\": entries.len(),\n    \"failed_entries\": entries.iter().filter(|entry| is_failed_status(&entry.result)).count(),\n    \"events\": selected.iter().map(|entry| compact_event(entry)).collect::<Vec<_>>(),\n    \"timeline_tool_call\": exact_test_tool_call(ToolName::Timeline, run_id, test_id),\n  })\n}\n\nfn failure_window(entries: &[Entry], max_events: usize) -> Vec<&Entry> {\n  if entries.is_empty() || max_events == 0 {\n    return Vec::new();\n  }\n  let failed_indexes: Vec<usize> = entries\n    .iter()\n    .enumerate()\n    .filter_map(|(index, entry)| is_failed_status(&entry.result).then_some(index))\n    .collect();\n\n  if failed_indexes.is_empty() {\n    return entries.iter().take(max_events).collect();\n  }\n\n  let mut selected = BTreeSet::new();\n  for index in failed_indexes {\n    let start = index.saturating_sub(2);\n    let end = (index + 2).min(entries.len().saturating_sub(1));\n    for selected_index in start..=end {\n      selected.insert(selected_index);\n    }\n  }\n\n  selected\n    .into_iter()\n    .take(max_events)\n    .filter_map(|index| entries.get(index))\n    .collect()\n}\n\nfn trace_summary(\n  spans: &[Span],\n  entries: &[Entry],\n  run_id: &str,\n  test_id: &str,\n  max_spans: usize,\n) -> Value {\n  if spans.is_empty() {\n    return json!({\n      \"trace_status\": \"uncorrelated\",\n      \"trace_ids\": trace_ids_from_entries(entries),\n      \"failed_spans\": 0,\n      \"exception_spans\": 0,\n      \"message\": \"No spans were correlated to this test. Fall back to timeline entries and logs if trace evidence is needed.\",\n    });\n  }\n\n  let ranked_trace_ids = ranked_trace_ids(spans, entries);\n  let failed_spans: Vec<&Span> = spans.iter().filter(|span| is_failed_span(span)).collect();\n  let exception_spans: Vec<&Span> = spans\n    .iter()\n    .filter(|span| span.exception_type.is_some())\n    .collect();\n  let primary_trace_id = ranked_trace_ids.first().cloned();\n  let critical_path = primary_trace_id.map_or_else(Vec::new, |trace_id| {\n    critical_path_for_trace(spans, &trace_id, max_spans)\n  });\n\n  json!({\n    \"trace_status\": \"correlated\",\n    \"trace_ids\": ranked_trace_ids,\n    \"total_spans\": spans.len(),\n    \"omitted_spans\": spans.len().saturating_sub(max_spans),\n    \"failed_spans\": failed_spans.len(),\n    \"exception_spans\": exception_spans.len(),\n    \"critical_path\": critical_path,\n    \"exceptions\": exception_spans\n      .iter()\n      .take(max_spans)\n      .map(|span| span_preview(span, 600))\n      .collect::<Vec<_>>(),\n    \"trace_tool_call\": exact_test_tool_call(ToolName::Trace, run_id, test_id),\n  })\n}\n\nfn trace_ids_from_entries(entries: &[Entry]) -> Vec<String> {\n  entries\n    .iter()\n    .filter_map(|entry| entry.trace_id.clone())\n    .filter(|trace_id| !trace_id.is_empty())\n    .collect::<BTreeSet<_>>()\n    .into_iter()\n    .collect()\n}\n\nfn ranked_trace_ids(spans: &[Span], entries: &[Entry]) -> Vec<String> {\n  let failed_entry_traces: HashSet<String> = entries\n    .iter()\n    .filter(|entry| is_failed_status(&entry.result))\n    .filter_map(|entry| entry.trace_id.clone())\n    .filter(|trace_id| !trace_id.is_empty())\n    .collect();\n\n  let mut scores: BTreeMap<String, i32> = BTreeMap::new();\n  for span in spans {\n    let mut score = 1;\n    if is_failed_span(span) {\n      score += 10;\n    }\n    if failed_entry_traces.contains(&span.trace_id) {\n      score += 20;\n    }\n    *scores.entry(span.trace_id.clone()).or_insert(0) += score;\n  }\n\n  let mut ranked: Vec<(String, i32)> = scores.into_iter().collect();\n  ranked.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));\n  ranked.into_iter().map(|(trace_id, _)| trace_id).collect()\n}\n\nfn critical_path_for_trace(spans: &[Span], trace_id: &str, max_spans: usize) -> Vec<Value> {\n  let trace_spans: Vec<&Span> = spans\n    .iter()\n    .filter(|span| span.trace_id == trace_id)\n    .collect();\n  let target = trace_spans\n    .iter()\n    .find(|span| is_failed_span(span))\n    .or_else(|| {\n      trace_spans\n        .iter()\n        .max_by_key(|span| span.end_time_nanos - span.start_time_nanos)\n    });\n\n  let Some(target) = target else {\n    return Vec::new();\n  };\n\n  let by_span_id: HashMap<&str, &Span> = trace_spans\n    .iter()\n    .map(|span| (span.span_id.as_str(), *span))\n    .collect();\n  let mut path = Vec::new();\n  let mut current = Some(*target);\n  let mut seen = HashSet::new();\n  while let Some(span) = current {\n    if !seen.insert(span.span_id.clone()) {\n      break;\n    }\n    path.push(span_preview(span, 240));\n    current = span\n      .parent_span_id\n      .as_deref()\n      .and_then(|parent_id| by_span_id.get(parent_id).copied());\n  }\n  path.reverse();\n  path.into_iter().take(max_spans).collect()\n}\n\nfn compact_event(entry: &Entry) -> Value {\n  json!({\n    \"id\": entry.id,\n    \"timestamp\": entry.timestamp,\n    \"system\": entry.system,\n    \"action\": entry.action,\n    \"result\": entry.result,\n    \"trace_id\": entry.trace_id,\n  })\n}\n\nfn groups_have_running_runs(groups: &[Value]) -> bool {\n  groups.iter().any(|group| {\n    group.get(\"run_status\").and_then(Value::as_str) == Some(RunStatusValue::Running.as_str())\n  })\n}\n\nfn correlated_test_for_trace(repository: &Repository, run: &Run, trace_id: &str) -> Option<Test> {\n  repository\n    .get_tests_for_run(&run.id)\n    .ok()?\n    .into_iter()\n    .find(|test| {\n      repository\n        .get_entries(&run.id, &test.id)\n        .is_ok_and(|entries| {\n          entries\n            .iter()\n            .any(|entry| entry.trace_id.as_deref() == Some(trace_id))\n        })\n    })\n}\n\nfn is_failed_test(test: &Test) -> bool {\n  matches!(test.status, TestStatus::Failed | TestStatus::Error)\n}\n\nfn is_failed_status(status: &TestStatus) -> bool {\n  matches!(status, TestStatus::Failed | TestStatus::Error)\n}\n\nfn is_failed_span(span: &Span) -> bool {\n  span.status.eq_ignore_ascii_case(STATUS_ERROR)\n    || span\n      .status\n      .eq_ignore_ascii_case(RunStatusValue::Failed.as_str())\n    || span.exception_type.is_some()\n}\n\npub(super) fn tool_call(tool: ToolName, arguments: Value) -> Value {\n  let mut call = serde_json::Map::new();\n  call.insert(\"tool\".to_string(), Value::String(tool.as_str().to_string()));\n  call.insert(\"arguments\".to_string(), arguments);\n  Value::Object(call)\n}\n\nfn exact_test_tool_call(tool: ToolName, run_id: &str, test_id: &str) -> Value {\n  tool_call(\n    tool,\n    tool_args([\n      (ArgName::RunId, json!(run_id)),\n      (ArgName::TestId, json!(test_id)),\n    ]),\n  )\n}\n\npub(super) fn tool_args(entries: impl IntoIterator<Item = (ArgName, Value)>) -> Value {\n  Value::Object(\n    entries\n      .into_iter()\n      .map(|(key, value)| (key.as_str().to_string(), value))\n      .collect(),\n  )\n}\n\nfn selector_rules() -> Value {\n  json!({\n    \"app_name\": \"grouping/filter only; multiple runs may exist per app\",\n    \"run_id\": \"canonical execution boundary\",\n    \"test_id\": \"unique only within run_id\",\n    \"exact_test_selector\": [ArgName::RunId.as_str(), ArgName::TestId.as_str()],\n  })\n}\n\nfn fallback_message() -> &'static str {\n  \"If Stove MCP is unavailable, incomplete, or ambiguous, fall back to normal test output, Stove failure reports, and logs.\"\n}\n\nfn output(structured: Value, heading: &str) -> ToolOutput {\n  let text = format!(\"{heading}\\n{}\", compact_text(&structured));\n  ToolOutput { structured, text }\n}\n\nfn compact_text(value: &Value) -> String {\n  serde_json::to_string_pretty(value)\n    .unwrap_or_else(|_| \"Stove MCP result could not be rendered as JSON\".to_string())\n}\n\nfn display_error(error: impl std::fmt::Display) -> String {\n  error.to_string()\n}\n"
  },
  {
    "path": "tools/stove-cli/src/mcp/args.rs",
    "content": "use serde::Deserialize;\nuse serde_json::Value;\n\nuse super::contract::BudgetValue;\n\nconst DEFAULT_LIMIT: usize = 20;\nconst MAX_LIMIT: usize = 100;\n\n#[derive(Debug, Deserialize, Default)]\npub(crate) struct CommonArgs {\n  pub(crate) limit: Option<usize>,\n  pub(crate) budget: Option<String>,\n  pub(crate) max_chars: Option<usize>,\n}\n\nimpl CommonArgs {\n  pub(crate) fn limit(&self) -> usize {\n    self.limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT)\n  }\n}\n\n#[derive(Debug, Deserialize, Default)]\npub(crate) struct ListArgs {\n  #[serde(flatten)]\n  common: CommonArgs,\n}\n\nimpl ListArgs {\n  pub(crate) fn limit(&self) -> usize {\n    self.common.limit()\n  }\n}\n\n#[derive(Debug, Deserialize, Default)]\npub(crate) struct RunsArgs {\n  #[serde(flatten)]\n  pub(crate) common: CommonArgs,\n  pub(crate) app_name: Option<String>,\n  pub(crate) status: Option<String>,\n}\n\n#[derive(Debug, Deserialize, Default)]\npub(crate) struct FailuresArgs {\n  #[serde(flatten)]\n  pub(crate) common: CommonArgs,\n  pub(crate) app_name: Option<String>,\n  pub(crate) run_id: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\npub(crate) struct ExactTestArgs {\n  #[serde(flatten)]\n  pub(crate) common: CommonArgs,\n  pub(crate) run_id: String,\n  pub(crate) test_id: String,\n}\n\n#[derive(Debug, Deserialize)]\npub(crate) struct TimelineArgs {\n  #[serde(flatten)]\n  pub(crate) exact: ExactTestArgs,\n  pub(crate) focus: Option<String>,\n}\n\n#[derive(Debug, Deserialize, Default)]\npub(crate) struct TraceArgs {\n  #[serde(flatten)]\n  pub(crate) common: CommonArgs,\n  pub(crate) run_id: Option<String>,\n  pub(crate) test_id: Option<String>,\n  pub(crate) trace_id: Option<String>,\n  pub(crate) view: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\npub(crate) struct SnapshotArgs {\n  #[serde(flatten)]\n  pub(crate) exact: ExactTestArgs,\n  pub(crate) system: Option<String>,\n  pub(crate) json_pointer: Option<String>,\n}\n\n#[derive(Debug, Deserialize)]\npub(crate) struct RawEvidenceArgs {\n  #[serde(flatten)]\n  pub(crate) common: CommonArgs,\n  pub(crate) kind: String,\n  pub(crate) id: i64,\n  pub(crate) run_id: Option<String>,\n  pub(crate) test_id: Option<String>,\n  pub(crate) trace_id: Option<String>,\n}\n\n#[derive(Debug, Clone, Copy)]\npub(crate) struct Budget {\n  pub(crate) string_chars: usize,\n  pub(crate) raw_string_chars: usize,\n  pub(crate) timeline_events: usize,\n  pub(crate) trace_spans: usize,\n  pub(crate) failed_entries: usize,\n  pub(crate) snapshots: usize,\n}\n\nimpl Budget {\n  pub(crate) fn from_args(name: Option<&str>, max_chars: Option<usize>) -> Self {\n    let budget_name = name\n      .and_then(BudgetValue::from_str)\n      .unwrap_or(BudgetValue::Compact);\n    let mut budget = match budget_name {\n      BudgetValue::Tiny => Self {\n        string_chars: 240,\n        raw_string_chars: 800,\n        timeline_events: 5,\n        trace_spans: 8,\n        failed_entries: 3,\n        snapshots: 3,\n      },\n      BudgetValue::Full => Self {\n        string_chars: 2_000,\n        raw_string_chars: 12_000,\n        timeline_events: 100,\n        trace_spans: 200,\n        failed_entries: 50,\n        snapshots: 50,\n      },\n      BudgetValue::Compact => Self {\n        string_chars: 600,\n        raw_string_chars: 4_000,\n        timeline_events: 15,\n        trace_spans: 40,\n        failed_entries: 10,\n        snapshots: 10,\n      },\n    };\n\n    if let Some(max_chars) = max_chars {\n      let max_chars = max_chars.clamp(120, 20_000);\n      budget.string_chars = budget.string_chars.min(max_chars);\n      budget.raw_string_chars = budget.raw_string_chars.min(max_chars);\n    }\n\n    budget\n  }\n}\n\npub(crate) fn parse<T>(arguments: Value) -> Result<T, String>\nwhere\n  T: for<'de> Deserialize<'de>,\n{\n  serde_json::from_value(arguments).map_err(|error| format!(\"invalid tool arguments: {error}\"))\n}\n"
  },
  {
    "path": "tools/stove-cli/src/mcp/contract.rs",
    "content": "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum MethodName {\n  Initialize,\n  Ping,\n  ToolsList,\n  ToolsCall,\n}\n\nimpl MethodName {\n  pub(crate) fn from_str(value: &str) -> Option<Self> {\n    match value {\n      \"initialize\" => Some(Self::Initialize),\n      \"ping\" => Some(Self::Ping),\n      \"tools/list\" => Some(Self::ToolsList),\n      \"tools/call\" => Some(Self::ToolsCall),\n      _ => None,\n    }\n  }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum ToolName {\n  Apps,\n  Runs,\n  Failures,\n  FailureDetail,\n  Timeline,\n  Trace,\n  Snapshot,\n  RawEvidence,\n}\n\nimpl ToolName {\n  pub(crate) const ALL: [Self; 8] = [\n    Self::Apps,\n    Self::Runs,\n    Self::Failures,\n    Self::FailureDetail,\n    Self::Timeline,\n    Self::Trace,\n    Self::Snapshot,\n    Self::RawEvidence,\n  ];\n\n  pub(crate) const fn as_str(self) -> &'static str {\n    match self {\n      Self::Apps => \"stove_apps\",\n      Self::Runs => \"stove_runs\",\n      Self::Failures => \"stove_failures\",\n      Self::FailureDetail => \"stove_failure_detail\",\n      Self::Timeline => \"stove_timeline\",\n      Self::Trace => \"stove_trace\",\n      Self::Snapshot => \"stove_snapshot\",\n      Self::RawEvidence => \"stove_raw_evidence\",\n    }\n  }\n\n  pub(crate) fn from_str(value: &str) -> Option<Self> {\n    Self::ALL.into_iter().find(|tool| tool.as_str() == value)\n  }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum ArgName {\n  AppName,\n  Budget,\n  Focus,\n  Id,\n  JsonPointer,\n  Kind,\n  Limit,\n  MaxChars,\n  RunId,\n  Status,\n  System,\n  TestId,\n  TraceId,\n  View,\n}\n\nimpl ArgName {\n  pub(crate) const fn as_str(self) -> &'static str {\n    match self {\n      Self::AppName => \"app_name\",\n      Self::Budget => \"budget\",\n      Self::Focus => \"focus\",\n      Self::Id => \"id\",\n      Self::JsonPointer => \"json_pointer\",\n      Self::Kind => \"kind\",\n      Self::Limit => \"limit\",\n      Self::MaxChars => \"max_chars\",\n      Self::RunId => \"run_id\",\n      Self::Status => \"status\",\n      Self::System => \"system\",\n      Self::TestId => \"test_id\",\n      Self::TraceId => \"trace_id\",\n      Self::View => \"view\",\n    }\n  }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum BudgetValue {\n  Tiny,\n  Compact,\n  Full,\n}\n\nimpl BudgetValue {\n  pub(crate) const ALL: [Self; 3] = [Self::Tiny, Self::Compact, Self::Full];\n\n  pub(crate) const fn as_str(self) -> &'static str {\n    match self {\n      Self::Tiny => \"tiny\",\n      Self::Compact => \"compact\",\n      Self::Full => \"full\",\n    }\n  }\n\n  pub(crate) fn from_str(value: &str) -> Option<Self> {\n    Self::ALL\n      .into_iter()\n      .find(|budget| budget.as_str() == value)\n  }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum TimelineFocus {\n  Failure,\n  All,\n}\n\nimpl TimelineFocus {\n  pub(crate) const ALL: [Self; 2] = [Self::Failure, Self::All];\n\n  pub(crate) const fn as_str(self) -> &'static str {\n    match self {\n      Self::Failure => \"failure\",\n      Self::All => \"all\",\n    }\n  }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum TraceView {\n  CriticalPath,\n  Exceptions,\n  Tree,\n}\n\nimpl TraceView {\n  pub(crate) const ALL: [Self; 3] = [Self::CriticalPath, Self::Exceptions, Self::Tree];\n\n  pub(crate) const fn as_str(self) -> &'static str {\n    match self {\n      Self::CriticalPath => \"critical_path\",\n      Self::Exceptions => \"exceptions\",\n      Self::Tree => \"tree\",\n    }\n  }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum RawEvidenceKind {\n  Entry,\n  Span,\n  Snapshot,\n}\n\nimpl RawEvidenceKind {\n  pub(crate) const ALL: [Self; 3] = [Self::Entry, Self::Span, Self::Snapshot];\n\n  pub(crate) const fn as_str(self) -> &'static str {\n    match self {\n      Self::Entry => \"entry\",\n      Self::Span => \"span\",\n      Self::Snapshot => \"snapshot\",\n    }\n  }\n\n  pub(crate) fn from_str(value: &str) -> Option<Self> {\n    Self::ALL.into_iter().find(|kind| kind.as_str() == value)\n  }\n}\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum RunStatusValue {\n  Running,\n  Passed,\n  Failed,\n}\n\nimpl RunStatusValue {\n  pub(crate) const ALL: [Self; 3] = [Self::Running, Self::Passed, Self::Failed];\n\n  pub(crate) const fn as_str(self) -> &'static str {\n    match self {\n      Self::Running => \"RUNNING\",\n      Self::Passed => \"PASSED\",\n      Self::Failed => \"FAILED\",\n    }\n  }\n}\n\npub(crate) const STATUS_ERROR: &str = \"ERROR\";\n"
  },
  {
    "path": "tools/stove-cli/src/mcp/mod.rs",
    "content": "mod analysis;\nmod args;\nmod contract;\nmod protocol;\nmod security;\nmod tools;\n\nuse std::net::SocketAddr;\n\nuse analysis::Analyzer;\nuse axum::body::Bytes;\nuse axum::extract::{ConnectInfo, State};\nuse axum::http::{HeaderMap, StatusCode};\nuse axum::response::{IntoResponse, Response};\nuse serde_json::{Value, json};\n\nuse crate::http::server::AppState;\n\nuse self::contract::MethodName;\nuse self::protocol::{JsonRpcRequest, RpcError, ToolCallParams};\n\npub async fn handle_get(\n  State(_state): State<AppState>,\n  connect_info: ConnectInfo<SocketAddr>,\n  headers: HeaderMap,\n) -> Response {\n  if let Some(response) = security::validate_local_request(&connect_info, &headers) {\n    return response;\n  }\n\n  (\n    StatusCode::METHOD_NOT_ALLOWED,\n    axum::Json(json!({\n      \"error\": \"Stove MCP is a stateless Streamable HTTP endpoint in v1. Use HTTP POST with JSON-RPC requests.\",\n    })),\n  )\n    .into_response()\n}\n\npub async fn handle_post(\n  State(state): State<AppState>,\n  connect_info: ConnectInfo<SocketAddr>,\n  headers: HeaderMap,\n  body: Bytes,\n) -> Response {\n  if let Some(response) = security::validate_local_request(&connect_info, &headers) {\n    return response;\n  }\n\n  if let Some(response) = security::validate_accept_header(&headers) {\n    return response;\n  }\n\n  let request = match serde_json::from_slice::<JsonRpcRequest>(&body) {\n    Ok(request) => request,\n    Err(error) => {\n      return protocol::rpc_error(\n        None,\n        StatusCode::BAD_REQUEST,\n        -32700,\n        \"Parse error\",\n        Some(json!({ \"error\": error.to_string() })),\n      );\n    }\n  };\n\n  let id = request.id.clone();\n  if request.id.is_none() {\n    return StatusCode::ACCEPTED.into_response();\n  }\n\n  match handle_request(state, request).await {\n    Ok(result) => protocol::rpc_result(id, result),\n    Err(error) => protocol::rpc_error(id, StatusCode::OK, error.code, &error.message, error.data),\n  }\n}\n\nasync fn handle_request(state: AppState, request: JsonRpcRequest) -> Result<Value, RpcError> {\n  match MethodName::from_str(&request.method) {\n    Some(MethodName::Initialize) => Ok(protocol::initialize_result()),\n    Some(MethodName::Ping) => Ok(json!({})),\n    Some(MethodName::ToolsList) => Ok(json!({ \"tools\": tools::definitions() })),\n    Some(MethodName::ToolsCall) => {\n      let params: ToolCallParams =\n        serde_json::from_value(request.params.unwrap_or_else(|| json!({}))).map_err(|error| {\n          RpcError::invalid_params(format!(\"invalid tools/call params: {error}\"))\n        })?;\n      let analyzer = Analyzer::new(state.repository, state.ingestor);\n      let arguments = params.arguments.unwrap_or_else(|| json!({}));\n      let output = analyzer\n        .call_tool(&params.name, arguments)\n        .await\n        .map_err(RpcError::tool_error)?;\n      Ok(protocol::tool_result(output))\n    }\n    None => Err(RpcError::method_not_found(format!(\n      \"unsupported MCP method: {}\",\n      request.method\n    ))),\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/mcp/protocol.rs",
    "content": "use axum::http::StatusCode;\nuse axum::response::{IntoResponse, Response};\nuse serde::Deserialize;\nuse serde_json::{Value, json};\n\nuse super::analysis::ToolOutput;\nuse crate::STOVE_CLI_VERSION;\n\nconst PROTOCOL_VERSION: &str = \"2025-06-18\";\n\npub(crate) fn initialize_result() -> Value {\n  json!({\n    \"protocolVersion\": PROTOCOL_VERSION,\n    \"capabilities\": {\n      \"tools\": {\n        \"listChanged\": false\n      }\n    },\n    \"serverInfo\": {\n      \"name\": \"stove\",\n      \"version\": STOVE_CLI_VERSION,\n      \"title\": \"Stove test observability\"\n    },\n    \"instructions\": \"Use Stove MCP to inspect recorded e2e test failures through compact app/run/test scoped tools. If MCP is unavailable, incomplete, or ambiguous, fall back to normal test output, Stove reports, and logs.\"\n  })\n}\n\npub(crate) fn tool_result(output: ToolOutput) -> Value {\n  let ToolOutput { structured, text } = output;\n  json!({\n    \"content\": [\n      {\n        \"type\": \"text\",\n        \"text\": text\n      }\n    ],\n    \"structuredContent\": structured,\n    \"isError\": false\n  })\n}\n\npub(crate) fn rpc_result(id: Option<Value>, result: Value) -> Response {\n  let mut envelope = serde_json::Map::new();\n  envelope.insert(\"jsonrpc\".to_string(), Value::String(\"2.0\".to_string()));\n  envelope.insert(\"id\".to_string(), id.unwrap_or(Value::Null));\n  envelope.insert(\"result\".to_string(), result);\n  (StatusCode::OK, axum::Json(Value::Object(envelope))).into_response()\n}\n\npub(crate) fn rpc_error(\n  id: Option<Value>,\n  status: StatusCode,\n  code: i32,\n  message: &str,\n  data: Option<Value>,\n) -> Response {\n  let mut error = json!({\n    \"code\": code,\n    \"message\": message,\n  });\n  if let Some(data) = data {\n    error[\"data\"] = data;\n  }\n\n  (\n    status,\n    axum::Json(json!({\n      \"jsonrpc\": \"2.0\",\n      \"id\": id.unwrap_or(Value::Null),\n      \"error\": error,\n    })),\n  )\n    .into_response()\n}\n\n#[derive(Debug, Deserialize)]\npub(crate) struct JsonRpcRequest {\n  pub(crate) id: Option<Value>,\n  pub(crate) method: String,\n  pub(crate) params: Option<Value>,\n}\n\n#[derive(Debug, Deserialize)]\npub(crate) struct ToolCallParams {\n  pub(crate) name: String,\n  pub(crate) arguments: Option<Value>,\n}\n\npub(crate) struct RpcError {\n  pub(crate) code: i32,\n  pub(crate) message: String,\n  pub(crate) data: Option<Value>,\n}\n\nimpl RpcError {\n  pub(crate) fn invalid_params(message: String) -> Self {\n    Self {\n      code: -32602,\n      message,\n      data: None,\n    }\n  }\n\n  pub(crate) fn method_not_found(message: String) -> Self {\n    Self {\n      code: -32601,\n      message,\n      data: None,\n    }\n  }\n\n  pub(crate) fn tool_error(message: String) -> Self {\n    Self {\n      code: -32001,\n      message,\n      data: None,\n    }\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/mcp/security.rs",
    "content": "use std::net::{IpAddr, SocketAddr};\n\nuse axum::extract::ConnectInfo;\nuse axum::http::header::{ACCEPT, HOST, ORIGIN};\nuse axum::http::{HeaderMap, StatusCode};\nuse axum::response::Response;\nuse serde_json::json;\n\npub(crate) fn validate_accept_header(headers: &HeaderMap) -> Option<Response> {\n  let accept = headers.get(ACCEPT).and_then(|value| value.to_str().ok())?;\n\n  if accept.contains(\"application/json\")\n    || accept.contains(\"*/*\")\n    || accept.contains(\"text/event-stream\")\n  {\n    None\n  } else {\n    Some(super::protocol::rpc_error(\n      None,\n      StatusCode::NOT_ACCEPTABLE,\n      -32000,\n      \"Not acceptable\",\n      Some(json!({ \"expected_accept\": \"application/json or text/event-stream\" })),\n    ))\n  }\n}\n\npub(crate) fn validate_local_request(\n  connect_info: &ConnectInfo<SocketAddr>,\n  headers: &HeaderMap,\n) -> Option<Response> {\n  let ConnectInfo(addr) = connect_info;\n  if !addr.ip().is_loopback() {\n    return Some(forbidden(\"MCP is only available to loopback clients\"));\n  }\n\n  let host = headers\n    .get(HOST)\n    .and_then(|value| value.to_str().ok())\n    .and_then(host_without_port);\n  if !host.as_deref().is_some_and(is_loopback_host) {\n    return Some(forbidden(\"MCP requests must use a localhost Host header\"));\n  }\n\n  if let Some(origin) = headers.get(ORIGIN).and_then(|value| value.to_str().ok())\n    && !origin_host(origin).is_some_and(|host| is_loopback_host(&host))\n  {\n    return Some(forbidden(\"MCP requests must use a localhost Origin\"));\n  }\n\n  None\n}\n\nfn forbidden(message: &str) -> Response {\n  super::protocol::rpc_error(\n    None,\n    StatusCode::FORBIDDEN,\n    -32000,\n    \"Forbidden\",\n    Some(json!({ \"reason\": message })),\n  )\n}\n\nfn host_without_port(host: &str) -> Option<String> {\n  let host = host.trim();\n  if host.is_empty() {\n    return None;\n  }\n  if let Some(rest) = host.strip_prefix('[') {\n    return rest.find(']').map(|end| rest[..end].to_string());\n  }\n  Some(host.split(':').next().unwrap_or(host).to_string())\n}\n\nfn origin_host(origin: &str) -> Option<String> {\n  let after_scheme = origin.split_once(\"://\").map_or(origin, |(_, rest)| rest);\n  let authority = after_scheme.split('/').next().unwrap_or(after_scheme);\n  host_without_port(authority)\n}\n\nfn is_loopback_host(host: &str) -> bool {\n  host.eq_ignore_ascii_case(\"localhost\")\n    || host\n      .parse::<IpAddr>()\n      .is_ok_and(|ip_address| ip_address.is_loopback())\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn host_parser_handles_ipv6_loopback() {\n    assert_eq!(host_without_port(\"[::1]:4040\").as_deref(), Some(\"::1\"));\n    assert!(is_loopback_host(\"::1\"));\n  }\n\n  #[test]\n  fn origin_parser_rejects_remote_hosts() {\n    assert_eq!(\n      origin_host(\"http://localhost:4040\").as_deref(),\n      Some(\"localhost\")\n    );\n    assert!(\n      !origin_host(\"https://example.com\")\n        .as_deref()\n        .is_some_and(is_loopback_host)\n    );\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/mcp/tools.rs",
    "content": "use serde_json::{Value, json};\n\nuse super::contract::{\n  ArgName, BudgetValue, RawEvidenceKind, RunStatusValue, TimelineFocus, ToolName, TraceView,\n};\n\npub(crate) fn definitions() -> Value {\n  Value::Array(\n    ToolName::ALL\n      .into_iter()\n      .map(ToolSpec::for_tool)\n      .map(|spec| spec.to_json())\n      .collect(),\n  )\n}\n\nstruct ToolSpec {\n  tool: ToolName,\n  description: &'static str,\n  fields: Vec<FieldSpec>,\n}\n\nimpl ToolSpec {\n  fn for_tool(tool: ToolName) -> Self {\n    match tool {\n      ToolName::Apps => Self {\n        tool,\n        description: \"List apps recorded in the Stove dashboard database. Use this when multiple apps may have test runs.\",\n        fields: list_fields(),\n      },\n      ToolName::Runs => Self {\n        tool,\n        description: \"List Stove runs, optionally filtered by app_name and status. run_id is the canonical execution boundary for detail tools.\",\n        fields: vec![\n          FieldSpec::string(ArgName::AppName).description(\"Optional app grouping label.\"),\n          FieldSpec::string_enum(ArgName::Status, SchemaEnum::RunStatus),\n          FieldSpec::limit(),\n          FieldSpec::budget(),\n          FieldSpec::max_chars(),\n        ],\n      },\n      ToolName::Failures => Self {\n        tool,\n        description: \"Default entrypoint for agents. Return failed or errored tests grouped by app and run, with ready-to-use detail tool calls.\",\n        fields: vec![\n          FieldSpec::string(ArgName::AppName)\n            .description(\"Optional app grouping label. Does not uniquely identify a run.\"),\n          FieldSpec::string(ArgName::RunId).description(\"Optional exact run id.\"),\n          FieldSpec::limit(),\n          FieldSpec::budget(),\n          FieldSpec::max_chars(),\n        ],\n      },\n      ToolName::FailureDetail => Self {\n        tool,\n        description: \"Return compact failure, timeline, trace, and snapshot summaries for one exact failed test.\",\n        fields: exact_test_fields(),\n      },\n      ToolName::Timeline => Self {\n        tool,\n        description: \"Return ordered report entries for one exact test. Failure-focused by default.\",\n        fields: with_extra(\n          exact_test_fields(),\n          FieldSpec::string_enum(ArgName::Focus, SchemaEnum::TimelineFocus)\n            .string_default(TimelineFocus::Failure.as_str()),\n        ),\n      },\n      ToolName::Trace => Self {\n        tool,\n        description: \"Return trace evidence by run_id + test_id or explicit trace_id. Multiple trace IDs are ranked with failed-entry traces first.\",\n        fields: vec![\n          FieldSpec::string(ArgName::RunId),\n          FieldSpec::string(ArgName::TestId),\n          FieldSpec::string(ArgName::TraceId),\n          FieldSpec::string_enum(ArgName::View, SchemaEnum::TraceView)\n            .string_default(TraceView::CriticalPath.as_str()),\n          FieldSpec::budget(),\n          FieldSpec::max_chars(),\n        ],\n      },\n      ToolName::Snapshot => Self {\n        tool,\n        description: \"Return snapshot summaries and targeted state drill-down for one exact test.\",\n        fields: with_extra(\n          with_extra(\n            exact_test_fields(),\n            FieldSpec::string(ArgName::System)\n              .description(\"Optional system name such as Kafka or WireMock.\"),\n          ),\n          FieldSpec::string(ArgName::JsonPointer).description(\n            \"Optional RFC 6901 JSON pointer into snapshot state, for example /published/0.\",\n          ),\n        ),\n      },\n      ToolName::RawEvidence => Self {\n        tool,\n        description: \"Explicit capped raw evidence lookup by entry, span, or snapshot id. Prefer summary tools first.\",\n        fields: vec![\n          FieldSpec::string_enum(ArgName::Kind, SchemaEnum::RawEvidenceKind).required(),\n          FieldSpec::integer(ArgName::Id).required(),\n          FieldSpec::string(ArgName::RunId),\n          FieldSpec::string(ArgName::TestId),\n          FieldSpec::string(ArgName::TraceId),\n          FieldSpec::budget(),\n          FieldSpec::max_chars(),\n        ],\n      },\n    }\n  }\n\n  fn to_json(&self) -> Value {\n    json!({\n      \"name\": self.tool.as_str(),\n      \"description\": self.description,\n      \"inputSchema\": InputSchema::from_fields(&self.fields).to_json(),\n    })\n  }\n}\n\nstruct InputSchema {\n  fields: Vec<FieldSpec>,\n}\n\nimpl InputSchema {\n  fn from_fields(fields: &[FieldSpec]) -> Self {\n    Self {\n      fields: fields.to_vec(),\n    }\n  }\n\n  fn to_json(&self) -> Value {\n    let properties = Value::Object(self.fields.iter().map(FieldSpec::property_entry).collect());\n    let required: Vec<&str> = self\n      .fields\n      .iter()\n      .filter(|field| field.required)\n      .map(|field| field.name.as_str())\n      .collect();\n\n    json!({\n      \"type\": \"object\",\n      \"properties\": properties,\n      \"required\": required,\n      \"additionalProperties\": false,\n    })\n  }\n}\n\n#[derive(Clone)]\nstruct FieldSpec {\n  name: ArgName,\n  kind: FieldKind,\n  required: bool,\n  description: Option<&'static str>,\n  default: Option<Value>,\n}\n\nimpl FieldSpec {\n  fn string(name: ArgName) -> Self {\n    Self::new(name, FieldKind::String)\n  }\n\n  fn string_enum(name: ArgName, enum_kind: SchemaEnum) -> Self {\n    Self::new(name, FieldKind::StringEnum(enum_kind))\n  }\n\n  fn integer(name: ArgName) -> Self {\n    Self::new(name, FieldKind::Integer)\n  }\n\n  fn limit() -> Self {\n    Self::integer(ArgName::Limit)\n      .minimum(1)\n      .maximum(100)\n      .integer_default(20)\n  }\n\n  fn budget() -> Self {\n    Self::string_enum(ArgName::Budget, SchemaEnum::Budget)\n      .string_default(BudgetValue::Compact.as_str())\n  }\n\n  fn max_chars() -> Self {\n    Self::integer(ArgName::MaxChars)\n      .minimum(120)\n      .maximum(20_000)\n      .description(\"Maximum characters for individual large evidence fields.\")\n  }\n\n  fn new(name: ArgName, kind: FieldKind) -> Self {\n    Self {\n      name,\n      kind,\n      required: false,\n      description: None,\n      default: None,\n    }\n  }\n\n  fn required(mut self) -> Self {\n    self.required = true;\n    self\n  }\n\n  fn description(mut self, description: &'static str) -> Self {\n    self.description = Some(description);\n    self\n  }\n\n  fn string_default(mut self, default: &'static str) -> Self {\n    self.default = Some(json!(default));\n    self\n  }\n\n  fn integer_default(mut self, default: i64) -> Self {\n    self.default = Some(json!(default));\n    self\n  }\n\n  fn minimum(mut self, minimum: i64) -> Self {\n    if let FieldKind::Integer = &mut self.kind {\n      self.kind = FieldKind::IntegerWithBounds {\n        minimum: Some(minimum),\n        maximum: None,\n      };\n    } else if let FieldKind::IntegerWithBounds { minimum: slot, .. } = &mut self.kind {\n      *slot = Some(minimum);\n    }\n    self\n  }\n\n  fn maximum(mut self, maximum: i64) -> Self {\n    if let FieldKind::Integer = &mut self.kind {\n      self.kind = FieldKind::IntegerWithBounds {\n        minimum: None,\n        maximum: Some(maximum),\n      };\n    } else if let FieldKind::IntegerWithBounds { maximum: slot, .. } = &mut self.kind {\n      *slot = Some(maximum);\n    }\n    self\n  }\n\n  fn property_entry(&self) -> (String, Value) {\n    let mut property = match self.kind {\n      FieldKind::String => json!({ \"type\": \"string\" }),\n      FieldKind::StringEnum(enum_kind) => json!({\n        \"type\": \"string\",\n        \"enum\": enum_kind.values(),\n      }),\n      FieldKind::Integer => json!({ \"type\": \"integer\" }),\n      FieldKind::IntegerWithBounds { minimum, maximum } => {\n        let mut property = json!({ \"type\": \"integer\" });\n        if let Some(minimum) = minimum {\n          property[\"minimum\"] = json!(minimum);\n        }\n        if let Some(maximum) = maximum {\n          property[\"maximum\"] = json!(maximum);\n        }\n        property\n      }\n    };\n\n    if let Some(description) = self.description {\n      property[\"description\"] = json!(description);\n    }\n    if let Some(default) = &self.default {\n      property[\"default\"] = default.clone();\n    }\n    (self.name.as_str().to_string(), property)\n  }\n}\n\n#[derive(Debug, Clone, Copy)]\nenum FieldKind {\n  String,\n  StringEnum(SchemaEnum),\n  Integer,\n  IntegerWithBounds {\n    minimum: Option<i64>,\n    maximum: Option<i64>,\n  },\n}\n\n#[derive(Debug, Clone, Copy)]\nenum SchemaEnum {\n  Budget,\n  TimelineFocus,\n  TraceView,\n  RawEvidenceKind,\n  RunStatus,\n}\n\nimpl SchemaEnum {\n  fn values(self) -> Vec<&'static str> {\n    match self {\n      Self::Budget => BudgetValue::ALL\n        .into_iter()\n        .map(BudgetValue::as_str)\n        .collect(),\n      Self::TimelineFocus => TimelineFocus::ALL\n        .into_iter()\n        .map(TimelineFocus::as_str)\n        .collect(),\n      Self::TraceView => TraceView::ALL.into_iter().map(TraceView::as_str).collect(),\n      Self::RawEvidenceKind => RawEvidenceKind::ALL\n        .into_iter()\n        .map(RawEvidenceKind::as_str)\n        .collect(),\n      Self::RunStatus => RunStatusValue::ALL\n        .into_iter()\n        .map(RunStatusValue::as_str)\n        .collect(),\n    }\n  }\n}\n\nfn list_fields() -> Vec<FieldSpec> {\n  vec![\n    FieldSpec::limit(),\n    FieldSpec::budget(),\n    FieldSpec::max_chars(),\n  ]\n}\n\nfn exact_test_fields() -> Vec<FieldSpec> {\n  vec![\n    FieldSpec::string(ArgName::RunId)\n      .required()\n      .description(\"Exact Stove run id. This is the canonical execution boundary.\"),\n    FieldSpec::string(ArgName::TestId)\n      .required()\n      .description(\"Exact Stove test id. Unique only within run_id.\"),\n    FieldSpec::budget(),\n    FieldSpec::max_chars(),\n  ]\n}\n\nfn with_extra(mut fields: Vec<FieldSpec>, field: FieldSpec) -> Vec<FieldSpec> {\n  fields.push(field);\n  fields\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n\n  #[test]\n  fn definitions_include_one_schema_per_tool() {\n    let definitions = definitions();\n    let tools = definitions.as_array().unwrap();\n\n    assert_eq!(tools.len(), ToolName::ALL.len());\n    assert_eq!(tools[0][\"name\"], ToolName::Apps.as_str());\n    assert_eq!(\n      tools[ToolName::ALL.len() - 1][\"name\"],\n      ToolName::RawEvidence.as_str()\n    );\n  }\n\n  #[test]\n  fn exact_test_tools_require_run_and_test_ids() {\n    let detail = ToolSpec::for_tool(ToolName::FailureDetail).to_json();\n    let required = detail[\"inputSchema\"][\"required\"].as_array().unwrap();\n\n    assert!(required.contains(&json!(ArgName::RunId.as_str())));\n    assert!(required.contains(&json!(ArgName::TestId.as_str())));\n  }\n\n  #[test]\n  fn typed_fields_preserve_defaults_and_enums() {\n    let apps = ToolSpec::for_tool(ToolName::Apps).to_json();\n    let limit = &apps[\"inputSchema\"][\"properties\"][ArgName::Limit.as_str()];\n    let budget = &apps[\"inputSchema\"][\"properties\"][ArgName::Budget.as_str()];\n\n    assert_eq!(limit[\"default\"], 20);\n    assert_eq!(budget[\"default\"], BudgetValue::Compact.as_str());\n    assert_eq!(\n      budget[\"enum\"],\n      json!([\n        BudgetValue::Tiny.as_str(),\n        BudgetValue::Compact.as_str(),\n        BudgetValue::Full.as_str()\n      ])\n    );\n  }\n\n  #[test]\n  fn raw_evidence_schema_requires_kind_and_id() {\n    let raw = ToolSpec::for_tool(ToolName::RawEvidence).to_json();\n    let required = raw[\"inputSchema\"][\"required\"].as_array().unwrap();\n    let kind = &raw[\"inputSchema\"][\"properties\"][ArgName::Kind.as_str()];\n\n    assert!(required.contains(&json!(ArgName::Kind.as_str())));\n    assert!(required.contains(&json!(ArgName::Id.as_str())));\n    assert_eq!(\n      kind[\"enum\"],\n      json!([\n        RawEvidenceKind::Entry.as_str(),\n        RawEvidenceKind::Span.as_str(),\n        RawEvidenceKind::Snapshot.as_str()\n      ])\n    );\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/skills.rs",
    "content": "//! Stove agent skills sync.\n//!\n//! Discovers the local Stove skill directory under a project (preferring\n//! `.agents/skills/stove`, falling back to `.claude/skills/stove` and\n//! `.agent/skills/stove`), compares it against the canonical copy on GitHub,\n//! and offers to install or update.\n//!\n//! Network and prompt behavior is conservative by default:\n//! - never modifies anything without explicit user consent on a TTY\n//! - falls back to a non-blocking warning on network/API errors\n//! - skips entirely when started outside a git repository on plain `stove`\n\nuse std::collections::BTreeMap;\nuse std::io::{IsTerminal, Write};\nuse std::path::{Path, PathBuf};\n\nuse serde::Deserialize;\nuse tokio::task::JoinSet;\n\nuse crate::config::{Config, SkillsCommand, StoveCommand};\n\n/// GitHub source coordinates and HTTP defaults.\nmod github {\n  use std::time::Duration;\n\n  pub const REPO_OWNER: &str = \"Trendyol\";\n  pub const REPO_NAME: &str = \"stove\";\n  pub const REPO_REF: &str = \"main\";\n  pub const USER_AGENT: &str = concat!(\"stove-cli/\", env!(\"STOVE_VERSION\"));\n  /// Capped low so a slow GitHub call cannot stall server bind on cold start.\n  pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);\n}\n\n/// Candidate skill directory paths, probed in order both locally and remotely.\n/// First entry is the preferred vendor-neutral path used as the install\n/// default when nothing exists yet.\nconst SKILL_PATHS: &[&str] = &[\n  \".agents/skills/stove\",\n  \".claude/skills/stove\",\n  \".agent/skills/stove\",\n];\n\n/// Handle a `skills` subcommand if one was requested.\n///\n/// Returns `Ok(true)` when a subcommand was handled and the CLI should exit;\n/// `Ok(false)` when no subcommand was specified.\npub async fn handle_skills_command(config: &Config) -> anyhow::Result<bool> {\n  let Some(StoveCommand::Skills { command }) = &config.command else {\n    return Ok(false);\n  };\n  match command {\n    SkillsCommand::Install { force } => install_skills_command(*force).await?,\n  }\n  Ok(true)\n}\n\n/// Suggest or apply a skills update during normal startup.\n///\n/// Network and IO failures are reported and never abort startup.\npub async fn maybe_update_skills(config: &Config) {\n  if config.no_skills_check {\n    return;\n  }\n\n  let Some(repo_root) = current_git_root() else {\n    println!(\n      \"  Stove skills can be installed from your project root: cd <your-repo> && stove skills install\"\n    );\n    return;\n  };\n\n  let target = resolve_local_target(&repo_root);\n\n  match decide_sync_action(&target, config.update_skills).await {\n    SyncAction::Skip(reason) => {\n      if let Some(message) = reason {\n        eprintln!(\"  warning: skipping Stove skills check ({message})\");\n      }\n    }\n    SyncAction::Apply(remote) => apply_install(&target, &remote),\n    SyncAction::Prompt(remote) => {\n      match prompt_yes_no(\"  Install/update Stove agent skills from GitHub? [y/N] \") {\n        Ok(true) => apply_install(&target, &remote),\n        Ok(false) => {}\n        Err(err) => eprintln!(\"  warning: skills prompt failed: {err}\"),\n      }\n    }\n  }\n}\n\n/// What to do once we know the local target and the remote snapshot.\nenum SyncAction {\n  /// Nothing to do. `Some(reason)` surfaces a recoverable error to the user.\n  Skip(Option<String>),\n  /// Install without prompting (`--update-skills` or non-TTY in some flows).\n  Apply(RemoteSkills),\n  /// TTY user-facing prompt path.\n  Prompt(RemoteSkills),\n}\n\nasync fn decide_sync_action(target: &Path, force_update: bool) -> SyncAction {\n  let remote = match fetch_remote_skills().await {\n    Ok(remote) => remote,\n    Err(err) => return SyncAction::Skip(Some(err.to_string())),\n  };\n  if remote.is_empty() || skills_match(target, &remote) {\n    return SyncAction::Skip(None);\n  }\n  if force_update {\n    SyncAction::Apply(remote)\n  } else if std::io::stdin().is_terminal() {\n    SyncAction::Prompt(remote)\n  } else {\n    SyncAction::Skip(None)\n  }\n}\n\nfn apply_install(target: &Path, remote: &RemoteSkills) {\n  match install_skills(target, remote) {\n    Ok(()) => println!(\"  Updated Stove agent skills at {}\", target.display()),\n    Err(err) => eprintln!(\"  warning: failed to install Stove skills: {err}\"),\n  }\n}\n\n/// `stove skills install` execution path.\n///\n/// Without `--force`: requires a git repository and installs into the resolved\n/// target under the repo root. With `--force`: skips git detection and\n/// installs into the resolved target relative to the current directory.\nasync fn install_skills_command(force: bool) -> anyhow::Result<()> {\n  let cwd = std::env::current_dir()?;\n  let target = if force {\n    resolve_local_target(&cwd)\n  } else {\n    let repo_root = find_git_root(&cwd).ok_or_else(|| {\n      anyhow::anyhow!(\n        \"stove skills install must be run inside a git repository (use --force to install in the current directory)\"\n      )\n    })?;\n    resolve_local_target(&repo_root)\n  };\n\n  let remote = fetch_remote_skills().await?;\n  if remote.is_empty() {\n    anyhow::bail!(\"no Stove skills found in remote repository at any known path\");\n  }\n\n  install_skills(&target, &remote)?;\n  println!(\n    \"Installed {} skill files at {}\",\n    remote.len(),\n    target.display()\n  );\n  Ok(())\n}\n\nfn current_git_root() -> Option<PathBuf> {\n  let cwd = std::env::current_dir().ok()?;\n  find_git_root(&cwd)\n}\n\n/// Walk up from `start` until a directory containing a `.git` entry is found.\n/// `.git` may be a directory (regular repo) or a file (worktree / submodule).\n#[must_use]\npub fn find_git_root(start: &Path) -> Option<PathBuf> {\n  start\n    .ancestors()\n    .find(|dir| dir.join(\".git\").exists())\n    .map(Path::to_path_buf)\n}\n\n/// Resolve the local skill target for installation.\n///\n/// Picks the first existing skill directory in [`SKILL_PATHS`]. If none\n/// exist, returns the first candidate (the vendor-neutral default).\n#[must_use]\npub fn resolve_local_target(root: &Path) -> PathBuf {\n  SKILL_PATHS\n    .iter()\n    .map(|candidate| root.join(candidate))\n    .find(|path| path.is_dir())\n    .unwrap_or_else(|| root.join(SKILL_PATHS[0]))\n}\n\n/// Snapshot of the canonical Stove skill directory on GitHub.\n#[derive(Debug, Clone, Default)]\npub struct RemoteSkills {\n  files: BTreeMap<String, Vec<u8>>,\n}\n\nimpl RemoteSkills {\n  #[must_use]\n  pub fn is_empty(&self) -> bool {\n    self.files.is_empty()\n  }\n\n  #[must_use]\n  pub fn len(&self) -> usize {\n    self.files.len()\n  }\n\n  pub fn iter(&self) -> impl Iterator<Item = (&String, &Vec<u8>)> {\n    self.files.iter()\n  }\n}\n\n#[derive(Debug, Deserialize)]\nstruct ContentsEntry {\n  name: String,\n  #[serde(rename = \"type\")]\n  kind: String,\n  download_url: Option<String>,\n}\n\n/// Fetch the Stove skill files from GitHub.\n///\n/// Probes [`SKILL_PATHS`] in order and uses the first path that returns a\n/// non-empty directory listing.\nasync fn fetch_remote_skills() -> anyhow::Result<RemoteSkills> {\n  let client = reqwest::Client::builder()\n    .user_agent(github::USER_AGENT)\n    .timeout(github::REQUEST_TIMEOUT)\n    .build()?;\n\n  for remote_path in SKILL_PATHS {\n    match fetch_remote_skills_for_path(&client, remote_path).await {\n      Ok(snapshot) if !snapshot.is_empty() => return Ok(snapshot),\n      Ok(_) => {}\n      Err(err) => tracing::debug!(\"remote skills probe failed for {remote_path}: {err}\"),\n    }\n  }\n  anyhow::bail!(\"no Stove skills found in remote repository at any known path\");\n}\n\nasync fn fetch_remote_skills_for_path(\n  client: &reqwest::Client,\n  remote_path: &str,\n) -> anyhow::Result<RemoteSkills> {\n  let listing_url = format!(\n    \"https://api.github.com/repos/{owner}/{repo}/contents/{path}?ref={ref_}\",\n    owner = github::REPO_OWNER,\n    repo = github::REPO_NAME,\n    path = remote_path,\n    ref_ = github::REPO_REF,\n  );\n  let response = client.get(&listing_url).send().await?;\n  if response.status() == reqwest::StatusCode::NOT_FOUND {\n    return Ok(RemoteSkills::default());\n  }\n  let entries: Vec<ContentsEntry> = response.error_for_status()?.json().await?;\n\n  let mut downloads: JoinSet<anyhow::Result<(String, Vec<u8>)>> = JoinSet::new();\n  for entry in entries {\n    if entry.kind != \"file\" {\n      continue;\n    }\n    let Some(url) = entry.download_url else {\n      continue;\n    };\n    let client = client.clone();\n    let name = entry.name;\n    downloads.spawn(async move {\n      let body = client\n        .get(&url)\n        .send()\n        .await?\n        .error_for_status()?\n        .bytes()\n        .await?;\n      Ok((name, body.to_vec()))\n    });\n  }\n\n  let mut files = BTreeMap::new();\n  while let Some(result) = downloads.join_next().await {\n    let (name, bytes) = result??;\n    files.insert(name, bytes);\n  }\n  Ok(RemoteSkills { files })\n}\n\n/// Compare an existing local target directory against a remote snapshot.\n///\n/// Matches when the target exists and contains exactly the same set of files\n/// with byte-identical contents. Local files outside the remote set are\n/// considered drift and force a mismatch (so a clean install replaces them).\n#[must_use]\npub fn skills_match(target: &Path, remote: &RemoteSkills) -> bool {\n  let Some(local) = read_local_files(target) else {\n    return false;\n  };\n  local == remote.files\n}\n\nfn read_local_files(target: &Path) -> Option<BTreeMap<String, Vec<u8>>> {\n  if !target.is_dir() {\n    return None;\n  }\n  std::fs::read_dir(target)\n    .ok()?\n    .flatten()\n    .map(|entry| entry.path())\n    .filter(|path| path.is_file())\n    .map(|path| {\n      let name = path.file_name()?.to_str()?.to_string();\n      let bytes = std::fs::read(&path).ok()?;\n      Some((name, bytes))\n    })\n    .collect()\n}\n\n/// Replace the target directory with the remote snapshot.\n///\n/// Writes files into a sibling staging directory first, then performs a\n/// best-effort atomic swap (move-aside-old + rename-staging-into-place).\npub fn install_skills(target: &Path, remote: &RemoteSkills) -> anyhow::Result<()> {\n  let parent = target\n    .parent()\n    .ok_or_else(|| anyhow::anyhow!(\"invalid skills target: {}\", target.display()))?;\n  std::fs::create_dir_all(parent)?;\n\n  let target_name = target\n    .file_name()\n    .and_then(|n| n.to_str())\n    .unwrap_or(\"stove\");\n  let timestamp = chrono::Local::now().format(\"%Y%m%d-%H%M%S%3f\");\n  let staging = parent.join(format!(\".{target_name}-staging-{timestamp}\"));\n  let aside = parent.join(format!(\".{target_name}-old-{timestamp}\"));\n\n  write_staging(&staging, remote)?;\n  swap_into_place(&staging, target, &aside)\n}\n\nfn write_staging(staging: &Path, remote: &RemoteSkills) -> anyhow::Result<()> {\n  // remove_dir_all on a missing path returns NotFound — discard intentionally.\n  let _ = std::fs::remove_dir_all(staging);\n  std::fs::create_dir_all(staging)?;\n  for (name, bytes) in remote.iter() {\n    std::fs::write(staging.join(name), bytes)?;\n  }\n  Ok(())\n}\n\nfn swap_into_place(staging: &Path, target: &Path, aside: &Path) -> anyhow::Result<()> {\n  let target_existed = target.exists();\n  if target_existed {\n    std::fs::rename(target, aside)?;\n  }\n\n  match std::fs::rename(staging, target) {\n    Ok(()) => {\n      if target_existed {\n        let _ = std::fs::remove_dir_all(aside);\n      }\n      Ok(())\n    }\n    Err(err) => {\n      if target_existed {\n        let _ = std::fs::rename(aside, target);\n      }\n      let _ = std::fs::remove_dir_all(staging);\n      Err(anyhow::anyhow!(\"failed to install skills: {err}\"))\n    }\n  }\n}\n\nfn prompt_yes_no(message: &str) -> anyhow::Result<bool> {\n  print!(\"{message}\");\n  std::io::stdout().flush()?;\n  let mut input = String::new();\n  std::io::stdin().read_line(&mut input)?;\n  Ok(matches!(\n    input.trim().to_ascii_lowercase().as_str(),\n    \"y\" | \"yes\"\n  ))\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n  use std::fs;\n  use tempfile::TempDir;\n\n  fn remote_with(files: &[(&str, &[u8])]) -> RemoteSkills {\n    RemoteSkills {\n      files: files\n        .iter()\n        .map(|(name, bytes)| ((*name).to_string(), bytes.to_vec()))\n        .collect(),\n    }\n  }\n\n  #[test]\n  fn find_git_root_detects_directory() {\n    let dir = TempDir::new().unwrap();\n    fs::create_dir(dir.path().join(\".git\")).unwrap();\n    let nested = dir.path().join(\"a/b/c\");\n    fs::create_dir_all(&nested).unwrap();\n\n    let root = find_git_root(&nested).unwrap();\n    assert_eq!(root, dir.path());\n  }\n\n  #[test]\n  fn find_git_root_detects_file_marker() {\n    let dir = TempDir::new().unwrap();\n    fs::write(dir.path().join(\".git\"), \"gitdir: /elsewhere\\n\").unwrap();\n    let nested = dir.path().join(\"nested\");\n    fs::create_dir_all(&nested).unwrap();\n\n    let root = find_git_root(&nested).unwrap();\n    assert_eq!(root, dir.path());\n  }\n\n  #[test]\n  fn find_git_root_returns_none_when_absent() {\n    let dir = TempDir::new().unwrap();\n    let nested = dir.path().join(\"a/b\");\n    fs::create_dir_all(&nested).unwrap();\n    assert!(find_git_root(&nested).is_none());\n  }\n\n  #[test]\n  fn resolve_local_target_prefers_existing_agents() {\n    let dir = TempDir::new().unwrap();\n    let agents = dir.path().join(\".agents/skills/stove\");\n    fs::create_dir_all(&agents).unwrap();\n    fs::create_dir_all(dir.path().join(\".claude/skills/stove\")).unwrap();\n\n    let target = resolve_local_target(dir.path());\n    assert_eq!(target, agents);\n  }\n\n  #[test]\n  fn resolve_local_target_falls_back_to_claude() {\n    let dir = TempDir::new().unwrap();\n    let claude = dir.path().join(\".claude/skills/stove\");\n    fs::create_dir_all(&claude).unwrap();\n\n    let target = resolve_local_target(dir.path());\n    assert_eq!(target, claude);\n  }\n\n  #[test]\n  fn resolve_local_target_defaults_to_agents_when_none_exist() {\n    let dir = TempDir::new().unwrap();\n    let target = resolve_local_target(dir.path());\n    assert_eq!(target, dir.path().join(\".agents/skills/stove\"));\n  }\n\n  #[test]\n  fn skills_match_detects_missing_target() {\n    let dir = TempDir::new().unwrap();\n    let target = dir.path().join(\"missing\");\n    let remote = remote_with(&[(\"a.md\", b\"hello\")]);\n    assert!(!skills_match(&target, &remote));\n  }\n\n  #[test]\n  fn skills_match_returns_true_for_identical_dirs() {\n    let dir = TempDir::new().unwrap();\n    let target = dir.path().join(\"local\");\n    fs::create_dir_all(&target).unwrap();\n    fs::write(target.join(\"a.md\"), b\"hello\").unwrap();\n    fs::write(target.join(\"b.md\"), b\"world\").unwrap();\n\n    let remote = remote_with(&[(\"a.md\", b\"hello\"), (\"b.md\", b\"world\")]);\n    assert!(skills_match(&target, &remote));\n  }\n\n  #[test]\n  fn skills_match_detects_content_drift() {\n    let dir = TempDir::new().unwrap();\n    let target = dir.path().join(\"local\");\n    fs::create_dir_all(&target).unwrap();\n    fs::write(target.join(\"a.md\"), b\"old content\").unwrap();\n    let remote = remote_with(&[(\"a.md\", b\"new content\")]);\n    assert!(!skills_match(&target, &remote));\n  }\n\n  #[test]\n  fn skills_match_detects_extra_local_file() {\n    let dir = TempDir::new().unwrap();\n    let target = dir.path().join(\"local\");\n    fs::create_dir_all(&target).unwrap();\n    fs::write(target.join(\"a.md\"), b\"hello\").unwrap();\n    fs::write(target.join(\"stale.md\"), b\"orphan\").unwrap();\n    let remote = remote_with(&[(\"a.md\", b\"hello\")]);\n    assert!(!skills_match(&target, &remote));\n  }\n\n  #[test]\n  fn install_skills_creates_target_when_missing() {\n    let dir = TempDir::new().unwrap();\n    let target = dir.path().join(\"nested/.agents/skills/stove\");\n    let remote = remote_with(&[(\"a.md\", b\"hello\"), (\"b.md\", b\"world\")]);\n\n    install_skills(&target, &remote).unwrap();\n\n    assert_eq!(fs::read(target.join(\"a.md\")).unwrap(), b\"hello\");\n    assert_eq!(fs::read(target.join(\"b.md\")).unwrap(), b\"world\");\n  }\n\n  #[test]\n  fn install_skills_replaces_existing_target() {\n    let dir = TempDir::new().unwrap();\n    let target = dir.path().join(\".agents/skills/stove\");\n    fs::create_dir_all(&target).unwrap();\n    fs::write(target.join(\"old.md\"), b\"obsolete\").unwrap();\n    fs::write(target.join(\"a.md\"), b\"old\").unwrap();\n\n    let remote = remote_with(&[(\"a.md\", b\"new\"), (\"b.md\", b\"fresh\")]);\n    install_skills(&target, &remote).unwrap();\n\n    assert_eq!(fs::read(target.join(\"a.md\")).unwrap(), b\"new\");\n    assert_eq!(fs::read(target.join(\"b.md\")).unwrap(), b\"fresh\");\n    assert!(!target.join(\"old.md\").exists());\n  }\n\n  #[test]\n  fn install_skills_cleans_up_staging_artifacts() {\n    let dir = TempDir::new().unwrap();\n    let target = dir.path().join(\".agents/skills/stove\");\n    let remote = remote_with(&[(\"a.md\", b\"hello\")]);\n    install_skills(&target, &remote).unwrap();\n\n    let parent = target.parent().unwrap();\n    let leftovers: Vec<_> = fs::read_dir(parent)\n      .unwrap()\n      .flatten()\n      .filter(|entry| {\n        let name = entry.file_name().to_string_lossy().into_owned();\n        name.contains(\"-staging-\") || name.contains(\"-old-\")\n      })\n      .collect();\n    assert!(leftovers.is_empty(), \"staging dirs should be removed\");\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/sse/manager.rs",
    "content": "use tokio::sync::broadcast;\n\n/// Manages SSE (Server-Sent Events) broadcasting to connected browser clients.\n///\n/// Uses `tokio::sync::broadcast` so multiple SSE clients each get their own receiver.\n/// Events are JSON-serialized dashboard events.\npub struct SseManager {\n  sender: broadcast::Sender<String>,\n}\n\nimpl SseManager {\n  #[must_use]\n  pub fn new() -> Self {\n    let (sender, _) = broadcast::channel(4096);\n    Self { sender }\n  }\n\n  /// Broadcast a JSON event to all connected SSE clients.\n  ///\n  /// Ignores `SendError` (no subscribers is fine — nobody is listening yet).\n  pub fn broadcast(&self, json: &str) {\n    if let Err(e) = self.sender.send(json.to_string()) {\n      tracing::debug!(\"No SSE subscribers to broadcast to: {e}\");\n    }\n  }\n\n  /// Create a new receiver for SSE clients to subscribe to.\n  #[must_use]\n  pub fn subscribe(&self) -> broadcast::Receiver<String> {\n    self.sender.subscribe()\n  }\n}\n\nimpl Default for SseManager {\n  fn default() -> Self {\n    Self::new()\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/sse/mod.rs",
    "content": "pub mod manager;\n"
  },
  {
    "path": "tools/stove-cli/src/storage/database.rs",
    "content": "use crate::error::Result;\nuse rusqlite::{Connection, OpenFlags};\nuse std::sync::atomic::{AtomicUsize, Ordering};\nuse tracing::info;\n\n/// Versioned SQL migrations, embedded at compile time.\n/// Add new migrations by creating `V{N}__description.sql` in `src/storage/migrations/`.\n/// Once deployed, never modify an existing migration — append a new one instead.\nconst MIGRATIONS: &[(&str, &str)] = &[\n  (\n    \"V1__initial_schema\",\n    include_str!(\"migrations/V1__initial_schema.sql\"),\n  ),\n  (\n    \"V2__run_stove_version\",\n    include_str!(\"migrations/V2__run_stove_version.sql\"),\n  ),\n  (\n    \"V3__test_path\",\n    include_str!(\"migrations/V3__test_path.sql\"),\n  ),\n];\n\n/// `SQLite` database wrapper with WAL mode and versioned schema migrations.\npub struct Database {\n  path: String,\n  use_uri: bool,\n  conn: Connection,\n}\n\nimpl Database {\n  /// Open (or create) the database at the given path.\n  ///\n  /// Uses WAL mode for concurrent reads and runs versioned migrations on startup.\n  pub fn open(path: &str) -> Result<Self> {\n    let (path, use_uri) = normalize_db_path(path);\n    let conn = open_connection(&path, use_uri)?;\n    apply_pragmas(&conn, &path)?;\n\n    run_migrations(&conn)?;\n\n    Ok(Self {\n      path,\n      use_uri,\n      conn,\n    })\n  }\n\n  /// Returns a reference to the underlying connection.\n  pub fn conn(&self) -> &Connection {\n    &self.conn\n  }\n\n  /// Returns a mutable reference to the underlying connection.\n  pub fn conn_mut(&mut self) -> &mut Connection {\n    &mut self.conn\n  }\n\n  /// Open another connection to the same database.\n  ///\n  /// The CLI uses this to isolate read traffic from the write path so the UI can\n  /// keep polling while gRPC ingestion is busy.\n  pub fn open_peer(&self) -> Result<Self> {\n    let conn = open_connection(&self.path, self.use_uri)?;\n    apply_pragmas(&conn, &self.path)?;\n    Ok(Self {\n      path: self.path.clone(),\n      use_uri: self.use_uri,\n      conn,\n    })\n  }\n}\n\n/// Run versioned migrations that haven't been applied yet.\n///\n/// Tracks applied migrations in a `schema_migrations` table. Each migration is\n/// applied inside a transaction and recorded with its name and version number.\nfn run_migrations(conn: &Connection) -> Result<()> {\n  conn.execute_batch(\n    \"CREATE TABLE IF NOT EXISTS schema_migrations (\n            version INTEGER PRIMARY KEY,\n            name TEXT NOT NULL,\n            applied_at TEXT NOT NULL DEFAULT (datetime('now'))\n        );\",\n  )?;\n\n  let current_version: i64 = conn.query_row(\n    \"SELECT COALESCE(MAX(version), 0) FROM schema_migrations\",\n    [],\n    |row| row.get(0),\n  )?;\n\n  for (i, (name, sql)) in MIGRATIONS.iter().enumerate() {\n    #[allow(clippy::cast_possible_wrap)]\n    let version = (i + 1) as i64;\n    if version <= current_version {\n      continue;\n    }\n\n    let tx = conn.unchecked_transaction()?;\n    tx.execute_batch(sql)?;\n    tx.execute(\n      \"INSERT INTO schema_migrations (version, name) VALUES (?1, ?2)\",\n      rusqlite::params![version, name],\n    )?;\n    tx.commit()?;\n\n    info!(version, name, \"Applied migration\");\n  }\n\n  Ok(())\n}\n\nfn normalize_db_path(path: &str) -> (String, bool) {\n  if path == \":memory:\" {\n    let id = IN_MEMORY_DB_COUNTER.fetch_add(1, Ordering::Relaxed);\n    (\n      format!(\"file:stove-test-{id}?mode=memory&cache=shared\"),\n      true,\n    )\n  } else {\n    (path.to_string(), false)\n  }\n}\n\nfn open_connection(path: &str, use_uri: bool) -> Result<Connection> {\n  let conn = if use_uri {\n    Connection::open_with_flags(\n      path,\n      OpenFlags::SQLITE_OPEN_READ_WRITE\n        | OpenFlags::SQLITE_OPEN_CREATE\n        | OpenFlags::SQLITE_OPEN_URI,\n    )?\n  } else {\n    Connection::open(path)?\n  };\n  Ok(conn)\n}\n\nfn apply_pragmas(conn: &Connection, path: &str) -> Result<()> {\n  if path.starts_with(\"file:stove-test-\") {\n    conn.execute_batch(\"PRAGMA foreign_keys=ON;\")?;\n  } else {\n    conn.execute_batch(\"PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;\")?;\n  }\n  Ok(())\n}\n\nstatic IN_MEMORY_DB_COUNTER: AtomicUsize = AtomicUsize::new(0);\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n  use tempfile::TempDir;\n\n  #[test]\n  fn open_in_memory_succeeds_and_creates_tables() {\n    let db = Database::open(\":memory:\").expect(\"should open in-memory database\");\n\n    let tables: Vec<String> = db\n      .conn()\n      .prepare(\"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\")\n      .unwrap()\n      .query_map([], |row| row.get(0))\n      .unwrap()\n      .filter_map(|r| r.ok())\n      .collect();\n\n    assert!(tables.contains(&\"runs\".to_string()));\n    assert!(tables.contains(&\"tests\".to_string()));\n    assert!(tables.contains(&\"entries\".to_string()));\n    assert!(tables.contains(&\"spans\".to_string()));\n    assert!(tables.contains(&\"snapshots\".to_string()));\n  }\n\n  #[test]\n  fn migrations_are_idempotent() {\n    let db = Database::open(\":memory:\").expect(\"first open\");\n\n    // Running migrations again should be a no-op\n    run_migrations(db.conn()).expect(\"re-run should succeed\");\n\n    let version: i64 = db\n      .conn()\n      .query_row(\"SELECT MAX(version) FROM schema_migrations\", [], |row| {\n        row.get(0)\n      })\n      .unwrap();\n    assert_eq!(version, MIGRATIONS.len() as i64);\n  }\n\n  #[test]\n  fn open_upgrades_v1_database_with_run_stove_version_column() {\n    let dir = TempDir::new().unwrap();\n    let path = dir.path().join(\"stove-v1.db\");\n    let conn = Connection::open(&path).unwrap();\n\n    conn.execute_batch(MIGRATIONS[0].1).unwrap();\n    conn\n      .execute_batch(\n        \"CREATE TABLE IF NOT EXISTS schema_migrations (\n          version INTEGER PRIMARY KEY,\n          name TEXT NOT NULL,\n          applied_at TEXT NOT NULL DEFAULT (datetime('now'))\n      );\",\n      )\n      .unwrap();\n    conn\n      .execute(\n        \"INSERT INTO schema_migrations (version, name) VALUES (?1, ?2)\",\n        rusqlite::params![1_i64, \"V1__initial_schema\"],\n      )\n      .unwrap();\n    drop(conn);\n\n    let db = Database::open(path.to_str().unwrap()).unwrap();\n    let stove_version_columns: i64 = db\n      .conn()\n      .query_row(\n        \"SELECT COUNT(*) FROM pragma_table_info('runs') WHERE name = 'stove_version'\",\n        [],\n        |row| row.get(0),\n      )\n      .unwrap();\n    let schema_version: i64 = db\n      .conn()\n      .query_row(\"SELECT MAX(version) FROM schema_migrations\", [], |row| {\n        row.get(0)\n      })\n      .unwrap();\n\n    assert_eq!(stove_version_columns, 1);\n    assert_eq!(schema_version, MIGRATIONS.len() as i64);\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/src/storage/migrations/V1__initial_schema.sql",
    "content": "CREATE TABLE IF NOT EXISTS runs (\n    id TEXT PRIMARY KEY,\n    app_name TEXT NOT NULL,\n    started_at TEXT NOT NULL,\n    ended_at TEXT,\n    status TEXT NOT NULL DEFAULT 'RUNNING',\n    total_tests INTEGER NOT NULL DEFAULT 0,\n    passed INTEGER NOT NULL DEFAULT 0,\n    failed INTEGER NOT NULL DEFAULT 0,\n    duration_ms INTEGER,\n    systems TEXT NOT NULL DEFAULT '[]'\n);\n\nCREATE TABLE IF NOT EXISTS tests (\n    id TEXT NOT NULL,\n    run_id TEXT NOT NULL,\n    test_name TEXT NOT NULL,\n    spec_name TEXT NOT NULL DEFAULT '',\n    started_at TEXT NOT NULL,\n    ended_at TEXT,\n    status TEXT NOT NULL DEFAULT 'RUNNING',\n    duration_ms INTEGER,\n    error TEXT,\n    PRIMARY KEY (run_id, id),\n    FOREIGN KEY (run_id) REFERENCES runs(id)\n);\n\nCREATE TABLE IF NOT EXISTS entries (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    run_id TEXT NOT NULL,\n    test_id TEXT NOT NULL,\n    timestamp TEXT NOT NULL,\n    system TEXT NOT NULL,\n    action TEXT NOT NULL,\n    result TEXT NOT NULL,\n    input TEXT,\n    output TEXT,\n    metadata TEXT,\n    expected TEXT,\n    actual TEXT,\n    error TEXT,\n    trace_id TEXT,\n    FOREIGN KEY (run_id) REFERENCES runs(id)\n);\n\nCREATE TABLE IF NOT EXISTS spans (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    run_id TEXT NOT NULL,\n    trace_id TEXT NOT NULL,\n    span_id TEXT NOT NULL,\n    parent_span_id TEXT,\n    operation_name TEXT NOT NULL,\n    service_name TEXT NOT NULL,\n    start_time_nanos INTEGER NOT NULL,\n    end_time_nanos INTEGER NOT NULL,\n    status TEXT NOT NULL,\n    attributes TEXT,\n    exception_type TEXT,\n    exception_message TEXT,\n    exception_stack_trace TEXT,\n    FOREIGN KEY (run_id) REFERENCES runs(id)\n);\n\nCREATE TABLE IF NOT EXISTS snapshots (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    run_id TEXT NOT NULL,\n    test_id TEXT NOT NULL,\n    system TEXT NOT NULL,\n    state_json TEXT NOT NULL,\n    summary TEXT NOT NULL,\n    FOREIGN KEY (run_id) REFERENCES runs(id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_tests_run_id ON tests(run_id);\nCREATE INDEX IF NOT EXISTS idx_entries_run_test ON entries(run_id, test_id);\nCREATE INDEX IF NOT EXISTS idx_spans_run_id ON spans(run_id);\nCREATE INDEX IF NOT EXISTS idx_spans_trace_id ON spans(trace_id);\nCREATE INDEX IF NOT EXISTS idx_snapshots_run_test ON snapshots(run_id, test_id);\nCREATE INDEX IF NOT EXISTS idx_runs_app_name ON runs(app_name);\n"
  },
  {
    "path": "tools/stove-cli/src/storage/migrations/V2__run_stove_version.sql",
    "content": "ALTER TABLE runs ADD COLUMN stove_version TEXT;\n"
  },
  {
    "path": "tools/stove-cli/src/storage/migrations/V3__test_path.sql",
    "content": "ALTER TABLE tests ADD COLUMN test_path TEXT NOT NULL DEFAULT '[]';\n"
  },
  {
    "path": "tools/stove-cli/src/storage/mod.rs",
    "content": "pub mod database;\npub mod models;\npub mod repository;\n"
  },
  {
    "path": "tools/stove-cli/src/storage/models.rs",
    "content": "use std::fmt;\nuse std::str::FromStr;\n\nuse serde::Serialize;\n\n/// Status of a test run.\n#[derive(Debug, Clone, Serialize, PartialEq, Eq)]\npub enum RunStatus {\n  #[serde(rename = \"RUNNING\")]\n  Running,\n  #[serde(rename = \"PASSED\")]\n  Passed,\n  #[serde(rename = \"FAILED\")]\n  Failed,\n}\n\nimpl FromStr for RunStatus {\n  type Err = String;\n\n  fn from_str(s: &str) -> Result<Self, Self::Err> {\n    match s {\n      \"PASSED\" => Ok(RunStatus::Passed),\n      \"FAILED\" => Ok(RunStatus::Failed),\n      \"RUNNING\" => Ok(RunStatus::Running),\n      other => Err(format!(\"unknown run status: {other}\")),\n    }\n  }\n}\n\nimpl fmt::Display for RunStatus {\n  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n    match self {\n      RunStatus::Running => write!(f, \"RUNNING\"),\n      RunStatus::Passed => write!(f, \"PASSED\"),\n      RunStatus::Failed => write!(f, \"FAILED\"),\n    }\n  }\n}\n\n/// Status of an individual test or entry result.\n#[derive(Debug, Clone, Serialize, PartialEq, Eq)]\npub enum TestStatus {\n  #[serde(rename = \"RUNNING\")]\n  Running,\n  #[serde(rename = \"PASSED\")]\n  Passed,\n  #[serde(rename = \"FAILED\")]\n  Failed,\n  #[serde(rename = \"ERROR\")]\n  Error,\n}\n\nimpl FromStr for TestStatus {\n  type Err = String;\n\n  fn from_str(s: &str) -> Result<Self, Self::Err> {\n    match s {\n      \"PASSED\" => Ok(TestStatus::Passed),\n      \"FAILED\" => Ok(TestStatus::Failed),\n      \"ERROR\" => Ok(TestStatus::Error),\n      \"RUNNING\" => Ok(TestStatus::Running),\n      other => Err(format!(\"unknown test status: {other}\")),\n    }\n  }\n}\n\nimpl fmt::Display for TestStatus {\n  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n    match self {\n      TestStatus::Running => write!(f, \"RUNNING\"),\n      TestStatus::Passed => write!(f, \"PASSED\"),\n      TestStatus::Failed => write!(f, \"FAILED\"),\n      TestStatus::Error => write!(f, \"ERROR\"),\n    }\n  }\n}\n\n/// Summary of an application known to the dashboard.\n#[derive(Debug, Clone, Serialize)]\npub struct AppSummary {\n  pub app_name: String,\n  pub latest_run_id: String,\n  pub latest_status: RunStatus,\n  pub stove_version: Option<String>,\n  pub total_runs: i32,\n}\n\n/// A single test run (one execution of a test suite).\n#[derive(Debug, Clone, Serialize)]\npub struct Run {\n  pub id: String,\n  pub app_name: String,\n  pub started_at: String,\n  pub ended_at: Option<String>,\n  pub status: RunStatus,\n  pub total_tests: i32,\n  pub passed: i32,\n  pub failed: i32,\n  pub duration_ms: Option<i64>,\n  pub stove_version: Option<String>,\n  pub systems: Vec<String>,\n}\n\n/// A single test within a run.\n#[derive(Debug, Clone, Serialize)]\npub struct Test {\n  pub id: String,\n  pub run_id: String,\n  pub test_name: String,\n  pub spec_name: String,\n  pub test_path: Vec<String>,\n  pub started_at: String,\n  pub ended_at: Option<String>,\n  pub status: TestStatus,\n  pub duration_ms: Option<i64>,\n  pub error: Option<String>,\n}\n\n/// A report entry (action + result) within a test.\n#[derive(Debug, Clone, Serialize)]\npub struct Entry {\n  pub id: i64,\n  pub run_id: String,\n  pub test_id: String,\n  pub timestamp: String,\n  pub system: String,\n  pub action: String,\n  pub result: TestStatus,\n  pub input: Option<String>,\n  pub output: Option<String>,\n  pub metadata: Option<String>,\n  pub expected: Option<String>,\n  pub actual: Option<String>,\n  pub error: Option<String>,\n  pub trace_id: Option<String>,\n}\n\n/// A span in a distributed trace.\n#[derive(Debug, Clone, Serialize)]\npub struct Span {\n  pub id: i64,\n  pub run_id: String,\n  pub trace_id: String,\n  pub span_id: String,\n  pub parent_span_id: Option<String>,\n  pub operation_name: String,\n  pub service_name: String,\n  pub start_time_nanos: i64,\n  pub end_time_nanos: i64,\n  pub status: String,\n  pub attributes: Option<String>,\n  pub exception_type: Option<String>,\n  pub exception_message: Option<String>,\n  pub exception_stack_trace: Option<String>,\n}\n\n/// A system snapshot captured during a test.\n#[derive(Debug, Clone, Serialize)]\npub struct Snapshot {\n  pub id: i64,\n  pub run_id: String,\n  pub test_id: String,\n  pub system: String,\n  pub state_json: String,\n  pub summary: String,\n}\n\n// --- Input structs for write operations ---\n\n/// Data required to save a new report entry.\n#[derive(Clone, Debug)]\npub struct NewEntry {\n  pub run_id: String,\n  pub test_id: String,\n  pub timestamp: String,\n  pub system: String,\n  pub action: String,\n  pub result: String,\n  pub input: String,\n  pub output: String,\n  pub metadata: String,\n  pub expected: String,\n  pub actual: String,\n  pub error: String,\n  pub trace_id: String,\n}\n\n/// Data required to save a new span.\n#[derive(Clone, Debug, Default)]\npub struct NewSpan {\n  pub run_id: String,\n  pub trace_id: String,\n  pub span_id: String,\n  pub parent_span_id: String,\n  pub operation_name: String,\n  pub service_name: String,\n  pub start_time_nanos: i64,\n  pub end_time_nanos: i64,\n  pub status: String,\n  pub attributes: String,\n  pub exception_type: String,\n  pub exception_message: String,\n  pub exception_stack_trace: String,\n}\n"
  },
  {
    "path": "tools/stove-cli/src/storage/repository.rs",
    "content": "use std::sync::{Arc, Mutex, MutexGuard};\n\nuse crate::error::Result;\nuse crate::ingest::PersistedDashboardEvent;\nuse crate::storage::database::Database;\nuse crate::storage::models::{\n  AppSummary, Entry, NewEntry, NewSpan, Run, RunStatus, Snapshot, Span, Test, TestStatus,\n};\n\n/// Thread-safe repository for CRUD operations on the `SQLite` database.\n///\n/// Writes and reads use separate `SQLite` connections so the UI can keep polling\n/// while ingestion is busy. Each side is still serialized through its own mutex\n/// because a single `rusqlite::Connection` is not `Sync`.\npub struct Repository {\n  write_db: Arc<Mutex<Database>>,\n  read_db: Arc<Mutex<Database>>,\n}\n\nimpl Repository {\n  pub fn new(db: Database) -> Self {\n    let read_db = db\n      .open_peer()\n      .expect(\"peer database connection should open for repository reads\");\n    Self {\n      write_db: Arc::new(Mutex::new(db)),\n      read_db: Arc::new(Mutex::new(read_db)),\n    }\n  }\n\n  fn lock_write_db(&self) -> MutexGuard<'_, Database> {\n    self.write_db.lock().expect(\"write database lock poisoned\")\n  }\n\n  fn lock_read_db(&self) -> MutexGuard<'_, Database> {\n    self.read_db.lock().expect(\"read database lock poisoned\")\n  }\n\n  // --- Write operations (called from gRPC handler) ---\n\n  pub fn save_run_start(\n    &self,\n    run_id: &str,\n    app_name: &str,\n    started_at: &str,\n    systems: &[String],\n  ) -> Result<()> {\n    self.save_run_start_with_version(run_id, app_name, started_at, None, systems)\n  }\n\n  pub fn save_run_start_with_version(\n    &self,\n    run_id: &str,\n    app_name: &str,\n    started_at: &str,\n    stove_version: Option<&str>,\n    systems: &[String],\n  ) -> Result<()> {\n    let db = self.lock_write_db();\n    save_run_start_on(\n      db.conn(),\n      run_id,\n      app_name,\n      started_at,\n      stove_version,\n      systems,\n    )?;\n    Ok(())\n  }\n\n  pub fn save_run_end(\n    &self,\n    run_id: &str,\n    ended_at: &str,\n    total_tests: i32,\n    passed: i32,\n    failed: i32,\n    duration_ms: i64,\n  ) -> Result<()> {\n    let db = self.lock_write_db();\n    save_run_end_on(\n      db.conn(),\n      run_id,\n      ended_at,\n      total_tests,\n      passed,\n      failed,\n      duration_ms,\n    )?;\n    Ok(())\n  }\n\n  pub fn save_test_start(\n    &self,\n    run_id: &str,\n    test_id: &str,\n    test_name: &str,\n    spec_name: &str,\n    test_path: &[String],\n    started_at: &str,\n  ) -> Result<()> {\n    let db = self.lock_write_db();\n    save_test_start_on(\n      db.conn(),\n      run_id,\n      test_id,\n      test_name,\n      spec_name,\n      test_path,\n      started_at,\n    )?;\n    Ok(())\n  }\n\n  pub fn save_test_end(\n    &self,\n    run_id: &str,\n    test_id: &str,\n    status: &str,\n    duration_ms: i64,\n    error: &str,\n    ended_at: &str,\n  ) -> Result<()> {\n    let db = self.lock_write_db();\n    save_test_end_on(\n      db.conn(),\n      run_id,\n      test_id,\n      status,\n      duration_ms,\n      error,\n      ended_at,\n    )?;\n    Ok(())\n  }\n\n  pub fn save_entry(&self, entry: &NewEntry) -> Result<()> {\n    let db = self.lock_write_db();\n    save_entry_on(db.conn(), entry)?;\n    Ok(())\n  }\n\n  pub fn save_span(&self, span: &NewSpan) -> Result<()> {\n    let db = self.lock_write_db();\n    save_span_on(db.conn(), span)?;\n    Ok(())\n  }\n\n  pub fn save_snapshot(\n    &self,\n    run_id: &str,\n    test_id: &str,\n    system: &str,\n    state_json: &str,\n    summary: &str,\n  ) -> Result<()> {\n    let db = self.lock_write_db();\n    save_snapshot_on(db.conn(), run_id, test_id, system, state_json, summary)?;\n    Ok(())\n  }\n\n  pub fn clear_all(&self) -> Result<()> {\n    let db = self.lock_write_db();\n    db.conn().execute_batch(\n            \"DELETE FROM snapshots; DELETE FROM spans; DELETE FROM entries; DELETE FROM tests; DELETE FROM runs;\",\n        )?;\n    Ok(())\n  }\n\n  pub fn apply_persisted_events(&self, events: &[PersistedDashboardEvent]) -> Result<()> {\n    let mut db = self.lock_write_db();\n    let tx = db.conn_mut().unchecked_transaction()?;\n    for event in events {\n      apply_persisted_event(&tx, event)?;\n    }\n    tx.commit()?;\n    Ok(())\n  }\n\n  // --- Read operations (called from HTTP handlers) ---\n\n  pub fn get_apps(&self) -> Result<Vec<AppSummary>> {\n    let db = self.lock_read_db();\n    let mut stmt = db.conn().prepare(\n      \"SELECT r.app_name, r.id, r.status, r.stove_version, (SELECT COUNT(*) FROM runs r2 WHERE r2.app_name = r.app_name)\n             FROM runs r\n             WHERE r.id = (\n               SELECT r3.id\n               FROM runs r3\n               WHERE r3.app_name = r.app_name\n               ORDER BY r3.started_at DESC, r3.rowid DESC\n               LIMIT 1\n             )\n             ORDER BY app_name\",\n    )?;\n    let rows = stmt\n      .query_map([], |row| {\n        Ok(AppSummary {\n          app_name: row.get(0)?,\n          latest_run_id: row.get(1)?,\n          latest_status: parse_run_status(&row.get::<_, String>(2)?),\n          stove_version: row.get(3)?,\n          total_runs: row.get(4)?,\n        })\n      })?\n      .filter_map(|r| r.ok())\n      .collect();\n    Ok(rows)\n  }\n\n  pub fn get_runs(&self, app_name: Option<&str>) -> Result<Vec<Run>> {\n    let db = self.lock_read_db();\n    let filter = match app_name {\n      Some(_) => \" WHERE app_name = ?1\",\n      None => \"\",\n    };\n    let sql =\n      format!(\"SELECT {RUN_COLUMNS} FROM runs{filter} ORDER BY started_at DESC, rowid DESC\");\n    let mut stmt = db.conn().prepare(&sql)?;\n    let rows = match app_name {\n      Some(name) => stmt.query_map(rusqlite::params![name], run_from_row)?,\n      None => stmt.query_map([], run_from_row)?,\n    };\n    Ok(rows.filter_map(|r| r.ok()).collect())\n  }\n\n  pub fn get_run(&self, run_id: &str) -> Result<Option<Run>> {\n    let db = self.lock_read_db();\n    let sql = format!(\"SELECT {RUN_COLUMNS} FROM runs WHERE id = ?1\");\n    let mut stmt = db.conn().prepare(&sql)?;\n    let mut rows = stmt.query_map(rusqlite::params![run_id], run_from_row)?;\n    Ok(rows.next().and_then(|r| r.ok()))\n  }\n\n  pub fn get_tests_for_run(&self, run_id: &str) -> Result<Vec<Test>> {\n    let db = self.lock_read_db();\n    let mut stmt = db.conn().prepare(\n            \"SELECT id, run_id, test_name, spec_name, test_path, started_at, ended_at, status, duration_ms, error FROM tests WHERE run_id = ?1 ORDER BY started_at\",\n        )?;\n    let rows = stmt\n      .query_map(rusqlite::params![run_id], test_from_row)?\n      .filter_map(|r| r.ok())\n      .collect();\n    Ok(rows)\n  }\n\n  pub fn get_entries(&self, run_id: &str, test_id: &str) -> Result<Vec<Entry>> {\n    let db = self.lock_read_db();\n    let mut stmt = db.conn().prepare(\n            \"SELECT id, run_id, test_id, timestamp, system, action, result, input, output, metadata, expected, actual, error, trace_id FROM entries WHERE run_id = ?1 AND test_id = ?2 ORDER BY timestamp\",\n        )?;\n    let rows = stmt\n      .query_map(rusqlite::params![run_id, test_id], entry_from_row)?\n      .filter_map(|r| r.ok())\n      .collect();\n    Ok(rows)\n  }\n\n  pub fn get_spans_for_test(&self, run_id: &str, test_id: &str) -> Result<Vec<Span>> {\n    let db = self.lock_read_db();\n    let sql = format!(\n      \"SELECT {SPAN_COLUMNS} FROM spans \\\n             WHERE run_id = ?1 AND trace_id IN ( \\\n               SELECT trace_id FROM entries WHERE run_id = ?1 AND test_id = ?2 AND trace_id != '' \\\n               UNION \\\n               SELECT DISTINCT trace_id FROM spans WHERE run_id = ?1 AND ( \\\n                 json_extract(attributes, '$.\\\"x-stove-test-id\\\"') = ?2 OR \\\n                 json_extract(attributes, '$.\\\"X-Stove-Test-Id\\\"') = ?2 OR \\\n                 json_extract(attributes, '$.\\\"stove.test.id\\\"') = ?2 OR \\\n                 json_extract(attributes, '$.\\\"stove_test_id\\\"') = ?2 \\\n               ) \\\n             ) \\\n             ORDER BY start_time_nanos\"\n    );\n    let mut stmt = db.conn().prepare(&sql)?;\n    let rows = stmt\n      .query_map(rusqlite::params![run_id, test_id], span_from_row)?\n      .filter_map(|r| r.ok())\n      .collect();\n    Ok(rows)\n  }\n\n  pub fn get_trace(&self, trace_id: &str) -> Result<Vec<Span>> {\n    let db = self.lock_read_db();\n    let sql =\n      format!(\"SELECT {SPAN_COLUMNS} FROM spans WHERE trace_id = ?1 ORDER BY start_time_nanos\");\n    let mut stmt = db.conn().prepare(&sql)?;\n    let rows = stmt\n      .query_map(rusqlite::params![trace_id], span_from_row)?\n      .filter_map(|r| r.ok())\n      .collect();\n    Ok(rows)\n  }\n\n  pub fn get_snapshots(&self, run_id: &str, test_id: &str) -> Result<Vec<Snapshot>> {\n    let db = self.lock_read_db();\n    let mut stmt = db.conn().prepare(\n            \"SELECT id, run_id, test_id, system, state_json, summary FROM snapshots WHERE run_id = ?1 AND test_id = ?2\",\n        )?;\n    let rows = stmt\n      .query_map(rusqlite::params![run_id, test_id], snapshot_from_row)?\n      .filter_map(|r| r.ok())\n      .collect();\n    Ok(rows)\n  }\n}\n\nfn apply_persisted_event(\n  conn: &rusqlite::Connection,\n  event: &PersistedDashboardEvent,\n) -> Result<()> {\n  match event {\n    PersistedDashboardEvent::RunStarted {\n      run_id,\n      app_name,\n      started_at,\n      stove_version,\n      systems,\n    } => save_run_start_on(\n      conn,\n      run_id,\n      app_name,\n      started_at,\n      stove_version.as_deref(),\n      systems,\n    ),\n    PersistedDashboardEvent::RunEnded {\n      run_id,\n      ended_at,\n      total_tests,\n      passed,\n      failed,\n      duration_ms,\n    } => save_run_end_on(\n      conn,\n      run_id,\n      ended_at,\n      *total_tests,\n      *passed,\n      *failed,\n      *duration_ms,\n    ),\n    PersistedDashboardEvent::TestStarted {\n      run_id,\n      test_id,\n      test_name,\n      spec_name,\n      test_path,\n      started_at,\n    } => save_test_start_on(\n      conn, run_id, test_id, test_name, spec_name, test_path, started_at,\n    ),\n    PersistedDashboardEvent::TestEnded {\n      run_id,\n      test_id,\n      status,\n      duration_ms,\n      error,\n      ended_at,\n    } => save_test_end_on(\n      conn,\n      run_id,\n      test_id,\n      status,\n      *duration_ms,\n      error.as_deref().unwrap_or_default(),\n      ended_at,\n    ),\n    PersistedDashboardEvent::EntryRecorded(entry) => save_entry_on(conn, entry),\n    PersistedDashboardEvent::SpanRecorded(span) => save_span_on(conn, span),\n    PersistedDashboardEvent::Snapshot {\n      run_id,\n      test_id,\n      system,\n      state_json,\n      summary,\n    } => save_snapshot_on(conn, run_id, test_id, system, state_json, summary),\n  }\n}\n\nfn save_run_start_on(\n  conn: &rusqlite::Connection,\n  run_id: &str,\n  app_name: &str,\n  started_at: &str,\n  stove_version: Option<&str>,\n  systems: &[String],\n) -> Result<()> {\n  let systems_json = serde_json::to_string(systems)?;\n  conn.execute(\n    \"INSERT OR REPLACE INTO runs (id, app_name, started_at, stove_version, systems) VALUES (?1, ?2, ?3, ?4, ?5)\",\n    rusqlite::params![run_id, app_name, started_at, stove_version, systems_json],\n  )?;\n  Ok(())\n}\n\nfn save_run_end_on(\n  conn: &rusqlite::Connection,\n  run_id: &str,\n  ended_at: &str,\n  total_tests: i32,\n  passed: i32,\n  failed: i32,\n  duration_ms: i64,\n) -> Result<()> {\n  let status = if failed > 0 {\n    RunStatus::Failed\n  } else {\n    RunStatus::Passed\n  };\n  conn.execute(\n    \"UPDATE runs SET ended_at = ?1, status = ?2, total_tests = ?3, passed = ?4, failed = ?5, duration_ms = ?6 WHERE id = ?7\",\n    rusqlite::params![ended_at, status.to_string(), total_tests, passed, failed, duration_ms, run_id],\n  )?;\n  Ok(())\n}\n\nfn save_test_start_on(\n  conn: &rusqlite::Connection,\n  run_id: &str,\n  test_id: &str,\n  test_name: &str,\n  spec_name: &str,\n  test_path: &[String],\n  started_at: &str,\n) -> Result<()> {\n  let test_path_json = serde_json::to_string(test_path)?;\n  conn.execute(\n    \"INSERT OR REPLACE INTO tests (id, run_id, test_name, spec_name, test_path, started_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)\",\n    rusqlite::params![test_id, run_id, test_name, spec_name, test_path_json, started_at],\n  )?;\n  Ok(())\n}\n\nfn save_test_end_on(\n  conn: &rusqlite::Connection,\n  run_id: &str,\n  test_id: &str,\n  status: &str,\n  duration_ms: i64,\n  error: &str,\n  ended_at: &str,\n) -> Result<()> {\n  conn.execute(\n    \"UPDATE tests SET ended_at = ?1, status = ?2, duration_ms = ?3, error = ?4 WHERE run_id = ?5 AND id = ?6\",\n    rusqlite::params![ended_at, status, duration_ms, non_empty(error), run_id, test_id],\n  )?;\n  Ok(())\n}\n\nfn save_entry_on(conn: &rusqlite::Connection, entry: &NewEntry) -> Result<()> {\n  conn.execute(\n    \"INSERT INTO entries (run_id, test_id, timestamp, system, action, result, input, output, metadata, expected, actual, error, trace_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)\",\n    rusqlite::params![\n      entry.run_id,\n      entry.test_id,\n      entry.timestamp,\n      entry.system,\n      entry.action,\n      entry.result,\n      non_empty(&entry.input),\n      non_empty(&entry.output),\n      non_empty(&entry.metadata),\n      non_empty(&entry.expected),\n      non_empty(&entry.actual),\n      non_empty(&entry.error),\n      non_empty(&entry.trace_id)\n    ],\n  )?;\n  Ok(())\n}\n\nfn save_span_on(conn: &rusqlite::Connection, span: &NewSpan) -> Result<()> {\n  conn.execute(\n    \"INSERT INTO spans (run_id, trace_id, span_id, parent_span_id, operation_name, service_name, start_time_nanos, end_time_nanos, status, attributes, exception_type, exception_message, exception_stack_trace) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)\",\n    rusqlite::params![\n      span.run_id,\n      span.trace_id,\n      span.span_id,\n      non_empty(&span.parent_span_id),\n      span.operation_name,\n      span.service_name,\n      span.start_time_nanos,\n      span.end_time_nanos,\n      span.status,\n      non_empty(&span.attributes),\n      non_empty(&span.exception_type),\n      non_empty(&span.exception_message),\n      non_empty(&span.exception_stack_trace)\n    ],\n  )?;\n  Ok(())\n}\n\nfn save_snapshot_on(\n  conn: &rusqlite::Connection,\n  run_id: &str,\n  test_id: &str,\n  system: &str,\n  state_json: &str,\n  summary: &str,\n) -> Result<()> {\n  conn.execute(\n    \"INSERT INTO snapshots (run_id, test_id, system, state_json, summary) VALUES (?1, ?2, ?3, ?4, ?5)\",\n    rusqlite::params![run_id, test_id, system, state_json, summary],\n  )?;\n  Ok(())\n}\n\n// --- SQL column constants ---\n\nconst RUN_COLUMNS: &str = \"id, app_name, started_at, ended_at, status, total_tests, passed, failed, duration_ms, stove_version, systems\";\nconst SPAN_COLUMNS: &str = \"id, run_id, trace_id, span_id, parent_span_id, operation_name, service_name, start_time_nanos, end_time_nanos, status, attributes, exception_type, exception_message, exception_stack_trace\";\n\n// --- Row-mapping helpers ---\n\n/// Convert empty strings to `None` for optional database fields.\nfn non_empty(s: &str) -> Option<&str> {\n  if s.is_empty() { None } else { Some(s) }\n}\n\n/// Parse a `RunStatus` from a database string, defaulting to `Running`.\nfn parse_run_status(s: &str) -> RunStatus {\n  s.parse().unwrap_or(RunStatus::Running)\n}\n\n/// Parse a `TestStatus` from a database string, defaulting to `Running`.\nfn parse_test_status(s: &str) -> TestStatus {\n  s.parse().unwrap_or(TestStatus::Running)\n}\n\nfn run_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Run> {\n  let systems_json: String = row.get(10)?;\n  let systems: Vec<String> = serde_json::from_str(&systems_json).unwrap_or_default();\n  Ok(Run {\n    id: row.get(0)?,\n    app_name: row.get(1)?,\n    started_at: row.get(2)?,\n    ended_at: row.get(3)?,\n    status: parse_run_status(&row.get::<_, String>(4)?),\n    total_tests: row.get(5)?,\n    passed: row.get(6)?,\n    failed: row.get(7)?,\n    duration_ms: row.get(8)?,\n    stove_version: row.get(9)?,\n    systems,\n  })\n}\n\nfn test_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Test> {\n  let test_path_json: String = row.get(4)?;\n  let test_path: Vec<String> = serde_json::from_str(&test_path_json).unwrap_or_default();\n  Ok(Test {\n    id: row.get(0)?,\n    run_id: row.get(1)?,\n    test_name: row.get(2)?,\n    spec_name: row.get(3)?,\n    test_path,\n    started_at: row.get(5)?,\n    ended_at: row.get(6)?,\n    status: parse_test_status(&row.get::<_, String>(7)?),\n    duration_ms: row.get(8)?,\n    error: row.get(9)?,\n  })\n}\n\nfn entry_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Entry> {\n  Ok(Entry {\n    id: row.get(0)?,\n    run_id: row.get(1)?,\n    test_id: row.get(2)?,\n    timestamp: row.get(3)?,\n    system: row.get(4)?,\n    action: row.get(5)?,\n    result: parse_test_status(&row.get::<_, String>(6)?),\n    input: row.get(7)?,\n    output: row.get(8)?,\n    metadata: row.get(9)?,\n    expected: row.get(10)?,\n    actual: row.get(11)?,\n    error: row.get(12)?,\n    trace_id: row.get(13)?,\n  })\n}\n\nfn snapshot_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Snapshot> {\n  Ok(Snapshot {\n    id: row.get(0)?,\n    run_id: row.get(1)?,\n    test_id: row.get(2)?,\n    system: row.get(3)?,\n    state_json: row.get(4)?,\n    summary: row.get(5)?,\n  })\n}\n\nfn span_from_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Span> {\n  Ok(Span {\n    id: row.get(0)?,\n    run_id: row.get(1)?,\n    trace_id: row.get(2)?,\n    span_id: row.get(3)?,\n    parent_span_id: row.get(4)?,\n    operation_name: row.get(5)?,\n    service_name: row.get(6)?,\n    start_time_nanos: row.get(7)?,\n    end_time_nanos: row.get(8)?,\n    status: row.get(9)?,\n    attributes: row.get(10)?,\n    exception_type: row.get(11)?,\n    exception_message: row.get(12)?,\n    exception_stack_trace: row.get(13)?,\n  })\n}\n\n#[cfg(test)]\nmod tests {\n  use super::*;\n  use crate::storage::database::Database;\n\n  fn test_repo() -> Repository {\n    Repository::new(Database::open(\":memory:\").unwrap())\n  }\n\n  #[test]\n  fn full_event_lifecycle() {\n    let repo = test_repo();\n\n    repo\n      .save_run_start_with_version(\n        \"run-1\",\n        \"product-api\",\n        \"2024-01-01T00:00:00Z\",\n        Some(\"0.23.2\"),\n        &[\"HTTP\".into(), \"Kafka\".into()],\n      )\n      .unwrap();\n\n    repo\n      .save_test_start(\n        \"run-1\",\n        \"test-1\",\n        \"should create product\",\n        \"ProductSpec\",\n        &[],\n        \"2024-01-01T00:00:01Z\",\n      )\n      .unwrap();\n\n    repo\n      .save_entry(&NewEntry {\n        run_id: \"run-1\".into(),\n        test_id: \"test-1\".into(),\n        timestamp: \"2024-01-01T00:00:02Z\".into(),\n        system: \"HTTP\".into(),\n        action: \"POST /products\".into(),\n        result: \"PASSED\".into(),\n        input: r#\"{\"name\":\"widget\"}\"#.into(),\n        output: r#\"{\"id\":1}\"#.into(),\n        metadata: \"{}\".into(),\n        expected: String::new(),\n        actual: String::new(),\n        error: String::new(),\n        trace_id: String::new(),\n      })\n      .unwrap();\n\n    repo\n      .save_span(&NewSpan {\n        run_id: \"run-1\".into(),\n        trace_id: \"trace-abc\".into(),\n        span_id: \"span-1\".into(),\n        operation_name: \"POST /products\".into(),\n        service_name: \"product-api\".into(),\n        start_time_nanos: 1_000_000_000,\n        end_time_nanos: 1_100_000_000,\n        status: \"OK\".into(),\n        attributes: r#\"{\"http.method\":\"POST\"}\"#.into(),\n        ..Default::default()\n      })\n      .unwrap();\n\n    repo\n      .save_snapshot(\n        \"run-1\",\n        \"test-1\",\n        \"Kafka\",\n        r#\"{\"consumed\":5}\"#,\n        \"5 messages consumed\",\n      )\n      .unwrap();\n\n    repo\n      .save_test_end(\n        \"run-1\",\n        \"test-1\",\n        \"PASSED\",\n        1500,\n        \"\",\n        \"2024-01-01T00:00:03Z\",\n      )\n      .unwrap();\n\n    repo\n      .save_run_end(\"run-1\", \"2024-01-01T00:00:10Z\", 1, 1, 0, 10000)\n      .unwrap();\n\n    let runs = repo.get_runs(None).unwrap();\n    assert_eq!(runs.len(), 1);\n    assert_eq!(runs[0].app_name, \"product-api\");\n    assert_eq!(runs[0].status, RunStatus::Passed);\n    assert_eq!(runs[0].stove_version.as_deref(), Some(\"0.23.2\"));\n    assert_eq!(runs[0].systems, vec![\"HTTP\", \"Kafka\"]);\n\n    let run = repo.get_run(\"run-1\").unwrap().unwrap();\n    assert_eq!(run.total_tests, 1);\n    assert_eq!(run.passed, 1);\n\n    let tests = repo.get_tests_for_run(\"run-1\").unwrap();\n    assert_eq!(tests.len(), 1);\n    assert_eq!(tests[0].test_name, \"should create product\");\n    assert_eq!(tests[0].status, TestStatus::Passed);\n\n    let entries = repo.get_entries(\"run-1\", \"test-1\").unwrap();\n    assert_eq!(entries.len(), 1);\n    assert_eq!(entries[0].system, \"HTTP\");\n    assert_eq!(entries[0].action, \"POST /products\");\n\n    let trace = repo.get_trace(\"trace-abc\").unwrap();\n    assert_eq!(trace.len(), 1);\n    assert_eq!(trace[0].operation_name, \"POST /products\");\n\n    let snapshots = repo.get_snapshots(\"run-1\", \"test-1\").unwrap();\n    assert_eq!(snapshots.len(), 1);\n    assert_eq!(snapshots[0].system, \"Kafka\");\n\n    let apps = repo.get_apps().unwrap();\n    assert_eq!(apps.len(), 1);\n    assert_eq!(apps[0].app_name, \"product-api\");\n    assert_eq!(apps[0].stove_version.as_deref(), Some(\"0.23.2\"));\n    assert_eq!(apps[0].total_runs, 1);\n  }\n\n  #[test]\n  fn latest_app_version_comes_from_latest_run() {\n    let repo = test_repo();\n    repo\n      .save_run_start_with_version(\n        \"run-1\",\n        \"product-api\",\n        \"2024-01-01T00:00:00Z\",\n        Some(\"0.23.0\"),\n        &[],\n      )\n      .unwrap();\n    repo\n      .save_run_start_with_version(\n        \"run-2\",\n        \"product-api\",\n        \"2024-01-02T00:00:00Z\",\n        Some(\"0.23.2\"),\n        &[],\n      )\n      .unwrap();\n\n    let apps = repo.get_apps().unwrap();\n\n    assert_eq!(apps.len(), 1);\n    assert_eq!(apps[0].latest_run_id, \"run-2\");\n    assert_eq!(apps[0].stove_version.as_deref(), Some(\"0.23.2\"));\n  }\n\n  #[test]\n  fn get_runs_filters_by_app_name() {\n    let repo = test_repo();\n    repo\n      .save_run_start(\"run-1\", \"product-api\", \"2024-01-01T00:00:00Z\", &[])\n      .unwrap();\n    repo\n      .save_run_start(\"run-2\", \"order-api\", \"2024-01-01T00:00:01Z\", &[])\n      .unwrap();\n\n    let product_runs = repo.get_runs(Some(\"product-api\")).unwrap();\n    assert_eq!(product_runs.len(), 1);\n    assert_eq!(product_runs[0].app_name, \"product-api\");\n\n    let all_runs = repo.get_runs(None).unwrap();\n    assert_eq!(all_runs.len(), 2);\n  }\n\n  #[test]\n  fn clear_all_removes_everything() {\n    let repo = test_repo();\n    repo\n      .save_run_start(\"run-1\", \"app\", \"2024-01-01T00:00:00Z\", &[])\n      .unwrap();\n    repo\n      .save_test_start(\"run-1\", \"test-1\", \"test\", \"\", &[], \"2024-01-01T00:00:01Z\")\n      .unwrap();\n\n    repo.clear_all().unwrap();\n\n    assert!(repo.get_runs(None).unwrap().is_empty());\n    assert!(repo.get_tests_for_run(\"run-1\").unwrap().is_empty());\n  }\n\n  #[test]\n  fn get_apps_returns_single_latest_run_when_started_at_ties() {\n    let repo = test_repo();\n    repo\n      .save_run_start(\"run-1\", \"my-app\", \"2024-06-01T00:00:00Z\", &[])\n      .unwrap();\n    repo\n      .save_run_start(\"run-2\", \"my-app\", \"2024-06-01T00:00:00Z\", &[])\n      .unwrap();\n\n    let apps = repo.get_apps().unwrap();\n\n    assert_eq!(apps.len(), 1);\n    assert_eq!(apps[0].app_name, \"my-app\");\n    assert_eq!(apps[0].latest_run_id, \"run-2\");\n    assert_eq!(apps[0].total_runs, 2);\n  }\n\n  #[test]\n  fn get_runs_orders_same_timestamp_runs_by_latest_inserted_first() {\n    let repo = test_repo();\n    repo\n      .save_run_start(\"run-1\", \"my-app\", \"2024-06-01T00:00:00Z\", &[])\n      .unwrap();\n    repo\n      .save_run_start(\"run-2\", \"my-app\", \"2024-06-01T00:00:00Z\", &[])\n      .unwrap();\n\n    let runs = repo.get_runs(Some(\"my-app\")).unwrap();\n\n    assert_eq!(runs.len(), 2);\n    assert_eq!(runs[0].id, \"run-2\");\n    assert_eq!(runs[1].id, \"run-1\");\n  }\n\n  #[test]\n  fn get_spans_for_test_does_not_cross_match_similar_test_ids() {\n    let repo = test_repo();\n    repo\n      .save_run_start(\"run-1\", \"my-app\", \"2024-06-01T00:00:00Z\", &[])\n      .unwrap();\n    repo\n      .save_test_start(\n        \"run-1\",\n        \"test-1\",\n        \"first test\",\n        \"Spec\",\n        &[],\n        \"2024-06-01T00:00:01Z\",\n      )\n      .unwrap();\n    repo\n      .save_test_start(\n        \"run-1\",\n        \"test-10\",\n        \"tenth test\",\n        \"Spec\",\n        &[],\n        \"2024-06-01T00:00:02Z\",\n      )\n      .unwrap();\n    repo\n      .save_span(&NewSpan {\n        run_id: \"run-1\".into(),\n        trace_id: \"trace-10\".into(),\n        span_id: \"span-10\".into(),\n        operation_name: \"GET /ten\".into(),\n        service_name: \"my-app\".into(),\n        start_time_nanos: 1_000_000_000,\n        end_time_nanos: 1_100_000_000,\n        status: \"OK\".into(),\n        attributes: r#\"{\"x-stove-test-id\":\"test-10\"}\"#.into(),\n        ..Default::default()\n      })\n      .unwrap();\n\n    let spans = repo.get_spans_for_test(\"run-1\", \"test-1\").unwrap();\n\n    assert!(spans.is_empty());\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/tests/api_e2e.rs",
    "content": "//! End-to-end tests for the Stove CLI REST API.\n//!\n//! Each test spins up a real axum server on an OS-assigned port backed by an\n//! in-memory SQLite database, then exercises the HTTP endpoints with `reqwest`.\n//! This gives us true black-box regression coverage of the full request path:\n//! routing -> handler -> repository -> SQLite -> JSON serialization.\n\nmod common;\n\nuse common::TestServer;\nuse reqwest::StatusCode;\nuse serde_json::Value;\nuse std::collections::HashMap;\nuse std::sync::Arc;\nuse std::time::Duration;\nuse stove::grpc::service::DashboardEventServiceImpl;\nuse stove::proto;\nuse stove::proto::dashboard_event_service_server::DashboardEventService;\nuse tonic::Request;\n\nfn ts(seconds: i64, nanos: i32) -> Option<prost_types::Timestamp> {\n  Some(prost_types::Timestamp { seconds, nanos })\n}\n\nfn run_started_event(\n  run_id: &str,\n  app_name: &str,\n  seconds: i64,\n  nanos: i32,\n) -> proto::DashboardEvent {\n  run_started_event_with_version(run_id, app_name, \"\", seconds, nanos)\n}\n\nfn run_started_event_with_version(\n  run_id: &str,\n  app_name: &str,\n  stove_version: &str,\n  seconds: i64,\n  nanos: i32,\n) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::RunStarted(\n      proto::RunStartedEvent {\n        timestamp: ts(seconds, nanos),\n        app_name: app_name.to_string(),\n        systems: vec![\"HTTP\".to_string(), \"Kafka\".to_string()],\n        stove_version: stove_version.to_string(),\n      },\n    )),\n  }\n}\n\nfn run_ended_event(\n  run_id: &str,\n  total_tests: i32,\n  passed: i32,\n  failed: i32,\n  duration_ms: i64,\n  seconds: i64,\n  nanos: i32,\n) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::RunEnded(\n      proto::RunEndedEvent {\n        timestamp: ts(seconds, nanos),\n        total_tests,\n        passed,\n        failed,\n        duration_ms,\n      },\n    )),\n  }\n}\n\nfn test_started_event(\n  run_id: &str,\n  test_id: &str,\n  test_name: &str,\n  spec_name: &str,\n  seconds: i64,\n  nanos: i32,\n) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::TestStarted(\n      proto::TestStartedEvent {\n        test_id: test_id.to_string(),\n        test_name: test_name.to_string(),\n        spec_name: spec_name.to_string(),\n        timestamp: ts(seconds, nanos),\n        test_path: vec![],\n      },\n    )),\n  }\n}\n\nfn test_ended_event(\n  run_id: &str,\n  test_id: &str,\n  status: &str,\n  duration_ms: i64,\n  error: &str,\n  seconds: i64,\n  nanos: i32,\n) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::TestEnded(\n      proto::TestEndedEvent {\n        test_id: test_id.to_string(),\n        status: status.to_string(),\n        duration_ms,\n        error: error.to_string(),\n        timestamp: ts(seconds, nanos),\n      },\n    )),\n  }\n}\n\nfn entry_recorded_event(\n  run_id: &str,\n  test_id: &str,\n  action: &str,\n  result: &str,\n  trace_id: &str,\n  seconds: i64,\n  nanos: i32,\n) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::EntryRecorded(\n      proto::EntryRecordedEvent {\n        test_id: test_id.to_string(),\n        timestamp: ts(seconds, nanos),\n        system: \"HTTP\".to_string(),\n        action: action.to_string(),\n        result: result.to_string(),\n        input: String::new(),\n        output: String::new(),\n        metadata: HashMap::default(),\n        expected: String::new(),\n        actual: String::new(),\n        error: String::new(),\n        trace_id: trace_id.to_string(),\n      },\n    )),\n  }\n}\n\nfn span_recorded_event(\n  run_id: &str,\n  trace_id: &str,\n  span_id: &str,\n  parent_span_id: &str,\n  operation_name: &str,\n  service_name: &str,\n  start_time_nanos: i64,\n  end_time_nanos: i64,\n) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::SpanRecorded(\n      proto::SpanRecordedEvent {\n        trace_id: trace_id.to_string(),\n        span_id: span_id.to_string(),\n        parent_span_id: parent_span_id.to_string(),\n        operation_name: operation_name.to_string(),\n        service_name: service_name.to_string(),\n        start_time_nanos,\n        end_time_nanos,\n        status: \"OK\".to_string(),\n        attributes: HashMap::default(),\n        exception: None,\n      },\n    )),\n  }\n}\n\nfn snapshot_event(\n  run_id: &str,\n  test_id: &str,\n  system: &str,\n  state_json: &str,\n  summary: &str,\n) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::Snapshot(\n      proto::SnapshotEvent {\n        test_id: test_id.to_string(),\n        system: system.to_string(),\n        state_json: state_json.to_string(),\n        summary: summary.to_string(),\n      },\n    )),\n  }\n}\n\nasync fn send_event(\n  service: &DashboardEventServiceImpl,\n  event: proto::DashboardEvent,\n) -> Result<(), tonic::Status> {\n  DashboardEventService::send_event(service, Request::new(event))\n    .await\n    .map(|_| ())\n}\n\nasync fn flush_events(service: &DashboardEventServiceImpl) {\n  service\n    .flush_pending()\n    .await\n    .expect(\"queued dashboard events should flush\");\n}\n\nfn extract_sse_data_frame(frame: &str) -> Option<String> {\n  let data_lines: Vec<&str> = frame\n    .lines()\n    .filter_map(|line| line.strip_prefix(\"data:\").map(str::trim_start))\n    .collect();\n\n  if data_lines.is_empty() {\n    None\n  } else {\n    Some(data_lines.join(\"\\n\"))\n  }\n}\n\nasync fn next_sse_data(\n  resp: &mut reqwest::Response,\n  buffer: &mut String,\n) -> Result<String, Box<dyn std::error::Error>> {\n  loop {\n    if let Some(frame_end) = buffer.find(\"\\n\\n\") {\n      let frame = buffer[..frame_end].to_string();\n      buffer.drain(..frame_end + 2);\n      if let Some(data) = extract_sse_data_frame(&frame) {\n        return Ok(data);\n      }\n    }\n\n    let chunk = tokio::time::timeout(std::time::Duration::from_secs(5), resp.chunk()).await??;\n    let chunk = chunk.ok_or(\"SSE stream ended before the next event\")?;\n    buffer.push_str(std::str::from_utf8(&chunk)?);\n  }\n}\n\n// ---------------------------------------------------------------------------\n// GET /api/v1/meta\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn meta_returns_cli_version() {\n  let server = TestServer::start().await;\n\n  let body = server.get_json(\"/meta\").await;\n\n  assert_eq!(body[\"stove_cli_version\"], stove::STOVE_CLI_VERSION);\n  assert_eq!(body[\"mcp\"][\"enabled\"], true);\n  assert_eq!(body[\"mcp\"][\"transport\"], \"streamable-http\");\n  assert_eq!(body[\"mcp\"][\"endpoint\"], format!(\"{}/mcp\", server.base_url));\n}\n\n// ---------------------------------------------------------------------------\n// GET /api/v1/apps\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn apps_returns_empty_when_no_data() {\n  let server = TestServer::start().await;\n\n  let body = server.get_json(\"/apps\").await;\n  assert_eq!(body, Value::Array(vec![]));\n}\n\n#[tokio::test]\nasync fn apps_returns_app_summaries() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/apps\").await;\n  let apps = body.as_array().expect(\"should be array\");\n  assert_eq!(apps.len(), 1);\n  assert_eq!(apps[0][\"app_name\"], \"product-api\");\n  assert_eq!(apps[0][\"latest_run_id\"], \"run-1\");\n  assert_eq!(apps[0][\"latest_status\"], \"FAILED\");\n  assert!(apps[0][\"stove_version\"].is_null());\n  assert_eq!(apps[0][\"total_runs\"], 1);\n}\n\n#[tokio::test]\nasync fn apps_returns_multiple_apps() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-a\", \"alpha-api\");\n  server.seed_run(\"run-b\", \"beta-api\");\n\n  let body = server.get_json(\"/apps\").await;\n  let apps = body.as_array().unwrap();\n  assert_eq!(apps.len(), 2);\n\n  let names: Vec<&str> = apps\n    .iter()\n    .map(|a| a[\"app_name\"].as_str().unwrap())\n    .collect();\n  assert!(names.contains(&\"alpha-api\"));\n  assert!(names.contains(&\"beta-api\"));\n}\n\n#[tokio::test]\nasync fn apps_return_latest_run_stove_version() {\n  let server = TestServer::start().await;\n  server.seed_run_at_with_version(\n    \"run-old\",\n    \"product-api\",\n    \"2024-01-01T00:00:00Z\",\n    Some(\"0.23.0\"),\n    &[],\n  );\n  server.seed_run_at_with_version(\n    \"run-new\",\n    \"product-api\",\n    \"2024-06-01T00:00:00Z\",\n    Some(\"0.23.2\"),\n    &[],\n  );\n\n  let body = server.get_json(\"/apps\").await;\n  let apps = body.as_array().unwrap();\n\n  assert_eq!(apps[0][\"latest_run_id\"], \"run-new\");\n  assert_eq!(apps[0][\"stove_version\"], \"0.23.2\");\n}\n\n// ---------------------------------------------------------------------------\n// GET /api/v1/runs\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn runs_returns_all_runs() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs\").await;\n  let runs = body.as_array().unwrap();\n  assert_eq!(runs.len(), 1);\n  assert_eq!(runs[0][\"id\"], \"run-1\");\n  assert_eq!(runs[0][\"app_name\"], \"product-api\");\n  assert_eq!(runs[0][\"status\"], \"FAILED\");\n  assert_eq!(runs[0][\"total_tests\"], 2);\n  assert_eq!(runs[0][\"passed\"], 1);\n  assert_eq!(runs[0][\"failed\"], 1);\n  assert_eq!(runs[0][\"duration_ms\"], 10000);\n  assert!(runs[0][\"stove_version\"].is_null());\n\n  let systems = runs[0][\"systems\"].as_array().unwrap();\n  assert_eq!(systems.len(), 3);\n  assert!(systems.contains(&Value::String(\"HTTP\".into())));\n  assert!(systems.contains(&Value::String(\"Kafka\".into())));\n}\n\n#[tokio::test]\nasync fn runs_return_stove_version() {\n  let server = TestServer::start().await;\n  server.seed_run_with_version(\"run-1\", \"product-api\", \"0.23.1\");\n\n  let body = server.get_json(\"/runs\").await;\n  let runs = body.as_array().unwrap();\n\n  assert_eq!(runs[0][\"stove_version\"], \"0.23.1\");\n}\n\n#[tokio::test]\nasync fn runs_filters_by_app_name() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-a\", \"alpha-api\");\n  server.seed_run(\"run-b\", \"beta-api\");\n\n  let body = server.get_json(\"/runs?app=alpha-api\").await;\n  let runs = body.as_array().unwrap();\n  assert_eq!(runs.len(), 1);\n  assert_eq!(runs[0][\"app_name\"], \"alpha-api\");\n}\n\n#[tokio::test]\nasync fn runs_returns_empty_for_unknown_app() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs?app=nonexistent\").await;\n  assert_eq!(body, Value::Array(vec![]));\n}\n\n// ---------------------------------------------------------------------------\n// GET /api/v1/runs/:run_id\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn get_run_returns_single_run() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs/run-1\").await;\n  assert_eq!(body[\"id\"], \"run-1\");\n  assert_eq!(body[\"app_name\"], \"product-api\");\n  assert_eq!(body[\"started_at\"], \"2024-06-01T10:00:00Z\");\n  assert_eq!(body[\"ended_at\"], \"2024-06-01T10:00:10Z\");\n}\n\n#[tokio::test]\nasync fn get_run_returns_null_for_unknown_id() {\n  let server = TestServer::start().await;\n\n  let body = server.get_json(\"/runs/nonexistent\").await;\n  assert_eq!(body, Value::Null);\n}\n\n// ---------------------------------------------------------------------------\n// GET /api/v1/runs/:run_id/tests\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn get_tests_returns_tests_for_run() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs/run-1/tests\").await;\n  let tests = body.as_array().unwrap();\n  assert_eq!(tests.len(), 2);\n\n  assert_eq!(tests[0][\"test_name\"], \"should create product\");\n  assert_eq!(tests[0][\"spec_name\"], \"ProductSpec\");\n  assert_eq!(tests[0][\"status\"], \"PASSED\");\n  assert_eq!(tests[0][\"duration_ms\"], 1500);\n  assert!(tests[0][\"error\"].is_null());\n\n  assert_eq!(tests[1][\"test_name\"], \"should reject duplicate\");\n  assert_eq!(tests[1][\"status\"], \"FAILED\");\n  assert_eq!(tests[1][\"error\"], \"Expected conflict but got success\");\n}\n\n#[tokio::test]\nasync fn get_tests_returns_empty_for_unknown_run() {\n  let server = TestServer::start().await;\n\n  let body = server.get_json(\"/runs/nonexistent/tests\").await;\n  assert_eq!(body, Value::Array(vec![]));\n}\n\n#[tokio::test]\nasync fn concurrent_running_tests_are_visible_via_api_while_run_is_in_progress() {\n  let server = TestServer::start().await;\n  let service = Arc::new(DashboardEventServiceImpl::new(\n    server.repo.clone(),\n    server.sse.clone(),\n  ));\n\n  send_event(\n    service.as_ref(),\n    run_started_event(\"run-concurrent\", \"concurrent-app\", 1_704_067_200, 0),\n  )\n  .await\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      send_event(\n        service_a.as_ref(),\n        test_started_event(\n          \"run-concurrent\",\n          \"test-a\",\n          \"handles checkout\",\n          \"ConcurrentSpec\",\n          1_704_067_201,\n          0,\n        ),\n      )\n      .await\n    },\n    async move {\n      send_event(\n        service_b.as_ref(),\n        test_started_event(\n          \"run-concurrent\",\n          \"test-b\",\n          \"handles payment\",\n          \"ConcurrentSpec\",\n          1_704_067_201,\n          0,\n        ),\n      )\n      .await\n    },\n  )\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      send_event(\n        service_a.as_ref(),\n        entry_recorded_event(\n          \"run-concurrent\",\n          \"test-a\",\n          \"GET /checkout\",\n          \"PASSED\",\n          \"trace-a\",\n          1_704_067_202,\n          0,\n        ),\n      )\n      .await\n    },\n    async move {\n      send_event(\n        service_b.as_ref(),\n        entry_recorded_event(\n          \"run-concurrent\",\n          \"test-b\",\n          \"POST /payment\",\n          \"PASSED\",\n          \"trace-b\",\n          1_704_067_202,\n          0,\n        ),\n      )\n      .await\n    },\n  )\n  .unwrap();\n\n  flush_events(service.as_ref()).await;\n\n  let run = server.get_json(\"/runs/run-concurrent\").await;\n  assert_eq!(run[\"status\"], \"RUNNING\");\n\n  let tests = server.get_json(\"/runs/run-concurrent/tests\").await;\n  let tests = tests.as_array().unwrap();\n  assert_eq!(tests.len(), 2);\n\n  let test_a = tests.iter().find(|test| test[\"id\"] == \"test-a\").unwrap();\n  assert_eq!(test_a[\"status\"], \"RUNNING\");\n  assert!(test_a[\"ended_at\"].is_null());\n\n  let test_b = tests.iter().find(|test| test[\"id\"] == \"test-b\").unwrap();\n  assert_eq!(test_b[\"status\"], \"RUNNING\");\n  assert!(test_b[\"ended_at\"].is_null());\n\n  let entries_a = server\n    .get_json(\"/runs/run-concurrent/tests/test-a/entries\")\n    .await;\n  let entries_a = entries_a.as_array().unwrap();\n  assert_eq!(entries_a.len(), 1);\n  assert_eq!(entries_a[0][\"action\"], \"GET /checkout\");\n\n  let entries_b = server\n    .get_json(\"/runs/run-concurrent/tests/test-b/entries\")\n    .await;\n  let entries_b = entries_b.as_array().unwrap();\n  assert_eq!(entries_b.len(), 1);\n  assert_eq!(entries_b[0][\"action\"], \"POST /payment\");\n}\n\n#[tokio::test]\nasync fn concurrent_interleaved_test_lifecycle_remains_isolated_across_api_views() {\n  let server = TestServer::start().await;\n  let service = Arc::new(DashboardEventServiceImpl::new(\n    server.repo.clone(),\n    server.sse.clone(),\n  ));\n\n  send_event(\n    service.as_ref(),\n    run_started_event(\"run-interleaved\", \"concurrent-app\", 1_704_067_300, 0),\n  )\n  .await\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      send_event(\n        service_a.as_ref(),\n        test_started_event(\n          \"run-interleaved\",\n          \"test-a\",\n          \"handles checkout\",\n          \"ConcurrentSpec\",\n          1_704_067_301,\n          0,\n        ),\n      )\n      .await\n    },\n    async move {\n      send_event(\n        service_b.as_ref(),\n        test_started_event(\n          \"run-interleaved\",\n          \"test-b\",\n          \"handles payment\",\n          \"ConcurrentSpec\",\n          1_704_067_301,\n          0,\n        ),\n      )\n      .await\n    },\n  )\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      send_event(\n        service_a.as_ref(),\n        entry_recorded_event(\n          \"run-interleaved\",\n          \"test-a\",\n          \"GET /checkout\",\n          \"PASSED\",\n          \"trace-a\",\n          1_704_067_302,\n          0,\n        ),\n      )\n      .await\n    },\n    async move {\n      send_event(\n        service_b.as_ref(),\n        entry_recorded_event(\n          \"run-interleaved\",\n          \"test-b\",\n          \"POST /payment\",\n          \"FAILED\",\n          \"trace-b\",\n          1_704_067_302,\n          0,\n        ),\n      )\n      .await\n    },\n  )\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      send_event(\n        service_a.as_ref(),\n        span_recorded_event(\n          \"run-interleaved\",\n          \"trace-a\",\n          \"span-a\",\n          \"\",\n          \"GET /checkout\",\n          \"checkout-api\",\n          1_000_000_000,\n          1_100_000_000,\n        ),\n      )\n      .await\n    },\n    async move {\n      send_event(\n        service_b.as_ref(),\n        span_recorded_event(\n          \"run-interleaved\",\n          \"trace-b\",\n          \"span-b\",\n          \"\",\n          \"POST /payment\",\n          \"payment-api\",\n          1_200_000_000,\n          1_350_000_000,\n        ),\n      )\n      .await\n    },\n  )\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      send_event(\n        service_a.as_ref(),\n        snapshot_event(\n          \"run-interleaved\",\n          \"test-a\",\n          \"Kafka\",\n          r#\"{\"published\":1}\"#,\n          \"1 published\",\n        ),\n      )\n      .await\n    },\n    async move {\n      send_event(\n        service_b.as_ref(),\n        snapshot_event(\n          \"run-interleaved\",\n          \"test-b\",\n          \"Redis\",\n          r#\"{\"keys\":2}\"#,\n          \"2 keys\",\n        ),\n      )\n      .await\n    },\n  )\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      send_event(\n        service_a.as_ref(),\n        test_ended_event(\n          \"run-interleaved\",\n          \"test-a\",\n          \"PASSED\",\n          1_200,\n          \"\",\n          1_704_067_303,\n          0,\n        ),\n      )\n      .await\n    },\n    async move {\n      send_event(\n        service_b.as_ref(),\n        test_ended_event(\n          \"run-interleaved\",\n          \"test-b\",\n          \"FAILED\",\n          1_500,\n          \"payment timeout\",\n          1_704_067_303,\n          0,\n        ),\n      )\n      .await\n    },\n  )\n  .unwrap();\n\n  send_event(\n    service.as_ref(),\n    run_ended_event(\"run-interleaved\", 2, 1, 1, 3_000, 1_704_067_304, 0),\n  )\n  .await\n  .unwrap();\n\n  flush_events(service.as_ref()).await;\n\n  let run = server.get_json(\"/runs/run-interleaved\").await;\n  assert_eq!(run[\"status\"], \"FAILED\");\n  assert_eq!(run[\"total_tests\"], 2);\n  assert_eq!(run[\"passed\"], 1);\n  assert_eq!(run[\"failed\"], 1);\n\n  let tests = server.get_json(\"/runs/run-interleaved/tests\").await;\n  let tests = tests.as_array().unwrap();\n  assert_eq!(tests.len(), 2);\n\n  let test_a = tests.iter().find(|test| test[\"id\"] == \"test-a\").unwrap();\n  assert_eq!(test_a[\"status\"], \"PASSED\");\n  assert!(test_a[\"error\"].is_null());\n\n  let test_b = tests.iter().find(|test| test[\"id\"] == \"test-b\").unwrap();\n  assert_eq!(test_b[\"status\"], \"FAILED\");\n  assert_eq!(test_b[\"error\"], \"payment timeout\");\n\n  let spans_a = server\n    .get_json(\"/runs/run-interleaved/tests/test-a/spans\")\n    .await;\n  let spans_a = spans_a.as_array().unwrap();\n  assert_eq!(spans_a.len(), 1);\n  assert_eq!(spans_a[0][\"span_id\"], \"span-a\");\n\n  let spans_b = server\n    .get_json(\"/runs/run-interleaved/tests/test-b/spans\")\n    .await;\n  let spans_b = spans_b.as_array().unwrap();\n  assert_eq!(spans_b.len(), 1);\n  assert_eq!(spans_b[0][\"span_id\"], \"span-b\");\n\n  let snapshots_a = server\n    .get_json(\"/runs/run-interleaved/tests/test-a/snapshots\")\n    .await;\n  let snapshots_a = snapshots_a.as_array().unwrap();\n  assert_eq!(snapshots_a.len(), 1);\n  assert_eq!(snapshots_a[0][\"system\"], \"Kafka\");\n\n  let snapshots_b = server\n    .get_json(\"/runs/run-interleaved/tests/test-b/snapshots\")\n    .await;\n  let snapshots_b = snapshots_b.as_array().unwrap();\n  assert_eq!(snapshots_b.len(), 1);\n  assert_eq!(snapshots_b[0][\"system\"], \"Redis\");\n}\n\n// ---------------------------------------------------------------------------\n// GET /api/v1/runs/:run_id/tests/:test_id/entries\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn get_entries_returns_entries_for_test() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs/run-1/tests/test-1/entries\").await;\n  let entries = body.as_array().unwrap();\n  assert_eq!(entries.len(), 1);\n  assert_eq!(entries[0][\"system\"], \"HTTP\");\n  assert_eq!(entries[0][\"action\"], \"POST /products\");\n  assert_eq!(entries[0][\"result\"], \"PASSED\");\n  assert_eq!(entries[0][\"input\"], r#\"{\"name\":\"widget\"}\"#);\n  assert_eq!(entries[0][\"output\"], r#\"{\"id\":42}\"#);\n  assert_eq!(entries[0][\"trace_id\"], \"trace-abc\");\n}\n\n#[tokio::test]\nasync fn get_entries_isolates_by_test_id() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs/run-1/tests/test-2/entries\").await;\n  let entries = body.as_array().unwrap();\n  assert_eq!(entries.len(), 1);\n  assert_eq!(entries[0][\"result\"], \"FAILED\");\n  assert_eq!(entries[0][\"expected\"], \"409 Conflict\");\n  assert_eq!(entries[0][\"actual\"], \"201 Created\");\n}\n\n#[tokio::test]\nasync fn get_entries_returns_empty_for_unknown_test() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server\n    .get_json(\"/runs/run-1/tests/nonexistent/entries\")\n    .await;\n  assert_eq!(body, Value::Array(vec![]));\n}\n\n// ---------------------------------------------------------------------------\n// GET /api/v1/runs/:run_id/tests/:test_id/spans\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn get_test_spans_returns_spans_linked_via_trace_id() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs/run-1/tests/test-1/spans\").await;\n  let spans = body.as_array().unwrap();\n  assert_eq!(spans.len(), 2);\n  assert_eq!(spans[0][\"operation_name\"], \"POST /products\");\n  assert_eq!(spans[0][\"status\"], \"OK\");\n  assert_eq!(spans[1][\"operation_name\"], \"INSERT INTO products\");\n  assert_eq!(spans[1][\"parent_span_id\"], \"span-1\");\n}\n\n#[tokio::test]\nasync fn get_test_spans_returns_empty_when_no_trace_linked() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs/run-1/tests/test-2/spans\").await;\n  let spans = body.as_array().unwrap();\n  assert_eq!(spans.len(), 0);\n}\n\n#[tokio::test]\nasync fn get_test_spans_returns_spans_linked_via_attribute() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"test-attr\", \"attribute test\", \"TracingSpec\");\n  // Entry without trace_id — spans linked only via attributes\n  server.seed_entry(\"run-1\", \"test-attr\", \"HTTP\", \"GET /items\", \"PASSED\");\n  // Root span with x-stove-test-id attribute\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-xyz\",\n    \"s1\",\n    \"\",\n    \"GET /items\",\n    \"my-app\",\n    1_000_000_000,\n    1_100_000_000,\n    r#\"{\"x-stove-test-id\":\"test-attr\",\"http.method\":\"GET\"}\"#,\n  );\n  // Child span in the same trace without the attribute\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-xyz\",\n    \"s2\",\n    \"s1\",\n    \"SELECT items\",\n    \"my-db\",\n    1_020_000_000,\n    1_080_000_000,\n    r#\"{\"db.system\":\"postgresql\"}\"#,\n  );\n  server.end_test(\"run-1\", \"test-attr\", 500);\n\n  let body = server.get_json(\"/runs/run-1/tests/test-attr/spans\").await;\n  let spans = body.as_array().unwrap();\n  assert_eq!(spans.len(), 2, \"both spans in the trace should appear\");\n  assert_eq!(spans[0][\"span_id\"], \"s1\");\n  assert_eq!(spans[1][\"span_id\"], \"s2\");\n}\n\n#[tokio::test]\nasync fn get_test_spans_does_not_cross_match_similar_test_ids_in_attributes() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"test-1\", \"first test\", \"TracingSpec\");\n  server.seed_test(\"run-1\", \"test-10\", \"tenth test\", \"TracingSpec\");\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-10\",\n    \"span-10\",\n    \"\",\n    \"GET /ten\",\n    \"my-app\",\n    1_000_000_000,\n    1_100_000_000,\n    r#\"{\"x-stove-test-id\":\"test-10\",\"http.method\":\"GET\"}\"#,\n  );\n\n  let body = server.get_json(\"/runs/run-1/tests/test-1/spans\").await;\n  let spans = body.as_array().unwrap();\n\n  assert_eq!(\n    spans.len(),\n    0,\n    \"test-1 should not receive spans from test-10\"\n  );\n}\n\n#[tokio::test]\nasync fn get_test_spans_combines_entry_and_attribute_linked_traces() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"test-combo\", \"combo test\", \"TracingSpec\");\n  // Entry links to trace-a\n  server.seed_entry_full(\n    \"run-1\",\n    \"test-combo\",\n    \"HTTP\",\n    \"POST /orders\",\n    \"PASSED\",\n    \"\",\n    \"\",\n    \"\",\n    \"\",\n    \"\",\n    \"trace-a\",\n  );\n  // Span in trace-a (linked via entry)\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-a\",\n    \"sa1\",\n    \"\",\n    \"POST /orders\",\n    \"order-svc\",\n    1_000_000_000,\n    1_100_000_000,\n    \"{}\",\n  );\n  // Span in trace-b (linked via attribute only)\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-b\",\n    \"sb1\",\n    \"\",\n    \"process-event\",\n    \"worker\",\n    2_000_000_000,\n    2_200_000_000,\n    r#\"{\"x-stove-test-id\":\"test-combo\"}\"#,\n  );\n  server.end_test(\"run-1\", \"test-combo\", 1000);\n\n  let body = server.get_json(\"/runs/run-1/tests/test-combo/spans\").await;\n  let spans = body.as_array().unwrap();\n  assert_eq!(spans.len(), 2);\n  let span_ids: Vec<&str> = spans\n    .iter()\n    .map(|s| s[\"span_id\"].as_str().unwrap())\n    .collect();\n  assert!(span_ids.contains(&\"sa1\"), \"entry-linked span\");\n  assert!(span_ids.contains(&\"sb1\"), \"attribute-linked span\");\n}\n\n#[tokio::test]\nasync fn get_test_spans_returns_full_trace_when_one_span_has_attribute() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"test-full\", \"full trace test\", \"TracingSpec\");\n  server.seed_entry(\"run-1\", \"test-full\", \"HTTP\", \"GET /\", \"PASSED\");\n  // Only root span has the test-id attribute\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-full\",\n    \"root\",\n    \"\",\n    \"GET /\",\n    \"gateway\",\n    1_000_000_000,\n    1_500_000_000,\n    r#\"{\"x-stove-test-id\":\"test-full\"}\"#,\n  );\n  // Children don't have the attribute\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-full\",\n    \"child-1\",\n    \"root\",\n    \"auth-check\",\n    \"auth-svc\",\n    1_050_000_000,\n    1_150_000_000,\n    r#\"{\"auth.type\":\"jwt\"}\"#,\n  );\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-full\",\n    \"child-2\",\n    \"root\",\n    \"db-query\",\n    \"db-svc\",\n    1_200_000_000,\n    1_400_000_000,\n    r#\"{\"db.system\":\"mysql\"}\"#,\n  );\n  server.end_test(\"run-1\", \"test-full\", 2000);\n\n  let body = server.get_json(\"/runs/run-1/tests/test-full/spans\").await;\n  let spans = body.as_array().unwrap();\n  assert_eq!(spans.len(), 3, \"all spans in the trace should be returned\");\n  assert_eq!(spans[0][\"span_id\"], \"root\");\n  assert_eq!(spans[1][\"span_id\"], \"child-1\");\n  assert_eq!(spans[2][\"span_id\"], \"child-2\");\n}\n\n#[tokio::test]\nasync fn get_test_spans_ordered_by_start_time() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"test-order\", \"ordering test\", \"TracingSpec\");\n  server.seed_entry_full(\n    \"run-1\",\n    \"test-order\",\n    \"HTTP\",\n    \"GET /\",\n    \"PASSED\",\n    \"\",\n    \"\",\n    \"\",\n    \"\",\n    \"\",\n    \"trace-ord\",\n  );\n  // Insert spans out of chronological order\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-ord\",\n    \"late\",\n    \"\",\n    \"late-op\",\n    \"svc\",\n    3_000_000_000,\n    3_500_000_000,\n    \"{}\",\n  );\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-ord\",\n    \"early\",\n    \"\",\n    \"early-op\",\n    \"svc\",\n    1_000_000_000,\n    1_500_000_000,\n    \"{}\",\n  );\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-ord\",\n    \"mid\",\n    \"\",\n    \"mid-op\",\n    \"svc\",\n    2_000_000_000,\n    2_500_000_000,\n    \"{}\",\n  );\n  server.end_test(\"run-1\", \"test-order\", 1000);\n\n  let body = server.get_json(\"/runs/run-1/tests/test-order/spans\").await;\n  let spans = body.as_array().unwrap();\n  assert_eq!(spans.len(), 3);\n  assert_eq!(spans[0][\"operation_name\"], \"early-op\");\n  assert_eq!(spans[1][\"operation_name\"], \"mid-op\");\n  assert_eq!(spans[2][\"operation_name\"], \"late-op\");\n}\n\n#[tokio::test]\nasync fn get_test_spans_includes_exception_data() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"test-exc\", \"exception test\", \"TracingSpec\");\n  server.seed_entry_full(\n    \"run-1\",\n    \"test-exc\",\n    \"HTTP\",\n    \"POST /fail\",\n    \"FAILED\",\n    \"\",\n    \"\",\n    \"\",\n    \"\",\n    \"\",\n    \"trace-exc\",\n  );\n  server.seed_span_with_exception(\n    \"run-1\",\n    \"trace-exc\",\n    \"err-span\",\n    \"\",\n    \"POST /fail\",\n    \"my-svc\",\n    \"ERROR\",\n    \"java.lang.NullPointerException\",\n    \"Cannot invoke method on null\",\n    \"at com.example.Service.process(Service.java:42)\",\n  );\n  server.end_test_failed(\"run-1\", \"test-exc\", 200, \"NPE\");\n\n  let body = server.get_json(\"/runs/run-1/tests/test-exc/spans\").await;\n  let spans = body.as_array().unwrap();\n  assert_eq!(spans.len(), 1);\n  assert_eq!(spans[0][\"status\"], \"ERROR\");\n  assert_eq!(spans[0][\"exception_type\"], \"java.lang.NullPointerException\");\n  assert_eq!(\n    spans[0][\"exception_message\"],\n    \"Cannot invoke method on null\"\n  );\n  assert!(\n    spans[0][\"exception_stack_trace\"]\n      .as_str()\n      .unwrap()\n      .contains(\"Service.java:42\")\n  );\n}\n\n#[tokio::test]\nasync fn get_test_spans_isolates_between_tests() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"t1\", \"test one\", \"Spec\");\n  server.seed_entry_full(\n    \"run-1\", \"t1\", \"HTTP\", \"GET /a\", \"PASSED\", \"\", \"\", \"\", \"\", \"\", \"trace-t1\",\n  );\n  server.seed_span(\"run-1\", \"trace-t1\", \"s-t1\", \"\", \"GET /a\", \"svc\");\n  server.end_test(\"run-1\", \"t1\", 100);\n  server.seed_test(\"run-1\", \"t2\", \"test two\", \"Spec\");\n  server.seed_entry_full(\n    \"run-1\", \"t2\", \"HTTP\", \"GET /b\", \"PASSED\", \"\", \"\", \"\", \"\", \"\", \"trace-t2\",\n  );\n  server.seed_span(\"run-1\", \"trace-t2\", \"s-t2\", \"\", \"GET /b\", \"svc\");\n  server.end_test(\"run-1\", \"t2\", 100);\n  server.end_run(\"run-1\", 2, 0, 500);\n\n  let t1_spans = server.get_json(\"/runs/run-1/tests/t1/spans\").await;\n  assert_eq!(t1_spans.as_array().unwrap().len(), 1);\n  assert_eq!(t1_spans[0][\"span_id\"], \"s-t1\");\n\n  let t2_spans = server.get_json(\"/runs/run-1/tests/t2/spans\").await;\n  assert_eq!(t2_spans.as_array().unwrap().len(), 1);\n  assert_eq!(t2_spans[0][\"span_id\"], \"s-t2\");\n}\n\n// ---------------------------------------------------------------------------\n// GET /api/v1/runs/:run_id/tests/:test_id/snapshots\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn get_snapshots_returns_snapshots_for_test() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs/run-1/tests/test-1/snapshots\").await;\n  let snapshots = body.as_array().unwrap();\n  assert_eq!(snapshots.len(), 1);\n  assert_eq!(snapshots[0][\"system\"], \"Kafka\");\n  assert_eq!(snapshots[0][\"summary\"], \"3 consumed, 1 published\");\n  assert_eq!(\n    snapshots[0][\"state_json\"],\n    r#\"{\"consumed\":3,\"published\":1}\"#\n  );\n}\n\n#[tokio::test]\nasync fn get_snapshots_returns_empty_for_test_without_snapshots() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/runs/run-1/tests/test-2/snapshots\").await;\n  assert_eq!(body, Value::Array(vec![]));\n}\n\n#[tokio::test]\nasync fn get_snapshots_returns_multiple_systems() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"test-snap\", \"snapshot test\", \"SnapshotSpec\");\n  server.seed_snapshot(\n    \"run-1\",\n    \"test-snap\",\n    \"Kafka\",\n    r#\"{\"consumed\":5,\"published\":2}\"#,\n    \"5 consumed, 2 published\",\n  );\n  server.seed_snapshot(\n    \"run-1\",\n    \"test-snap\",\n    \"PostgreSQL\",\n    r#\"{\"rows_inserted\":10}\"#,\n    \"10 rows inserted\",\n  );\n  server.seed_snapshot(\n    \"run-1\",\n    \"test-snap\",\n    \"Redis\",\n    r#\"{\"keys_set\":3}\"#,\n    \"3 keys set\",\n  );\n  server.end_test(\"run-1\", \"test-snap\", 300);\n\n  let body = server\n    .get_json(\"/runs/run-1/tests/test-snap/snapshots\")\n    .await;\n  let snaps = body.as_array().unwrap();\n  assert_eq!(snaps.len(), 3);\n  let systems: Vec<&str> = snaps\n    .iter()\n    .map(|s| s[\"system\"].as_str().unwrap())\n    .collect();\n  assert!(systems.contains(&\"Kafka\"));\n  assert!(systems.contains(&\"PostgreSQL\"));\n  assert!(systems.contains(&\"Redis\"));\n}\n\n#[tokio::test]\nasync fn get_snapshots_isolates_between_tests() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"t1\", \"test one\", \"Spec\");\n  server.seed_snapshot(\"run-1\", \"t1\", \"Kafka\", r#\"{\"consumed\":1}\"#, \"1 consumed\");\n  server.end_test(\"run-1\", \"t1\", 100);\n  server.seed_test(\"run-1\", \"t2\", \"test two\", \"Spec\");\n  server.seed_snapshot(\"run-1\", \"t2\", \"Redis\", r#\"{\"keys\":5}\"#, \"5 keys\");\n  server.end_test(\"run-1\", \"t2\", 100);\n  server.end_run(\"run-1\", 2, 0, 500);\n\n  let t1_snaps = server.get_json(\"/runs/run-1/tests/t1/snapshots\").await;\n  let t1_arr = t1_snaps.as_array().unwrap();\n  assert_eq!(t1_arr.len(), 1);\n  assert_eq!(t1_arr[0][\"system\"], \"Kafka\");\n\n  let t2_snaps = server.get_json(\"/runs/run-1/tests/t2/snapshots\").await;\n  let t2_arr = t2_snaps.as_array().unwrap();\n  assert_eq!(t2_arr.len(), 1);\n  assert_eq!(t2_arr[0][\"system\"], \"Redis\");\n}\n\n#[tokio::test]\nasync fn snapshot_state_json_preserves_complex_json() {\n  let server = TestServer::start().await;\n  server.seed_run(\"run-1\", \"my-app\");\n  server.seed_test(\"run-1\", \"test-json\", \"json test\", \"Spec\");\n  let complex_json = r#\"{\"messages\":[{\"topic\":\"orders\",\"key\":\"k1\",\"value\":{\"orderId\":123}},{\"topic\":\"orders\",\"key\":\"k2\",\"value\":{\"orderId\":456}}],\"count\":2}\"#;\n  server.seed_snapshot(\"run-1\", \"test-json\", \"Kafka\", complex_json, \"2 messages\");\n  server.end_test(\"run-1\", \"test-json\", 100);\n\n  let body = server\n    .get_json(\"/runs/run-1/tests/test-json/snapshots\")\n    .await;\n  let snaps = body.as_array().unwrap();\n  assert_eq!(snaps[0][\"state_json\"], complex_json);\n}\n\n// ---------------------------------------------------------------------------\n// GET /api/v1/traces/:trace_id\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn get_trace_returns_all_spans_for_trace() {\n  let server = TestServer::start().await;\n  server.seed_full_run();\n\n  let body = server.get_json(\"/traces/trace-abc\").await;\n  let spans = body.as_array().unwrap();\n  assert_eq!(spans.len(), 2);\n  assert_eq!(spans[0][\"span_id\"], \"span-1\");\n  assert!(spans[0][\"parent_span_id\"].is_null());\n  assert_eq!(spans[0][\"service_name\"], \"product-api\");\n  assert_eq!(spans[1][\"span_id\"], \"span-2\");\n  assert_eq!(spans[1][\"parent_span_id\"], \"span-1\");\n  assert_eq!(spans[1][\"service_name\"], \"product-db\");\n}\n\n#[tokio::test]\nasync fn get_trace_returns_empty_for_unknown_trace() {\n  let server = TestServer::start().await;\n\n  let body = server.get_json(\"/traces/nonexistent\").await;\n  assert_eq!(body, Value::Array(vec![]));\n}\n\n// ---------------------------------------------------------------------------\n// SSE endpoint\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn sse_endpoint_returns_200_with_event_stream_content_type() {\n  let server = TestServer::start().await;\n\n  let resp = server.get(\"/events/stream\").await;\n  assert_eq!(resp.status(), StatusCode::OK);\n  let content_type = resp\n    .headers()\n    .get(\"content-type\")\n    .unwrap()\n    .to_str()\n    .unwrap();\n  assert!(\n    content_type.contains(\"text/event-stream\"),\n    \"Expected text/event-stream, got: {content_type}\",\n  );\n}\n\n#[tokio::test]\nasync fn sse_stream_pushes_full_events_before_database_flush() {\n  let server = TestServer::start().await;\n  let service = Arc::new(DashboardEventServiceImpl::new_with_ingest_config(\n    server.repo.clone(),\n    server.sse.clone(),\n    50,\n    Duration::from_secs(60),\n  ));\n  let mut resp = server.get(\"/events/stream\").await;\n  let mut buffer = String::new();\n\n  assert_eq!(resp.status(), StatusCode::OK);\n\n  send_event(\n    service.as_ref(),\n    run_started_event_with_version(\"run-live-sse\", \"live-sse-app\", \"0.23.2\", 1_704_067_200, 0),\n  )\n  .await\n  .unwrap();\n  send_event(\n    service.as_ref(),\n    test_started_event(\n      \"run-live-sse\",\n      \"test-live\",\n      \"streams before sqlite\",\n      \"LiveSpec\",\n      1_704_067_201,\n      0,\n    ),\n  )\n  .await\n  .unwrap();\n\n  let first_event: Value =\n    serde_json::from_str(&next_sse_data(&mut resp, &mut buffer).await.unwrap()).unwrap();\n  assert_eq!(first_event[\"seq\"], 1);\n  assert_eq!(first_event[\"run_id\"], \"run-live-sse\");\n  assert_eq!(first_event[\"event_type\"], \"run_started\");\n  assert_eq!(first_event[\"payload\"][\"app_name\"], \"live-sse-app\");\n  assert_eq!(first_event[\"payload\"][\"stove_version\"], \"0.23.2\");\n\n  let second_event: Value =\n    serde_json::from_str(&next_sse_data(&mut resp, &mut buffer).await.unwrap()).unwrap();\n  assert_eq!(second_event[\"seq\"], 2);\n  assert_eq!(second_event[\"event_type\"], \"test_started\");\n  assert_eq!(second_event[\"payload\"][\"test_id\"], \"test-live\");\n  assert_eq!(second_event[\"payload\"][\"status\"], \"RUNNING\");\n\n  let run_before_flush = server.get_json(\"/runs/run-live-sse\").await;\n  assert_eq!(run_before_flush, Value::Null);\n\n  let tests_before_flush = server.get_json(\"/runs/run-live-sse/tests\").await;\n  assert_eq!(tests_before_flush, Value::Array(vec![]));\n\n  flush_events(service.as_ref()).await;\n\n  let run_after_flush = server.get_json(\"/runs/run-live-sse\").await;\n  assert_eq!(run_after_flush[\"status\"], \"RUNNING\");\n\n  let tests_after_flush = server.get_json(\"/runs/run-live-sse/tests\").await;\n  let tests_after_flush = tests_after_flush.as_array().unwrap();\n  assert_eq!(tests_after_flush.len(), 1);\n  assert_eq!(tests_after_flush[0][\"id\"], \"test-live\");\n  assert_eq!(tests_after_flush[0][\"status\"], \"RUNNING\");\n}\n\n#[tokio::test]\nasync fn sse_broadcast_data_is_readable_after_notification() {\n  // Simulates the real-world SSE flow: when the frontend receives an SSE\n  // event and immediately refetches, the data must already be in the DB.\n  //\n  // The broadcast must fire AFTER the DB write, not before.\n  let server = TestServer::start().await;\n  let mut rx = server.sse.subscribe();\n\n  // Seed a run via the repo so the FK is satisfied\n  server.seed_run(\"run-sse\", \"sse-app\");\n\n  // Seed a test via the repo (bypasses gRPC, but simulates the write)\n  server.seed_test(\"run-sse\", \"t-1\", \"my test\", \"Spec\");\n\n  // Broadcast an SSE event (simulates what process_event does after writing)\n  server\n    .sse\n    .broadcast(r#\"{\"run_id\":\"run-sse\",\"event_type\":\"test_started\"}\"#);\n\n  // Subscriber receives the notification\n  let msg = rx.try_recv().expect(\"should receive broadcast\");\n  assert!(msg.contains(\"run-sse\"));\n\n  // Immediately refetch — data must be present (this is what the browser does)\n  let body = server.get_json(\"/runs/run-sse/tests\").await;\n  let tests = body.as_array().unwrap();\n  assert_eq!(\n    tests.len(),\n    1,\n    \"Data must be readable when SSE notification arrives\"\n  );\n}\n\n#[tokio::test]\nasync fn sse_stream_sends_keep_alive() {\n  // Without keep-alive, browsers and proxies close idle SSE connections\n  // after 30-90 seconds, causing the UI to stop updating during long tests.\n  //\n  // With keep-alive enabled, the server sends a comment (`: keep-alive`)\n  // periodically. We verify by reading the first chunk with a timeout.\n  let server = TestServer::start().await;\n\n  let mut resp = server.get(\"/events/stream\").await;\n  assert_eq!(resp.status(), StatusCode::OK);\n\n  // Read the first chunk — with keep-alive, the server should send a\n  // comment within the interval (15s). Without it, this times out.\n  let result = tokio::time::timeout(std::time::Duration::from_secs(20), resp.chunk()).await;\n\n  assert!(\n    result.is_ok(),\n    \"SSE stream should send a keep-alive comment within 20 seconds\"\n  );\n  let chunk = result.unwrap().unwrap();\n  assert!(chunk.is_some(), \"Keep-alive chunk should not be empty\");\n}\n\n#[tokio::test]\nasync fn sse_stream_delivers_interleaved_notifications_for_concurrent_test_load() {\n  let server = TestServer::start().await;\n  let service = Arc::new(DashboardEventServiceImpl::new(\n    server.repo.clone(),\n    server.sse.clone(),\n  ));\n  let mut resp = server.get(\"/events/stream\").await;\n  let mut buffer = String::new();\n  let entry_count_per_test = 80usize;\n\n  assert_eq!(resp.status(), StatusCode::OK);\n\n  send_event(\n    service.as_ref(),\n    run_started_event(\"run-sse-load\", \"sse-load-app\", 1_704_067_400, 0),\n  )\n  .await\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      send_event(\n        service_a.as_ref(),\n        test_started_event(\n          \"run-sse-load\",\n          \"test-a\",\n          \"handles checkout\",\n          \"ConcurrentSpec\",\n          1_704_067_401,\n          0,\n        ),\n      )\n      .await\n    },\n    async move {\n      send_event(\n        service_b.as_ref(),\n        test_started_event(\n          \"run-sse-load\",\n          \"test-b\",\n          \"handles payment\",\n          \"ConcurrentSpec\",\n          1_704_067_401,\n          1,\n        ),\n      )\n      .await\n    },\n  )\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      for index in 0..entry_count_per_test {\n        send_event(\n          service_a.as_ref(),\n          entry_recorded_event(\n            \"run-sse-load\",\n            \"test-a\",\n            &format!(\"GET /checkout/{index}\"),\n            \"PASSED\",\n            \"trace-a\",\n            1_704_067_402 + index as i64,\n            0,\n          ),\n        )\n        .await?;\n      }\n      Ok::<(), tonic::Status>(())\n    },\n    async move {\n      for index in 0..entry_count_per_test {\n        send_event(\n          service_b.as_ref(),\n          entry_recorded_event(\n            \"run-sse-load\",\n            \"test-b\",\n            &format!(\"POST /payment/{index}\"),\n            \"FAILED\",\n            \"trace-b\",\n            1_704_067_402 + index as i64,\n            1,\n          ),\n        )\n        .await?;\n      }\n      Ok::<(), tonic::Status>(())\n    },\n  )\n  .unwrap();\n\n  let service_a = service.clone();\n  let service_b = service.clone();\n  tokio::try_join!(\n    async move {\n      send_event(\n        service_a.as_ref(),\n        test_ended_event(\n          \"run-sse-load\",\n          \"test-a\",\n          \"PASSED\",\n          2_000,\n          \"\",\n          1_704_067_500,\n          0,\n        ),\n      )\n      .await\n    },\n    async move {\n      send_event(\n        service_b.as_ref(),\n        test_ended_event(\n          \"run-sse-load\",\n          \"test-b\",\n          \"FAILED\",\n          2_200,\n          \"payment timeout\",\n          1_704_067_500,\n          1,\n        ),\n      )\n      .await\n    },\n  )\n  .unwrap();\n\n  send_event(\n    service.as_ref(),\n    run_ended_event(\"run-sse-load\", 2, 1, 1, 4_200, 1_704_067_501, 0),\n  )\n  .await\n  .unwrap();\n\n  let expected_events = 1 + 2 + (entry_count_per_test * 2) + 2 + 1;\n  let mut event_counts: HashMap<String, usize> = HashMap::new();\n\n  for _ in 0..expected_events {\n    let payload = next_sse_data(&mut resp, &mut buffer).await.unwrap();\n    let event: Value = serde_json::from_str(&payload).unwrap();\n    assert_eq!(event[\"run_id\"], \"run-sse-load\");\n\n    let event_type = event[\"event_type\"]\n      .as_str()\n      .expect(\"event_type should be present\")\n      .to_string();\n    *event_counts.entry(event_type).or_default() += 1;\n  }\n\n  assert_eq!(event_counts.get(\"run_started\"), Some(&1));\n  assert_eq!(event_counts.get(\"test_started\"), Some(&2));\n  assert_eq!(\n    event_counts.get(\"entry_recorded\"),\n    Some(&(entry_count_per_test * 2))\n  );\n  assert_eq!(event_counts.get(\"test_ended\"), Some(&2));\n  assert_eq!(event_counts.get(\"run_ended\"), Some(&1));\n\n  flush_events(service.as_ref()).await;\n\n  let tests = server.get_json(\"/runs/run-sse-load/tests\").await;\n  let tests = tests.as_array().unwrap();\n  assert_eq!(tests.len(), 2);\n\n  let test_a = tests.iter().find(|test| test[\"id\"] == \"test-a\").unwrap();\n  assert_eq!(test_a[\"status\"], \"PASSED\");\n\n  let test_b = tests.iter().find(|test| test[\"id\"] == \"test-b\").unwrap();\n  assert_eq!(test_b[\"status\"], \"FAILED\");\n\n  let entries_a = server\n    .get_json(\"/runs/run-sse-load/tests/test-a/entries\")\n    .await;\n  assert_eq!(entries_a.as_array().unwrap().len(), entry_count_per_test);\n\n  let entries_b = server\n    .get_json(\"/runs/run-sse-load/tests/test-b/entries\")\n    .await;\n  assert_eq!(entries_b.as_array().unwrap().len(), entry_count_per_test);\n}\n\n// ---------------------------------------------------------------------------\n// CORS headers\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn cors_headers_are_present() {\n  let server = TestServer::start().await;\n\n  let resp = server.get(\"/apps\").await;\n  assert!(\n    resp.headers().contains_key(\"access-control-allow-origin\"),\n    \"CORS header should be present\"\n  );\n}\n\n// ---------------------------------------------------------------------------\n// Running status (in-progress run)\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn in_progress_run_has_running_status() {\n  let server = TestServer::start().await;\n  server.seed_run_at(\"run-live\", \"live-api\", \"2024-06-01T12:00:00Z\", &[\"HTTP\"]);\n\n  let body = server.get_json(\"/runs/run-live\").await;\n  assert_eq!(body[\"status\"], \"RUNNING\");\n  assert!(body[\"ended_at\"].is_null());\n  assert!(body[\"duration_ms\"].is_null());\n\n  let apps = server.get_json(\"/apps\").await;\n  assert_eq!(apps[0][\"latest_status\"], \"RUNNING\");\n}\n\n// ---------------------------------------------------------------------------\n// SPA fallback\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn spa_fallback_serves_index_html_for_unknown_paths() {\n  let server = TestServer::start().await;\n\n  let resp = server\n    .client\n    .get(format!(\"{}/some/frontend/route\", server.base_url))\n    .send()\n    .await\n    .unwrap();\n\n  assert_ne!(\n    resp.status(),\n    StatusCode::METHOD_NOT_ALLOWED,\n    \"Should not return 405 for SPA routes\"\n  );\n}\n\n#[tokio::test]\nasync fn missing_spa_assets_return_404_instead_of_index_html() {\n  let server = TestServer::start().await;\n\n  let resp = server.get(\"/assets/does-not-exist.js\").await;\n\n  assert_eq!(resp.status(), StatusCode::NOT_FOUND);\n}\n\n// ---------------------------------------------------------------------------\n// Multiple runs (ordering and latest-run logic)\n// ---------------------------------------------------------------------------\n\n#[tokio::test]\nasync fn runs_are_ordered_by_started_at_desc() {\n  let server = TestServer::start().await;\n  server.seed_run_at(\"run-old\", \"my-app\", \"2024-01-01T00:00:00Z\", &[]);\n  server.seed_run_at(\"run-new\", \"my-app\", \"2024-06-01T00:00:00Z\", &[]);\n\n  let body = server.get_json(\"/runs?app=my-app\").await;\n  let runs = body.as_array().unwrap();\n  assert_eq!(runs.len(), 2);\n  assert_eq!(runs[0][\"id\"], \"run-new\");\n  assert_eq!(runs[1][\"id\"], \"run-old\");\n}\n\n#[tokio::test]\nasync fn runs_with_same_started_at_use_latest_inserted_as_tie_breaker() {\n  let server = TestServer::start().await;\n  server.seed_run_at(\"run-1\", \"my-app\", \"2024-06-01T00:00:00Z\", &[]);\n  server.seed_run_at(\"run-2\", \"my-app\", \"2024-06-01T00:00:00Z\", &[]);\n\n  let body = server.get_json(\"/runs?app=my-app\").await;\n  let runs = body.as_array().unwrap();\n\n  assert_eq!(runs.len(), 2);\n  assert_eq!(runs[0][\"id\"], \"run-2\");\n  assert_eq!(runs[1][\"id\"], \"run-1\");\n}\n\n#[tokio::test]\nasync fn apps_returns_latest_run_id_for_multi_run_app() {\n  let server = TestServer::start().await;\n  server.seed_run_at(\"run-1\", \"my-app\", \"2024-01-01T00:00:00Z\", &[]);\n  server.seed_run_at(\"run-2\", \"my-app\", \"2024-06-01T00:00:00Z\", &[]);\n\n  let body = server.get_json(\"/apps\").await;\n  let apps = body.as_array().unwrap();\n  assert_eq!(apps.len(), 1);\n  assert_eq!(apps[0][\"latest_run_id\"], \"run-2\");\n  assert_eq!(apps[0][\"total_runs\"], 2);\n}\n\n#[tokio::test]\nasync fn apps_does_not_duplicate_app_when_latest_runs_share_same_timestamp() {\n  let server = TestServer::start().await;\n  server.seed_run_at(\"run-1\", \"my-app\", \"2024-06-01T00:00:00Z\", &[]);\n  server.seed_run_at(\"run-2\", \"my-app\", \"2024-06-01T00:00:00Z\", &[]);\n\n  let body = server.get_json(\"/apps\").await;\n  let apps = body.as_array().unwrap();\n\n  assert_eq!(\n    apps.len(),\n    1,\n    \"same app should appear only once in the sidebar\"\n  );\n  assert_eq!(apps[0][\"latest_run_id\"], \"run-2\");\n  assert_eq!(apps[0][\"total_runs\"], 2);\n}\n"
  },
  {
    "path": "tools/stove-cli/tests/common/mod.rs",
    "content": "//! Shared test infrastructure for e2e tests.\n//!\n//! Provides `TestServer` (a real axum server on a random port with in-memory SQLite)\n//! and ergonomic seed helpers that hide `.unwrap()` noise and struct boilerplate.\n\n#![allow(dead_code)]\n\nuse std::sync::Arc;\n\nuse serde_json::Value;\nuse stove::http::server::create_router;\nuse stove::http::server::create_router_with_ingestor;\nuse stove::ingest::EventIngestor;\nuse stove::sse::manager::SseManager;\nuse stove::storage::database::Database;\nuse stove::storage::models::{NewEntry, NewSpan};\nuse stove::storage::repository::Repository;\n\n/// A running test server with its base URL and repository handle.\npub struct TestServer {\n  pub base_url: String,\n  pub repo: Arc<Repository>,\n  pub sse: Arc<SseManager>,\n  pub client: reqwest::Client,\n}\n\nimpl TestServer {\n  /// Start a test server on an OS-assigned port with an in-memory database.\n  pub async fn start() -> Self {\n    let db = Database::open(\":memory:\").expect(\"in-memory database should open\");\n    let repo = Arc::new(Repository::new(db));\n    let sse_manager = Arc::new(SseManager::new());\n    let router = create_router(repo.clone(), sse_manager.clone());\n\n    let listener = tokio::net::TcpListener::bind(\"127.0.0.1:0\")\n      .await\n      .expect(\"should bind to a free port\");\n    let port = listener.local_addr().unwrap().port();\n    let base_url = format!(\"http://127.0.0.1:{port}\");\n\n    tokio::spawn(async move {\n      axum::serve(\n        listener,\n        router.into_make_service_with_connect_info::<std::net::SocketAddr>(),\n      )\n      .await\n      .unwrap();\n    });\n\n    Self {\n      base_url,\n      repo,\n      sse: sse_manager,\n      client: reqwest::Client::new(),\n    }\n  }\n\n  /// Start a test server that shares an ingest queue with callers.\n  pub async fn start_with_ingestor() -> (Self, EventIngestor) {\n    let db = Database::open(\":memory:\").expect(\"in-memory database should open\");\n    let repo = Arc::new(Repository::new(db));\n    let sse_manager = Arc::new(SseManager::new());\n    let ingestor = EventIngestor::with_config(repo.clone(), 50, std::time::Duration::from_secs(60));\n    let router =\n      create_router_with_ingestor(repo.clone(), sse_manager.clone(), Some(ingestor.clone()));\n\n    let listener = tokio::net::TcpListener::bind(\"127.0.0.1:0\")\n      .await\n      .expect(\"should bind to a free port\");\n    let port = listener.local_addr().unwrap().port();\n    let base_url = format!(\"http://127.0.0.1:{port}\");\n\n    tokio::spawn(async move {\n      axum::serve(\n        listener,\n        router.into_make_service_with_connect_info::<std::net::SocketAddr>(),\n      )\n      .await\n      .unwrap();\n    });\n\n    (\n      Self {\n        base_url,\n        repo,\n        sse: sse_manager,\n        client: reqwest::Client::new(),\n      },\n      ingestor,\n    )\n  }\n\n  // ── HTTP helpers ──────────────────────────────────────────────────\n\n  pub fn url(&self, path: &str) -> String {\n    format!(\"{}/api/v1{path}\", self.base_url)\n  }\n\n  pub fn mcp_url(&self) -> String {\n    format!(\"{}/mcp\", self.base_url)\n  }\n\n  pub async fn get(&self, path: &str) -> reqwest::Response {\n    self\n      .client\n      .get(self.url(path))\n      .send()\n      .await\n      .expect(\"request should succeed\")\n  }\n\n  pub async fn get_json(&self, path: &str) -> Value {\n    self\n      .get(path)\n      .await\n      .json::<Value>()\n      .await\n      .expect(\"response should be valid JSON\")\n  }\n\n  // ── Seed helpers ──────────────────────────────────────────────────\n  //\n  // Each helper wraps a repository call with sensible defaults and panics\n  // on failure (tests should never hit DB errors with in-memory SQLite).\n\n  /// Start a run (status = RUNNING until `end_run` is called).\n  pub fn seed_run(&self, run_id: &str, app_name: &str) {\n    self.seed_run_at(run_id, app_name, \"2024-06-01T10:00:00Z\", &[]);\n  }\n\n  pub fn seed_run_with_version(&self, run_id: &str, app_name: &str, stove_version: &str) {\n    self.seed_run_at_with_version(\n      run_id,\n      app_name,\n      \"2024-06-01T10:00:00Z\",\n      Some(stove_version),\n      &[],\n    );\n  }\n\n  /// Start a run with explicit timestamp and systems.\n  pub fn seed_run_at(&self, run_id: &str, app_name: &str, started_at: &str, systems: &[&str]) {\n    self.seed_run_at_with_version(run_id, app_name, started_at, None, systems);\n  }\n\n  pub fn seed_run_at_with_version(\n    &self,\n    run_id: &str,\n    app_name: &str,\n    started_at: &str,\n    stove_version: Option<&str>,\n    systems: &[&str],\n  ) {\n    let systems: Vec<String> = systems.iter().map(|s| (*s).to_string()).collect();\n    self\n      .repo\n      .save_run_start_with_version(run_id, app_name, started_at, stove_version, &systems)\n      .unwrap();\n  }\n\n  /// End a run with stats.\n  pub fn end_run(&self, run_id: &str, passed: i32, failed: i32, duration_ms: i64) {\n    self\n      .repo\n      .save_run_end(\n        run_id,\n        \"2024-06-01T10:00:10Z\",\n        passed + failed,\n        passed,\n        failed,\n        duration_ms,\n      )\n      .unwrap();\n  }\n\n  /// Start a test within a run.\n  pub fn seed_test(&self, run_id: &str, test_id: &str, name: &str, spec: &str) {\n    self\n      .repo\n      .save_test_start(run_id, test_id, name, spec, &[], \"2024-06-01T10:00:01Z\")\n      .unwrap();\n  }\n\n  /// End a test (pass). For failures, use `end_test_failed`.\n  pub fn end_test(&self, run_id: &str, test_id: &str, duration_ms: i64) {\n    self\n      .repo\n      .save_test_end(\n        run_id,\n        test_id,\n        \"PASSED\",\n        duration_ms,\n        \"\",\n        \"2024-06-01T10:00:03Z\",\n      )\n      .unwrap();\n  }\n\n  /// End a test with FAILED status and an error message.\n  pub fn end_test_failed(&self, run_id: &str, test_id: &str, duration_ms: i64, error: &str) {\n    self\n      .repo\n      .save_test_end(\n        run_id,\n        test_id,\n        \"FAILED\",\n        duration_ms,\n        error,\n        \"2024-06-01T10:00:05Z\",\n      )\n      .unwrap();\n  }\n\n  /// Save a test entry with only the important fields; the rest default to empty.\n  pub fn seed_entry(&self, run_id: &str, test_id: &str, system: &str, action: &str, result: &str) {\n    self.seed_entry_full(\n      run_id, test_id, system, action, result, \"\", \"\", \"\", \"\", \"\", \"\",\n    );\n  }\n\n  /// Save a test entry with all fields specified.\n  #[allow(clippy::too_many_arguments)]\n  pub fn seed_entry_full(\n    &self,\n    run_id: &str,\n    test_id: &str,\n    system: &str,\n    action: &str,\n    result: &str,\n    input: &str,\n    output: &str,\n    expected: &str,\n    actual: &str,\n    error: &str,\n    trace_id: &str,\n  ) {\n    self\n      .repo\n      .save_entry(&NewEntry {\n        run_id: run_id.into(),\n        test_id: test_id.into(),\n        timestamp: \"2024-06-01T10:00:02Z\".into(),\n        system: system.into(),\n        action: action.into(),\n        result: result.into(),\n        input: input.into(),\n        output: output.into(),\n        metadata: \"{}\".into(),\n        expected: expected.into(),\n        actual: actual.into(),\n        error: error.into(),\n        trace_id: trace_id.into(),\n      })\n      .unwrap();\n  }\n\n  /// Save a span with only the key fields; the rest default to empty/zero.\n  pub fn seed_span(\n    &self,\n    run_id: &str,\n    trace_id: &str,\n    span_id: &str,\n    parent_span_id: &str,\n    operation: &str,\n    service: &str,\n  ) {\n    self\n      .repo\n      .save_span(&NewSpan {\n        run_id: run_id.into(),\n        trace_id: trace_id.into(),\n        span_id: span_id.into(),\n        parent_span_id: parent_span_id.into(),\n        operation_name: operation.into(),\n        service_name: service.into(),\n        start_time_nanos: 1_000_000_000,\n        end_time_nanos: 1_250_000_000,\n        status: \"OK\".into(),\n        attributes: \"{}\".into(),\n        ..Default::default()\n      })\n      .unwrap();\n  }\n\n  /// Save a span with explicit timing (for ordering assertions).\n  #[allow(clippy::too_many_arguments)]\n  pub fn seed_span_timed(\n    &self,\n    run_id: &str,\n    trace_id: &str,\n    span_id: &str,\n    parent_span_id: &str,\n    operation: &str,\n    service: &str,\n    start_nanos: i64,\n    end_nanos: i64,\n    attributes: &str,\n  ) {\n    self\n      .repo\n      .save_span(&NewSpan {\n        run_id: run_id.into(),\n        trace_id: trace_id.into(),\n        span_id: span_id.into(),\n        parent_span_id: parent_span_id.into(),\n        operation_name: operation.into(),\n        service_name: service.into(),\n        start_time_nanos: start_nanos,\n        end_time_nanos: end_nanos,\n        status: \"OK\".into(),\n        attributes: attributes.into(),\n        ..Default::default()\n      })\n      .unwrap();\n  }\n\n  /// Save a span with exception details.\n  #[allow(clippy::too_many_arguments)]\n  pub fn seed_span_with_exception(\n    &self,\n    run_id: &str,\n    trace_id: &str,\n    span_id: &str,\n    parent_span_id: &str,\n    operation: &str,\n    service: &str,\n    status: &str,\n    exception_type: &str,\n    exception_message: &str,\n    exception_stack_trace: &str,\n  ) {\n    self\n      .repo\n      .save_span(&NewSpan {\n        run_id: run_id.into(),\n        trace_id: trace_id.into(),\n        span_id: span_id.into(),\n        parent_span_id: parent_span_id.into(),\n        operation_name: operation.into(),\n        service_name: service.into(),\n        start_time_nanos: 1_000_000_000,\n        end_time_nanos: 1_250_000_000,\n        status: status.into(),\n        attributes: \"{}\".into(),\n        exception_type: exception_type.into(),\n        exception_message: exception_message.into(),\n        exception_stack_trace: exception_stack_trace.into(),\n      })\n      .unwrap();\n  }\n\n  /// Save a snapshot for a test.\n  pub fn seed_snapshot(\n    &self,\n    run_id: &str,\n    test_id: &str,\n    system: &str,\n    state_json: &str,\n    summary: &str,\n  ) {\n    self\n      .repo\n      .save_snapshot(run_id, test_id, system, state_json, summary)\n      .unwrap();\n  }\n\n  /// Seed a complete run with one passing test and one failing test.\n  ///\n  /// Creates: run-1 (product-api), test-1 (PASSED), test-2 (FAILED),\n  /// entries for both, 2 spans with trace-abc, a Kafka snapshot on test-1.\n  pub fn seed_full_run(&self) {\n    self.seed_run_at(\n      \"run-1\",\n      \"product-api\",\n      \"2024-06-01T10:00:00Z\",\n      &[\"HTTP\", \"Kafka\", \"PostgreSQL\"],\n    );\n\n    // Test 1: passing\n    self.seed_test(\"run-1\", \"test-1\", \"should create product\", \"ProductSpec\");\n    self.seed_entry_full(\n      \"run-1\",\n      \"test-1\",\n      \"HTTP\",\n      \"POST /products\",\n      \"PASSED\",\n      r#\"{\"name\":\"widget\"}\"#,\n      r#\"{\"id\":42}\"#,\n      \"\",\n      \"\",\n      \"\",\n      \"trace-abc\",\n    );\n    self.seed_span_timed(\n      \"run-1\",\n      \"trace-abc\",\n      \"span-1\",\n      \"\",\n      \"POST /products\",\n      \"product-api\",\n      1_000_000_000,\n      1_250_000_000,\n      r#\"{\"http.method\":\"POST\",\"http.status_code\":\"201\"}\"#,\n    );\n    self.seed_span_timed(\n      \"run-1\",\n      \"trace-abc\",\n      \"span-2\",\n      \"span-1\",\n      \"INSERT INTO products\",\n      \"product-db\",\n      1_050_000_000,\n      1_200_000_000,\n      r#\"{\"db.system\":\"postgresql\"}\"#,\n    );\n    self.seed_snapshot(\n      \"run-1\",\n      \"test-1\",\n      \"Kafka\",\n      r#\"{\"consumed\":3,\"published\":1}\"#,\n      \"3 consumed, 1 published\",\n    );\n    self.end_test(\"run-1\", \"test-1\", 1500);\n\n    // Test 2: failing\n    self.seed_test(\"run-1\", \"test-2\", \"should reject duplicate\", \"ProductSpec\");\n    self.seed_entry_full(\n      \"run-1\",\n      \"test-2\",\n      \"HTTP\",\n      \"POST /products\",\n      \"FAILED\",\n      r#\"{\"name\":\"widget\"}\"#,\n      \"\",\n      \"409 Conflict\",\n      \"201 Created\",\n      \"Expected conflict but got success\",\n      \"\",\n    );\n    self.end_test_failed(\"run-1\", \"test-2\", 800, \"Expected conflict but got success\");\n\n    // End run\n    self.end_run(\"run-1\", 1, 1, 10000);\n  }\n}\n"
  },
  {
    "path": "tools/stove-cli/tests/mcp_e2e.rs",
    "content": "//! End-to-end tests for the Stove MCP endpoint.\n\nmod common;\n\nuse common::TestServer;\nuse reqwest::StatusCode;\nuse serde_json::{Value, json};\nuse stove::grpc::service::DashboardEventServiceImpl;\nuse stove::proto;\nuse stove::proto::dashboard_event_service_server::DashboardEventService;\nuse tonic::Request;\n\nasync fn mcp_call(server: &TestServer, method: &str, params: Value) -> Value {\n  server\n    .client\n    .post(server.mcp_url())\n    .json(&json!({\n      \"jsonrpc\": \"2.0\",\n      \"id\": 1,\n      \"method\": method,\n      \"params\": params,\n    }))\n    .send()\n    .await\n    .expect(\"MCP request should succeed\")\n    .json::<Value>()\n    .await\n    .expect(\"MCP response should be valid JSON\")\n}\n\nasync fn mcp_tool(server: &TestServer, name: &str, arguments: Value) -> Value {\n  mcp_call(\n    server,\n    \"tools/call\",\n    json!({\n      \"name\": name,\n      \"arguments\": arguments,\n    }),\n  )\n  .await\n}\n\n#[tokio::test]\nasync fn mcp_lists_tools_and_initializes() {\n  let server = TestServer::start().await;\n\n  let initialized = mcp_call(\n    &server,\n    \"initialize\",\n    json!({\n      \"protocolVersion\": \"2025-06-18\",\n      \"capabilities\": {},\n      \"clientInfo\": { \"name\": \"test\", \"version\": \"1\" }\n    }),\n  )\n  .await;\n  let tools = mcp_call(&server, \"tools/list\", json!({})).await;\n\n  assert_eq!(initialized[\"result\"][\"serverInfo\"][\"name\"], \"stove\");\n  assert_eq!(tools[\"result\"][\"tools\"][0][\"name\"], \"stove_apps\");\n  assert!(\n    tools[\"result\"][\"tools\"]\n      .as_array()\n      .unwrap()\n      .iter()\n      .any(|tool| tool[\"name\"] == \"stove_failure_detail\")\n  );\n}\n\n#[tokio::test]\nasync fn failures_are_grouped_by_app_and_run_with_exact_detail_calls() {\n  let server = TestServer::start().await;\n  seed_multi_app_failures(&server);\n\n  let response = mcp_tool(&server, \"stove_failures\", json!({ \"limit\": 10 })).await;\n  let groups = response[\"result\"][\"structuredContent\"][\"groups\"]\n    .as_array()\n    .unwrap();\n\n  assert_eq!(groups.len(), 3);\n  assert_eq!(groups[0][\"app_name\"], \"checkout-api\");\n  assert_eq!(groups[0][\"failures\"][0][\"run_id\"], \"run-checkout-2\");\n  assert_eq!(\n    groups[0][\"failures\"][0][\"detail_tool_call\"][\"arguments\"][\"test_id\"],\n    \"duplicate-name\"\n  );\n  assert_eq!(groups[1][\"app_name\"], \"catalog-api\");\n  assert_eq!(groups[2][\"app_name\"], \"checkout-api\");\n}\n\n#[tokio::test]\nasync fn failure_detail_includes_timeline_trace_and_snapshot_summaries() {\n  let server = TestServer::start().await;\n  server.seed_run_at(\n    \"run-1\",\n    \"checkout-api\",\n    \"2024-06-01T10:00:00Z\",\n    &[\"HTTP\", \"Kafka\"],\n  );\n  server.seed_test(\"run-1\", \"test-1\", \"declines expired card\", \"CheckoutSpec\");\n  server.seed_entry_full(\n    \"run-1\",\n    \"test-1\",\n    \"HTTP\",\n    \"POST /checkout\",\n    \"PASSED\",\n    r#\"{\"card\":\"expired\",\"authorization\":\"secret\"}\"#,\n    r#\"{\"status\":\"PENDING\"}\"#,\n    \"\",\n    \"\",\n    \"\",\n    \"trace-1\",\n  );\n  server.seed_entry_full(\n    \"run-1\",\n    \"test-1\",\n    \"Kafka\",\n    \"should publish OrderRejected\",\n    \"FAILED\",\n    r#\"{\"authorization\":\"secret\"}\"#,\n    \"\",\n    \"OrderRejected\",\n    \"nothing\",\n    \"Expected rejection event\",\n    \"trace-1\",\n  );\n  server.seed_span_timed(\n    \"run-1\",\n    \"trace-1\",\n    \"span-1\",\n    \"\",\n    \"POST /checkout\",\n    \"checkout-api\",\n    1_000,\n    2_000,\n    r#\"{\"x-stove-test-id\":\"test-1\"}\"#,\n  );\n  server.seed_span_with_exception(\n    \"run-1\",\n    \"trace-1\",\n    \"span-2\",\n    \"span-1\",\n    \"PaymentClient.authorize\",\n    \"checkout-api\",\n    \"ERROR\",\n    \"PaymentDeclinedException\",\n    \"expired card\",\n    \"stack line 1\\nstack line 2\",\n  );\n  server.seed_snapshot(\n    \"run-1\",\n    \"test-1\",\n    \"Kafka\",\n    r#\"{\"published\":[],\"failed\":[{\"topic\":\"orders\",\"token\":\"secret\"}]}\"#,\n    \"Published: 0\\nFailed: 1\",\n  );\n  server.end_test_failed(\"run-1\", \"test-1\", 800, \"Expected rejection event\");\n  server.end_run(\"run-1\", 0, 1, 900);\n\n  let response = mcp_tool(\n    &server,\n    \"stove_failure_detail\",\n    json!({ \"run_id\": \"run-1\", \"test_id\": \"test-1\" }),\n  )\n  .await;\n  let content = &response[\"result\"][\"structuredContent\"];\n\n  assert_eq!(content[\"app_name\"], \"checkout-api\");\n  assert_eq!(content[\"timeline_summary\"][\"failed_entries\"], 1);\n  assert_eq!(content[\"trace_summary\"][\"trace_status\"], \"correlated\");\n  assert_eq!(content[\"trace_summary\"][\"exception_spans\"], 1);\n  assert_eq!(content[\"snapshot_summaries\"][0][\"system\"], \"Kafka\");\n  assert_eq!(\n    content[\"failed_entries\"][0][\"input\"][\"authorization\"],\n    \"[REDACTED]\"\n  );\n}\n\n#[tokio::test]\nasync fn mcp_handles_no_failures_and_caps_oversized_detail() {\n  let server = TestServer::start().await;\n\n  server.seed_run(\"run-pass\", \"checkout-api\");\n  server.seed_test(\"run-pass\", \"test-pass\", \"happy path\", \"CheckoutSpec\");\n  server.end_test(\"run-pass\", \"test-pass\", 100);\n  server.end_run(\"run-pass\", 1, 0, 100);\n\n  let no_failures = mcp_tool(&server, \"stove_failures\", json!({ \"run_id\": \"run-pass\" })).await;\n  let no_failure_content = &no_failures[\"result\"][\"structuredContent\"];\n  assert_eq!(no_failure_content[\"failure_count\"], 0);\n  assert_eq!(no_failure_content[\"groups\"].as_array().unwrap().len(), 0);\n\n  server.seed_run(\"run-big\", \"checkout-api\");\n  server.seed_test(\"run-big\", \"test-big\", \"large payload\", \"CheckoutSpec\");\n  let oversized = format!(r#\"{{\"payload\":\"{}\"}}\"#, \"x\".repeat(400));\n  server.seed_entry_full(\n    \"run-big\",\n    \"test-big\",\n    \"HTTP\",\n    \"POST /checkout\",\n    \"FAILED\",\n    &oversized,\n    \"\",\n    \"\",\n    \"\",\n    \"large failure\",\n    \"\",\n  );\n  for index in 0..5 {\n    server.seed_snapshot(\n      \"run-big\",\n      \"test-big\",\n      \"Kafka\",\n      r#\"{\"published\":[]}\"#,\n      &format!(\"snapshot {index}\"),\n    );\n  }\n  server.end_test_failed(\"run-big\", \"test-big\", 200, \"large failure\");\n  server.end_run(\"run-big\", 0, 1, 200);\n\n  let detail = mcp_tool(\n    &server,\n    \"stove_failure_detail\",\n    json!({ \"run_id\": \"run-big\", \"test_id\": \"test-big\", \"budget\": \"tiny\", \"max_chars\": 120 }),\n  )\n  .await;\n  let detail_content = &detail[\"result\"][\"structuredContent\"];\n\n  assert!(\n    detail_content[\"failed_entries\"][0][\"input\"][\"payload\"]\n      .as_str()\n      .unwrap()\n      .contains(\"<truncated\")\n  );\n  assert_eq!(\n    detail_content[\"snapshot_summaries\"]\n      .as_array()\n      .unwrap()\n      .len(),\n    3\n  );\n  assert_eq!(detail_content[\"omitted\"][\"snapshots\"], 2);\n}\n\n#[tokio::test]\nasync fn mcp_flushes_pending_ingest_before_reads() {\n  let (server, ingestor) = TestServer::start_with_ingestor().await;\n  let service =\n    DashboardEventServiceImpl::new_with_ingestor(server.repo.clone(), server.sse.clone(), ingestor);\n\n  send_event(&service, run_started(\"run-pending\", \"checkout-api\")).await;\n  send_event(&service, test_started(\"run-pending\", \"test-pending\")).await;\n  send_event(&service, test_ended_failed(\"run-pending\", \"test-pending\")).await;\n\n  let response = mcp_tool(\n    &server,\n    \"stove_failures\",\n    json!({ \"run_id\": \"run-pending\" }),\n  )\n  .await;\n  let groups = response[\"result\"][\"structuredContent\"][\"groups\"]\n    .as_array()\n    .unwrap();\n\n  assert_eq!(groups.len(), 1);\n  assert_eq!(groups[0][\"failures\"][0][\"test_id\"], \"test-pending\");\n  assert_eq!(groups[0][\"run_status\"], \"RUNNING\");\n}\n\n#[tokio::test]\nasync fn mcp_rejects_non_local_host_headers() {\n  let server = TestServer::start().await;\n\n  let response = server\n    .client\n    .post(server.mcp_url())\n    .header(reqwest::header::HOST, \"example.com\")\n    .json(&json!({\n      \"jsonrpc\": \"2.0\",\n      \"id\": 1,\n      \"method\": \"tools/list\"\n    }))\n    .send()\n    .await\n    .expect(\"request should complete\");\n\n  assert_eq!(response.status(), StatusCode::FORBIDDEN);\n}\n\nfn seed_multi_app_failures(server: &TestServer) {\n  server.seed_run_at(\n    \"run-checkout-1\",\n    \"checkout-api\",\n    \"2024-06-01T10:00:00Z\",\n    &[\"HTTP\"],\n  );\n  server.seed_test(\n    \"run-checkout-1\",\n    \"duplicate-name\",\n    \"same display name\",\n    \"CheckoutSpec\",\n  );\n  server.end_test_failed(\n    \"run-checkout-1\",\n    \"duplicate-name\",\n    100,\n    \"first checkout failure\",\n  );\n  server.end_run(\"run-checkout-1\", 0, 1, 100);\n\n  server.seed_run_at(\n    \"run-catalog-1\",\n    \"catalog-api\",\n    \"2024-06-01T10:01:00Z\",\n    &[\"HTTP\"],\n  );\n  server.seed_test(\n    \"run-catalog-1\",\n    \"duplicate-name\",\n    \"same display name\",\n    \"CatalogSpec\",\n  );\n  server.end_test_failed(\"run-catalog-1\", \"duplicate-name\", 100, \"catalog failure\");\n  server.end_run(\"run-catalog-1\", 0, 1, 100);\n\n  server.seed_run_at(\n    \"run-checkout-2\",\n    \"checkout-api\",\n    \"2024-06-01T10:02:00Z\",\n    &[\"Kafka\"],\n  );\n  server.seed_test(\n    \"run-checkout-2\",\n    \"duplicate-name\",\n    \"same display name\",\n    \"CheckoutSpec\",\n  );\n  server.end_test_failed(\n    \"run-checkout-2\",\n    \"duplicate-name\",\n    100,\n    \"latest checkout failure\",\n  );\n  server.end_run(\"run-checkout-2\", 0, 1, 100);\n}\n\nasync fn send_event(service: &DashboardEventServiceImpl, event: proto::DashboardEvent) {\n  DashboardEventService::send_event(service, Request::new(event))\n    .await\n    .expect(\"event should be accepted\");\n}\n\nfn timestamp() -> Option<prost_types::Timestamp> {\n  Some(prost_types::Timestamp {\n    seconds: 1_704_067_200,\n    nanos: 0,\n  })\n}\n\nfn run_started(run_id: &str, app_name: &str) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::RunStarted(\n      proto::RunStartedEvent {\n        timestamp: timestamp(),\n        app_name: app_name.to_string(),\n        systems: vec![\"HTTP\".to_string()],\n        stove_version: \"0.23.2\".to_string(),\n      },\n    )),\n  }\n}\n\nfn test_started(run_id: &str, test_id: &str) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::TestStarted(\n      proto::TestStartedEvent {\n        test_id: test_id.to_string(),\n        test_name: \"pending failure\".to_string(),\n        spec_name: \"PendingSpec\".to_string(),\n        timestamp: timestamp(),\n        test_path: vec![\"pending\".to_string()],\n      },\n    )),\n  }\n}\n\nfn test_ended_failed(run_id: &str, test_id: &str) -> proto::DashboardEvent {\n  proto::DashboardEvent {\n    run_id: run_id.to_string(),\n    event: Some(proto::dashboard_event::Event::TestEnded(\n      proto::TestEndedEvent {\n        test_id: test_id.to_string(),\n        status: \"FAILED\".to_string(),\n        duration_ms: 42,\n        error: \"pending failure\".to_string(),\n        timestamp: timestamp(),\n      },\n    )),\n  }\n}\n"
  }
]