Files
2026-05-27 23:29:53 +01:00

7.4 KiB
Raw Permalink Blame History

Capability Spec Delta: testing-integration (testing-integration-bun-runtime-compat)

Package: @suaveplan/testing-integration and every @suaveplan/testing-integration-* plugin package Status: PROPOSED Approach: Option B — Bun-friendly fixture defaults; testcontainers replaced with a docker run/docker exec wrapper if its dep tree pulls ssh2. (CLAUDE.md mandates Bun for everything; Option A's dual-runner split is rejected as a "Bun-purity violation" the user does not accept.)

ADDED Requirements

Requirement: Container fixtures MUST start under bunx vitest run

A consumer running bunx vitest run --config vitest.integration.config.ts against a fixture in container mode (no env-bridge variables set) MUST observe the fixture successfully start a real Docker container, return a TConfig, and tear it down on teardown. The runner MUST NOT panic, hang, or short-circuit to env-bridge mode in this scenario.

This requirement guards against two stacked Bun limitations:

  1. uv_version_string panic — dockerode (which testcontainers uses for the Docker socket) transitively pulls ssh2, whose NAPI sshcrypto.node calls uv_version_string at module-load. Bun's libuv shim does not implement this symbol (Bun issue oven-sh/bun#18546). Loading the module crashes the runner before any fixture's start() is called.
  2. Default wait strategy hang — testcontainers's default Wait.forListeningPorts() runs port probes inside Bun's net stack and never resolves on macOS/Linux (Bun issue oven-sh/bun#21342, open since 2025-07-24). The container starts but the wait promise never settles.

Scenario: bunx vitest starts a real container

  • WHEN a consumer runs bunx vitest run against an integration test that consumes RedisFixture (or any other *-Fixture in container mode) on a host where Docker is reachable
  • THEN the runner reaches the test body and observes the fixture's returned TConfig pointing at a real container; tear-down completes cleanly

Scenario: no panic at module load

  • WHEN bunx vitest run loads a testing-integration-* package or its transitive deps
  • THEN no uv_version_string-related panic, NAPI load failure, or Bun-runtime crash occurs

Verified by: bun run --filter=@suaveplan/storage-s3 test:integration running 4/4 of the existing set-partial.integration.test.ts cases against a real Garage container under Bun; equivalent runs for at least one other fixture (e.g. surrealdb-integration-tests).


Requirement: Default wait strategy is Bun-compatible

Every *-Fixture.ts in the @suaveplan/testing-integration-* family SHALL default to Wait.forLogMessage(<known-ready-line>, { times: <n> }) (or an equivalent log-based readiness probe) rather than Wait.forListeningPorts() when running in container mode. Each fixture SHALL declare its known-ready log line in source as a named constant whose value is documented in the fixture's .md doc.

The known-ready line is the first stable log line a healthy container emits at the end of its startup sequence. If a service emits multiple such lines per restart, times: <n> MUST match the count for a single startup.

Scenario: fixture readiness probe uses log-message

  • WHEN a *-Fixture is constructed in container mode
  • THEN the testcontainers builder used internally has withWaitStrategy(Wait.forLogMessage(...)) set; the default forListeningPorts strategy is NOT used

Scenario: known-ready line is documented

  • WHEN the fixture's <service>-fixture.md is read
  • THEN it contains a "Readiness probe" section naming the exact log line and the expected occurrence count

Verified by: the fixture's *-fixture.test.ts injecting a fake containerFactory and asserting the wait strategy applied; the fixture's co-located .md documentation update; the runtime requirement above (containers actually start under Bun).


Requirement: Dependency tree MUST NOT load ssh2 at module-init

The dependency closure of @suaveplan/testing-integration and every @suaveplan/testing-integration-* plugin SHALL NOT include any package that calls uv_version_string, loads sshcrypto.node, or otherwise crashes Bun at module-load.

If testcontainers continues to drag in ssh2 transitively (via dockerode or otherwise) and a published version free of that dependency is not available, the implementation SHALL replace testcontainers with an in-repo docker run -d / docker exec ... <healthcheck> / docker stop wrapper that talks directly to the Docker daemon over the socket without going through dockerode. Garage S3's existing 7-step init sequence already shells out, so the precedent for this pattern exists in-repo.

Scenario: dep audit confirms ssh2 absence

  • WHEN bun.lock is grepped for ssh2, cpu-features, nan, sshcrypto
  • THEN none of these packages are present in the closure of @suaveplan/testing-integration*, OR they are present but proven not to be loaded at module-init under Bun (e.g. lazy-loaded behind a feature flag the fixture does not enable)

Scenario: docker-shell fallback exists if needed

  • WHEN testcontainers cannot be made ssh2-free in time for this change
  • THEN the package ships an internal DockerShellRunner (or equivalent) that implements the same start()/stop()/exec() surface using child_process.spawn("docker", [...]) and the fixtures consume it transparently

Verified by: bun.lock audit (committed as part of this change); under-Bun smoke run of any container-mode fixture (no uv_version_string panic in the runner output).


Requirement: Fixture self-tests run under Bun

Every fixture's *-fixture.test.ts SHALL run cleanly under bunx vitest run (the project's mandated runner per CLAUDE.md). Tests that exercise the testcontainers integration path SHALL inject a fake containerFactory so the test does not require a Docker daemon; tests that exercise the docker-shell path SHALL inject a fake execFn. No fixture self-test may require running on Node.

Scenario: fixture unit tests pass under Bun

  • WHEN bunx turbo test:coverage --filter=@suaveplan/testing-integration-* runs
  • THEN all fixture self-tests pass at 100% Istanbul coverage with no Bun-specific failures

Verified by: the existing bunx turbo test:coverage gate, which already exercises every fixture; the regression check (testing-integration-vite-entry-fix task 3.1–3.6) extended to confirm Bun-runtime parity.


Requirement: No dual-runner split

This change rejects Option A's dual-runner pattern (running test:integration under Node while test stays on Bun). bunx turbo test:integration MUST run under Bun for all consumers, end-to-end.

The reasoning: CLAUDE.md mandates Bun for everything in this monorepo; an integration-test runtime split is a permanent maintenance tax that fragments the toolchain and contradicts the project's stated runtime stance. The proper fix is to make the fixtures work under Bun, not to route around the runner.

Scenario: integration scripts use bunx, not node

  • WHEN a package.json scripts.test:integration entry is added in any consumer
  • THEN the script SHALL invoke vitest run via bunx (or omit the runner prefix entirely so Bun's auto-resolution picks up vitest); it MUST NOT shell out to node ./node_modules/vitest/dist/cli-wrapper.js or any equivalent Node-only invocation

Verified by: code review of every test:integration script in the monorepo at archive time.