4.7 KiB
Design notes
Image pinning enforcement
The verifier (scripts/verify-image-pinning.ts) walks every docker-compose.*.yml, every plugin's image-config TS source, and every reference inside testcontainers config helpers. It uses a strict regex ^[a-z0-9./-]+:[A-Za-z0-9._-]+@sha256:[a-f0-9]{64}$ and fails on any reference that does not match. Tag-only references are a hard error, not a warning, because partial enforcement is what allowed axllent/mailpit:latest to ship in the first place.
Digest refresh follows a dependabot-style monthly cadence: a scheduled job opens a PR with the updated digests pulled from docker manifest inspect. CVE advisories from upstream (Mailpit, Redis, Postgres, SurrealDB, Garage, Typesense, Verdaccio) trigger immediate refresh outside the monthly window. The reviewer SHALL verify the upstream changelog before merge.
Resource limits — compose vs testcontainers
Compose stacks declare limits under deploy.resources.limits. testcontainers does not honour the compose v3 schema, so plugin fixture configs accept { memoryMiB?: number; cpuCount?: number } and translate to HostConfig.Memory (bytes) and HostConfig.NanoCpus (cpu × 1e9). Defaults are identical between the two paths so behaviour is consistent regardless of which entry point CI uses.
Database services get the larger budget (2.0 cpu, 2Gi) because a parallel test sweep can drive Surreal/Postgres to its working-set limit. Garage and MinIO sit in the middle (1.5 cpu, 1.5Gi) because both have a memory-resident metadata index that grows with bucket size during long-running E2E runs.
CI matrix shape
Three axes — runtime, OS, arch — multiplied conservatively because Playwright on Windows triples job time and arm64 is unsupported by some images we depend on. The plugin-side archSupport declaration is required so the matrix-generator script can prune unsupported cells before scheduling. bun-canary is allowed to fail-open because canary breakage is an upstream signal, not a merge blocker — but the warning is loud and surfaces in PR checks.
The Node 22 LTS parity job runs only the universal-purity suite (no integration, no E2E). It catches accidental Bun-only API leakage in browser-runnable packages (testing, testing-dom, crypto-utils, etc.) which is otherwise invisible until a downstream consumer hits it.
Airgap rebuild
The nightly airgap-rebuild job exists because enterprise consumers regularly run in environments without internet egress at build/test time. The job pre-warms a Bun cache via a normal bun install in a separate step, then runs bun install --offline, bunx turbo build, and a network-isolated test phase. Failure indicates a missing offline-cache entry — typically a transitive dep that pulled at runtime — and blocks the next release. We test the smallest changed-since-last-week filter (...[HEAD~7]) to keep the job under the 30-minute wall-clock budget.
Verdaccio fixture
Verdaccio is the only "test-the-test-of-the-test" case where a @suaveplan/* package's primary use case (publishing) requires a fixture. We accept the meta-circularity: testing-integration-verdaccio does not depend on any @suaveplan/* package by Layer-0 rule, but bun scripts/publish.ts (a repo-level script, not a package) is exercised against it in CI.
Memory storage is the default for speed; tmpfs is available for tests that need filesystem persistence semantics. Users and packages config follow Verdaccio's native YAML shape, translated from the TS config object at fixture startup.
Network sandboxing
The default block is implemented via page.route('**/*') registered before each test. The allow-list is checked by URL origin (not full path) using the URL constructor; the auto-allow set is localhost, 127.0.0.1, and the configured testbed origin from env. The test.use({ allowExternalNetwork: true }) escape hatch logs a warning at suite start so the reviewer can challenge it during code review.
Leak detection
After every TestEnvironment.teardown() the runner snapshots active handles. On Bun we use Bun.peek and the runtime's internal handle table; on Node fallback we use process._getActiveHandles(). The diff between pre-setup and post-teardown is the leak set. Categories: HANDLE (file descriptors, dns lookups), TIMER (setTimeout/setInterval), SOCKET (TCP/UDP). The error code is granular so triage can route to the responsible fixture.
Open questions
- Whether to extend the airgap test to E2E (Playwright requires browser binaries that may not be in the offline cache by default).
- Whether
quarantine.jsonshould live at repo root or underopenspec/. Preference is repo root so CI tooling does not need OpenSpec parsing.