8.1 KiB
ADDED Requirements
Requirement: Image pinning by tag and digest
Every Docker image referenced by any testing-integration-* plugin, by docker-compose.{dev,test,testbed}.yml, or by any testcontainers config helper SHALL be pinned by both tag AND sha256 digest in the canonical form image: <registry>/<name>:<tag>@sha256:<64-hex>. Tag-only pinning is a defect. The pin SHALL be verified by scripts/verify-image-pinning.ts, which fails CI on any reference that does not match ^[a-z0-9./-]+:[A-Za-z0-9._-]+@sha256:[a-f0-9]{64}$. Digest refresh follows a monthly dependabot-style cadence with reviewer approval; security CVE advisories trigger immediate refresh outside the monthly window.
Scenario: Tag-only image fails verification
- WHEN a plugin or compose file declares
image: foo/bar:v1.2.3without a@sha256:digest - THEN
scripts/verify-image-pinning.tsexits non-zero AND CI fails the lint stage
Scenario: Mailpit is pinned to v1.x with a digest
- WHEN the Mailpit plugin or testbed compose stack references the Mailpit image
- THEN the reference is
axllent/mailpit:v1.<minor>@sha256:<digest>(never:latest)
Scenario: CVE refresh bypasses monthly cadence
- WHEN an upstream CVE advisory is filed against a pinned image
- THEN a digest-bump PR SHALL be opened immediately AND the monthly cadence does not block it
Requirement: Container resource limits
Every service in docker-compose.{dev,test,testbed}.yml SHALL declare deploy.resources.limits.{cpus,memory}. Defaults: 1.0 cpu / 1Gi for application services, 2.0 cpu / 2Gi for database services (Postgres, SurrealDB, Redis, Typesense), 1.5 cpu / 1.5Gi for object stores (Garage, MinIO). testcontainers-managed containers SHALL set HostConfig.Memory (bytes) and HostConfig.NanoCpus (cpu × 1e9) via the plugin config shape { memoryMiB?: number; cpuCount?: number }, with the same defaults applied when fields are omitted.
Scenario: Compose service lacking limits fails verification
- WHEN a service in any tracked
docker-compose.*.ymldoes not declaredeploy.resources.limits.cpusANDdeploy.resources.limits.memory - THEN the compose-lint step fails
Scenario: testcontainer translates plugin budget to docker HostConfig
- WHEN a plugin starts with
{ memoryMiB: 1024, cpuCount: 1.0 } - THEN the resulting docker
HostConfig.Memory === 1073741824ANDHostConfig.NanoCpus === 1_000_000_000
Requirement: CI matrix on Bun, OS, and architecture
CI SHALL execute on a matrix of bun-latest × bun-canary, ubuntu-22.04 × ubuntu-24.04 × macos-14 × windows-latest (Playwright-only on Windows), and amd64 × arm64 on Linux. Each testing-integration-* plugin SHALL declare archSupport: ('amd64'|'arm64')[] and the matrix-generator SHALL skip unsupported cells. The canonical Bun version is pinned via root package.json engines.bun; the bun-canary job is allowed to fail-open with a warning annotation. A separate Node 22 LTS job SHALL run the universal-purity suite for browser-runnable packages (testing, testing-dom); failures here block merge.
Scenario: Plugin without arm64 support skips arm64 cell
- WHEN a plugin declares
archSupport: ['amd64'] - THEN the matrix-generator emits no
arm64job for that plugin AND CI does not flag the absence as a failure
Scenario: Bun canary fails
- WHEN the
bun-canaryjob exits non-zero - THEN the PR check surfaces a warning annotation AND merge is not blocked
Scenario: Node parity fails
- WHEN the Node 22 LTS parity job for browser-runnable packages exits non-zero
- THEN merge is blocked
Requirement: Airgap rebuild gate
A nightly workflow airgap-rebuild SHALL: pre-warm the Bun cache via a normal install, run bun install --offline from that cache, run bunx turbo build, then run bunx turbo test --filter=...[HEAD~7] with all required images pre-pulled and the test phase executed inside --network=none. Failure indicates a missing offline-cache entry and SHALL block the next release.
Scenario: Offline install missing dep fails the job
- WHEN any package pulls a dependency that is not present in the pre-warmed cache during
bun install --offline - THEN the airgap job fails AND release is blocked until the offline cache is fixed
Scenario: Test phase isolated from network
- WHEN the airgap job enters the test phase
- THEN the Docker network for the runner is
--network=noneAND any test that attempts external network egress fails fast
Requirement: Secret management policy
CI secrets SHALL be sourced from GitHub OIDC plus a Vault or SOPS bridge. The variables GARAGE_S3_SECRET_ACCESS_KEY, OIDC_CLIENT_SECRET, NPM_TOKEN, and PACT_BROKER_TOKEN MUST be referenced via the bridge — never inline in workflow YAML. .env.example SHALL document each secret's CI source and its local-dev source. scripts/scan-secrets.ts SHALL fail CI on any inline secret pattern using @suaveplan/security-codes detectors.
Scenario: Inline secret in workflow YAML fails CI
- WHEN a workflow declares
env: NPM_TOKEN: <literal-token>(any pattern detected by@suaveplan/security-codes) - THEN
scripts/scan-secrets.tsexits non-zero AND CI fails
Scenario: Bridge resolution
- WHEN a workflow references
${{ secrets.NPM_TOKEN }}via the OIDC + Vault/SOPS bridge step - THEN the secret is fetched at job start AND no inline value appears in the YAML source
Requirement: Port collision policy
Static port ranges (dev 8001-8902, test 7004-7011, testbed 3200) are reserved for compose stacks. testcontainers-managed integration tests SHALL use random ephemeral ports (the testcontainers default). docker/scripts/preflight.ts SHALL fail loudly if any reserved port is already bound, with a remediation hint pointing the user at either killing the conflicting process or remapping via PORTS_OFFSET.
Scenario: Bound reserved port aborts startup
- WHEN
docker compose -f docker-compose.dev.yml upruns and port 8001 is already bound by another process - THEN preflight exits non-zero AND prints the offending PID + a
PORTS_OFFSETremediation hint
Scenario: PORTS_OFFSET shift
- WHEN
PORTS_OFFSET=100is set in the environment - THEN every reserved port is shifted by 100 AND preflight passes if the shifted range is free
Requirement: OOM and suite wall-clock budgets
Per-spec memory cap SHALL default to 1Gi via the shared createTestConfig()'s pool.forks.maxMemory; packages MAY override per-package. Per-bunx turbo test invocation wall-clock ceiling SHALL be 30 minutes; a soft warning SHALL be emitted at 20 minutes; the run SHALL be aborted at 30 minutes. No automatic retries on timeout — timeout indicates a pathological condition that must be diagnosed.
Scenario: Single-spec OOM aborts that fork only
- WHEN a single test spec exceeds 1Gi resident
- THEN the forked vitest worker is killed with a
TESTING.MEMORY.LIMIT_EXCEEDEDerror AND sibling specs continue
Scenario: Wall-clock 30-minute abort
- WHEN
bunx turbo testexceeds 30 minutes wall-clock - THEN the run is aborted with a non-zero exit AND no automatic retry is attempted
Requirement: Leak detection on fixture teardown
After every TestEnvironment.teardown() the runner SHALL assert no leaked timers, sockets, or file descriptors via Bun.peek / runtime handle table on Bun or process._getActiveHandles() on Node fallback. Leaks SHALL fail the suite with a categorised error: TESTING.LEAK.HANDLE, TESTING.LEAK.TIMER, or TESTING.LEAK.SOCKET, each carrying { count, sample } context.
Scenario: Unreleased timer leaks
- WHEN a fixture sets
setIntervaland does not clear it before teardown - THEN teardown throws
TESTING.LEAK.TIMERANDcontext.count >= 1
Scenario: Unclosed socket leaks
- WHEN a fixture opens a TCP socket and does not close it before teardown
- THEN teardown throws
TESTING.LEAK.SOCKETANDcontext.samplecontains the offending socket descriptor
Scenario: Clean teardown passes
- WHEN all timers are cleared and all sockets are closed before teardown returns
- THEN the leak assertion passes silently