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

97 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.