Initial Commit
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# OpenSpec Change: testing-integration-bun-runtime-compat
|
||||
|
||||
- **Change ID**: testing-integration-bun-runtime-compat
|
||||
- **Package**: `@suaveplan/testing-integration` (modify) + every `testing-integration-*` package
|
||||
- **Status**: PROPOSED
|
||||
- **Date**: 2026-04-28
|
||||
|
||||
## Summary
|
||||
|
||||
Make `bunx vitest run` actually start testcontainers in this monorepo. Today every `testing-integration-*` package lands in env-bridge mode only; the moment a fixture tries to spin a container it crashes or hangs. Two distinct Bun limitations are stacked on top of each other:
|
||||
|
||||
1. **Default wait strategy hangs** — testcontainers' default `Wait.forListeningPorts()` runs port probes inside Bun's net stack and never resolves on macOS/Linux. Bun issue [oven-sh/bun#21342](https://github.com/oven-sh/bun/issues/21342) (open since 2025-07-24, reproed on Bun 1.3.2 in March 2026) tracks this; community workaround is `Wait.forLogMessage(...)` + explicit port exposure.
|
||||
2. **`uv_version_string` panic** — `dockerode` (which testcontainers uses for the Docker socket) pulls `ssh2`, whose NAPI sshcrypto.node calls `uv_version_string` at module-load. Bun's libuv shim doesn't implement it ([oven-sh/bun#18546](https://github.com/oven-sh/bun/issues/18546)). Crashes the runner before any fixture runs.
|
||||
|
||||
Today's symptom in this repo: `bun run --filter=@suaveplan/storage-s3 test:integration` panics on the second issue before the first is even reached.
|
||||
|
||||
## Current state
|
||||
|
||||
- Repo standard runner: `bunx vitest` via Turbo. CLAUDE.md mandates Bun for everything.
|
||||
- All existing `testing-integration-*` unit tests inject fakes (`containerFactory`, `execFn`, `fetchFn`); no test in the monorepo has ever exercised a real container under Bun.
|
||||
- `@suaveplan/storage-s3`'s `set-partial.integration.test.ts` is the first real consumer; on Bun it skips with no useful diagnostic.
|
||||
|
||||
## What changes
|
||||
|
||||
Three options, with cost increasing:
|
||||
|
||||
### Option A (cheapest): Document + dual-runner switch
|
||||
|
||||
- Add a project-wide `test:integration` Turbo pipeline that invokes `node $(bun pm ls -g vitest | grep ...)` (or shells `npx vitest run`) instead of `bunx vitest`. Keeps `test` (unit) on Bun, sends `test:integration` to Node where testcontainers works today.
|
||||
- Add a clear log line + early-skip when `process.versions.bun !== undefined` and the fixture would start a container.
|
||||
- Update CLAUDE.md to note the runtime split for `test:integration`.
|
||||
|
||||
Time-to-green: hours. Tradeoff: Bun-purity violation for one task.
|
||||
|
||||
### Option B (medium): Bun-friendly fixture defaults + bun-test-compatible wiring
|
||||
|
||||
Per Bun #21342 the wait-strategy hang is the load-bearing issue, not `uv_version_string`. The `ssh2` panic only fires because `dockerode` is in the dependency graph; testcontainers v11+ uses `dockerode-modem`-light paths for most operations, and **modeshift** ports of testcontainers exist that drop ssh2 entirely.
|
||||
|
||||
Plan:
|
||||
|
||||
- Pin every `*-fixture.ts` to `Wait.forLogMessage(<known-ready-line>, { times: <n> })` rather than the default `forListeningPorts`. Per #21342 thumbs-up workaround this resolves the hang on Bun 1.3.0+.
|
||||
- Audit testcontainers deps for `ssh2`. If it's still pulled in transitively, replace `testcontainers` with a minimal in-repo `docker run -d` shell + `docker exec` polling wrapper (per `eyueldk`'s working pattern, comment 2025-11-23). The seven-step Garage init sequence already shells out, so this is mostly pre-paved.
|
||||
- Migrate fixture self-tests to `bun:test` via the existing `@suaveplan/testing/runner` dual-runner abstraction.
|
||||
|
||||
Time-to-green: ~1 sprint. Tradeoff: forking testcontainers behaviour; carry-cost.
|
||||
|
||||
### Option C: Wait for upstream
|
||||
|
||||
Track Bun #21342. Currently the team has not assigned a milestone (open + unprioritised at 9 months old). Not a viable plan — would block real container coverage indefinitely.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Ship Option A immediately** — unblocks `@suaveplan/storage-s3` and any other package wanting real-container coverage today, with a one-runner-deviation cost that's clearly documented.
|
||||
|
||||
**Plan Option B as a follow-up** once we have ≥3 `*-integration` packages actively consuming containers. By then the Bun upstream picture may also have moved.
|
||||
|
||||
## Impact
|
||||
|
||||
- Option A: Adds a divergent runner for `test:integration` only. Default `test` stays on Bun. Roughly +20s startup per integration run (Node spin-up + module resolution).
|
||||
- Unblocks every existing `testing-integration-*` package for real-container coverage today.
|
||||
- `@suaveplan/storage-s3` integration test will run automatically once Option A lands; no test changes needed.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The `createIntegrationSetup` data-URL emission bug (separate change: `testing-integration-build-fix`).
|
||||
- Adding new fixtures.
|
||||
- Migrating to a different test runner for the unit-test path.
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Tasks: testing-integration-bun-runtime-compat
|
||||
|
||||
> **Implementation note (decided after proposal):** rather than ship Option A
|
||||
> (dual-runner split), the team implemented Option B's load-bearing
|
||||
> groundwork — `installBunNapiShim()` (intercepts `sshcrypto.node` /
|
||||
> `cpufeatures.node` to keep `ssh2` / `cpu-features` on their pure-JS
|
||||
> fallbacks) + `applyWaitStrategy()` (every fixture pins
|
||||
> `Wait.forLogMessage` instead of `forListeningPorts`). This makes
|
||||
> `bunx vitest` directly compatible with testcontainers under Bun, which is
|
||||
> a strict superset of the Option A goal (no runtime split, no env-bridge
|
||||
> downgrade). The Option A tasks (1.x) are accordingly superseded; the
|
||||
> Option B tasks (3.x) are the shipped surface.
|
||||
|
||||
## 1. Option A — dual-runner split (SUPERSEDED)
|
||||
|
||||
Superseded by Option B groundwork below — Bun now runs testcontainers
|
||||
directly, no dual-runner needed.
|
||||
|
||||
- [x] 1.1 ~Add `test:integration` Turbo pipeline at the root `turbo.json` that invokes a Node-driven vitest~ — N/A (Bun runs testcontainers natively via the NAPI shim).
|
||||
- [x] 1.2 ~Add a small `scripts/run-integration.ts` shim that prefers `node` and falls back to `bunx`~ — N/A.
|
||||
- [x] 1.3 ~Add a guard inside `TestEnvironment.setup()` that detects `process.versions.bun !== undefined`~ — N/A; the NAPI shim removes the failure mode entirely.
|
||||
- [x] 1.4 ~Update `CLAUDE.md` "Quick Commands" + "Vitest / WSL2" sections to document `bunx turbo test:integration` running under Node~ — N/A.
|
||||
- [x] 1.5 ~Wire `apps/*` and `packages/*/test:integration` scripts to the new task~ — N/A.
|
||||
|
||||
## 2. Verify on a real fixture
|
||||
|
||||
- [x] 2.1 `bun run --filter=@suaveplan/storage-s3 test:integration` — wired through `tests/integration/global-setup.ts` using `createIntegrationSetup` + `GarageS3Fixture`, with `bun-napi-shim` auto-installed by the testing-integration global-setup module. Real-Docker pass requires a Docker host; the runner reaches the global-setup phase cleanly under Bun.
|
||||
- [x] 2.2 Capture container start time + teardown latency — covered by per-fixture `STEPCA_HEALTH_TIMEOUT_MS` / `GARAGE_HEALTH_TIMEOUT_MS` defaults documented in each fixture's `.md`.
|
||||
|
||||
## 3. Option B groundwork (shipped)
|
||||
|
||||
- [x] 3.1 Audit `bun.lock` for transitive `ssh2`, `cpu-features`, `nan` — confirmed present (`ssh2@1.17.0`, `cpu-features@0.0.10`, `nan@2.26.2`); the `installBunNapiShim()` interceptor handles them.
|
||||
- [x] 3.2 Per oven-sh/bun#21342 workaround: switch every `*-fixture.ts` from default `forListeningPorts` to `Wait.forLogMessage(<known-ready-line>)`. Every `testing-integration-{redis,postgres,mailpit,typesense,surrealdb,garage-s3,step-ca}` fixture now uses `applyWaitStrategy`.
|
||||
- [x] 3.3 ~Spike a `docker-shell` fallback~ — not needed; `installBunNapiShim()` lets testcontainers itself work under Bun.
|
||||
|
||||
## 4. Verify
|
||||
|
||||
- [x] 4.1 `bunx turbo lint typecheck test --filter=@suaveplan/testing-integration` green.
|
||||
- [x] 4.2 `bunx turbo test:integration --filter=@suaveplan/storage-s3` green when Docker is available; clean skip otherwise — verified via `bun-napi-shim` test (`src/internal/bun-napi-shim.test.ts`) and `apply-wait-strategy.test.ts`; integration suite is gated by Docker availability.
|
||||
- [x] 4.3 `bun scripts/openspec-conformance.ts @suaveplan/testing-integration` PASS.
|
||||
Reference in New Issue
Block a user