Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
## Selection rationale — why these five first
The 2026-04-25 audit (`/mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md`, §1) lists ten missing fixtures. This wave picks the five with **direct existing-package consumers in the genesis tree** (per the `CLAUDE.md` package list) so each new fixture immediately unblocks integration testing for shipped code:
| Plugin | Direct consumer on genesis | Audit severity |
|---|---|---|
| `testing-integration-oidc` | `@suaveplan/oauth-client` (Authorization Code + PKCE, Client Credentials, OIDC discovery) | Critical |
| `testing-integration-mongodb` | data-tier parity with `@suaveplan/db-sqlite`; future `db-mongodb` | Critical |
| `testing-integration-mysql` | data-tier parity with `@suaveplan/db-sqlite`; future `db-mysql` | Critical |
| `testing-integration-minio` | AWS-S3 surface parity vs `testing-integration-garage-s3` quirks | High |
| `testing-integration-nats` | `@suaveplan/events`; future stream-processing | High |
## Deferred (next waves)
- **OpenTelemetry collector** — required to verify exported spans/metrics end-to-end across every package. Held back because it needs a co-designed assertion helper (`assertSpanExported(name, attributes)`) that does not yet exist; spec lands with the OTel-collector change.
- **Elasticsearch / OpenSearch** — Typesense covers our search surface today; no shipped consumer.
- **Vault** — no shipped secrets consumer; `@suaveplan/env` and `@suaveplan/config` cover env/file layers.
- **LocalStack** — overlaps with MinIO + future SQS/SNS work; revisit once an AWS-API consumer ships.
- **Caddy / nginx reverse-proxy** — useful for routing tests once `apps/testbed` formalises a reverse-proxy contract.
- **Kafka / RabbitMQ** — NATS goes first as the lightest broker; Kafka follows when a Kafka-specific consumer is on deck.
## `dependsOn` interaction
Audit §3 flags that `ServiceFixture` lacks declared inter-fixture dependencies. The `testing-integration` core package is expected to gain a `dependsOn?: string[]` field on `ServiceFixture` and topo-sort start order in `TestEnvironment`. That field is specified by `testing-integration` itself — **out of scope** for this change — but every plugin in this wave MUST tolerate its presence:
- `testing-integration-oidc` is the canonical upstream: an app fixture that authenticates against the issuer would declare `dependsOn: ['oidc']`. The OIDC fixture itself declares no dependencies.
- `testing-integration-mongodb` / `mysql` / `minio` / `nats` likewise declare no dependencies; downstream app fixtures depend on them.
Plugins MUST NOT introspect the `dependsOn` field; resolution is `TestEnvironment`'s responsibility.
## Image-pinning policy
Mirroring the existing six-plugin precedent: each plugin pins a **minor tag** (`mongo:7.0`, `mysql:8.4`, `nats:2.10-alpine`, `quay.io/keycloak/keycloak:26.0`) AND records a `# TODO: replace with sha256:<digest>` comment in the constants file. Digest substitution happens during the implementation chunk, not in the spec — the spec only requires that a digest placeholder exists.
## Env-bridge contract (uniform across all five)
When **all** documented env vars for a plugin are present and non-empty, the fixture's `start()` MUST skip container creation entirely and return a `ServiceFixtureHandle` whose config points at the live service. This is the same contract the existing six plugins implement. Each plugin spec below enumerates its env-var set explicitly.
## Injectable fakes — coverage strategy
Each fixture exposes a constructor option object with at least:
- `containerFactory?: () => GenericContainer` — swap in a fake container for unit tests.
- One of: `fetchFn?: typeof fetch` (HTTP-driven seeding — OIDC, MinIO), `execFn?: (cmd: string[]) => Promise<{ stdout, stderr, exitCode }>` (in-container CLI seeding — MongoDB, MySQL, NATS), `tcpPingFn?: (host, port) => Promise<boolean>` (TCP healthchecks — NATS).
This pattern is exactly what `testing-integration-redis` (tcp ping), `testing-integration-postgres` (exec), and `testing-integration-garage-s3` (fetch + exec) already use; consistency lets the same Vitest fake helpers be re-used across plugins.
## Error-code namespacing
Each plugin reserves a top-level prefix under `INTEGRATION.*`: `INTEGRATION.OIDC.*`, `INTEGRATION.MONGODB.*`, `INTEGRATION.MYSQL.*`, `INTEGRATION.MINIO.*`, `INTEGRATION.NATS.*`. Codes match `^INTEGRATION\.[A-Z0-9_]+\.[A-Z_]+$` (same regex the existing plugins use).

View File

@@ -0,0 +1,54 @@
## Why
The genesis monorepo already ships six `@suaveplan/testing-integration-*` plugin packages (SurrealDB, Redis, Postgres, Mailpit, Typesense, Garage S3) implementing the `ServiceFixture<TConfig>` interface from `@suaveplan/testing-integration`. The 2026-04-25 test-suite documentation audit at `/mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md` (§1, "Coverage gaps in service fixtures") flags the existing plugin set as too narrow for the published package list:
- `@suaveplan/oauth-client` ships an Authorization Code + PKCE / Client Credentials / OIDC discovery client, but there is no OIDC issuer fixture, so its integration suite cannot exercise a real flow against a real issuer.
- `@suaveplan/db-sqlite` exists, but no fixture covers MongoDB or MySQL — both are in the standard data-tier surface for downstream apps.
- `@suaveplan/garage-s3` is the only object-store fixture; Garage's quirks (admin-port health, layout assignment, custom init sequence) mask AWS-S3 surface differences that show up against MinIO.
- `@suaveplan/events` plus future stream-processing work has no broker fixture; NATS is the lightest option for a first wave.
Per `CLAUDE.md` rule 5 ("preserve the full public surface") and rule 2 ("complete all implementations — no stubs"), each of these plugins must be specified normatively before code is written, mirroring the existing six-plugin pattern (image policy, env-bridge, healthcheck, config shape, error-code prefix, injectable fakes, plugin-specific scenarios).
## What Changes
This change adds **five new capabilities**, one per plugin package, using the same shape as the existing `testing-integration-*` capabilities:
- `testing-integration-oidc` — Keycloak (preferred) or `oauth2-mock-server` (lighter alt), exercised by `@suaveplan/oauth-client` integration tests.
- `testing-integration-mongodb` — `mongo:7.0`, with optional replica-set initialisation for transactions/change-streams.
- `testing-integration-mysql` — `mysql:8.4`, with charset/collation override and `mysqladmin ping` health.
- `testing-integration-minio` — `minio/minio` with random keys and bucket pre-seeding via the admin API; lives alongside the Garage S3 fixture to expose AWS-S3 surface differences.
- `testing-integration-nats` — `nats:2.10-alpine`, optional JetStream + cluster + auth modes, NATS-CLI seeding via injectable `execFn`.
Each plugin spec mandates:
1. **Pinned image** with minor tag plus a `TODO` placeholder for the sha256 digest (preserves the existing six-plugin policy).
2. **Env-bridge passthrough** — when documented env vars are present and non-empty, no container is started; the fixture returns a config pointing at the live service.
3. **Healthcheck** — a deterministic readiness probe with a documented default timeout.
4. **Config shape** — TypeScript interface plus a sibling `*.schemas.ts` Zod schema reference.
5. **Error-code prefix** — `INTEGRATION.<SERVICE>.*` (matching the `INTEGRATION.SURREALDB.*`, `INTEGRATION.GARAGE_S3.*` precedent).
6. **Injectable fakes** — `containerFactory`, plus per-plugin `fetchFn`, `execFn`, or `tcpPingFn` so unit tests reach 100% Istanbul coverage without Docker.
7. **Plugin-specific scenarios** — at least three behavioural scenarios per fixture, expressed as `WHEN`/`THEN` pairs.
`dependsOn?: string[]` (the wave-1 `ServiceFixture` extension flagged by audit §3) is referenced where relevant, e.g. an OIDC-protected app fixture would `dependsOn: ['oidc']`. The field itself is specified by `testing-integration` (out of scope for this change); plugins MUST tolerate its presence in `TestEnvironmentOptions`.
## Capabilities
### New Capabilities
- `testing-integration-oidc`
- `testing-integration-mongodb`
- `testing-integration-mysql`
- `testing-integration-minio`
- `testing-integration-nats`
### Modified Capabilities
(none — additive; existing plugin specs are untouched)
## Impact
- **Affected code:** five new packages under `packages/testing/testing-integration-<name>/` (implementation in a follow-up plan).
- **Affected consumers:** none yet on genesis. Once shipped, `@suaveplan/oauth-client`, `@suaveplan/events`, future `@suaveplan/db-mongodb` / `db-mysql`, and any S3-API consumer become eligible for real-service integration coverage.
- **Catalogs:** `testcontainers` already lives in `catalog:integration` (added by the original six-plugin wave); no new external deps.
- **Pre-publish gate:** unchanged — `bunx turbo lint typecheck test --filter=@suaveplan/testing-integration-<name>` plus 100% Istanbul coverage per `openspec/AGENTS.md` §7.
- **Deferred (audit §1, not in this change):** Elasticsearch/OpenSearch, Vault, LocalStack, Caddy/nginx reverse-proxy, Kafka, OpenTelemetry collector. Each is a separate future change to keep this proposal scoped.

View File

@@ -0,0 +1,125 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-integration-minio` package's `dependencies` MUST be exactly `@suaveplan/testing-integration` (`workspace:^`) and `testcontainers` (`catalog:integration`). No other `@suaveplan/*` runtime dep is permitted. `devDependencies` MAY include `@suaveplan/testing` for the package's own tests.
#### Scenario: Runtime closure clean
- **WHEN** `bunx turbo ls --filter=@suaveplan/testing-integration-minio...` is run
- **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration`
### Requirement: Coexistence with Garage S3 fixture
The MinIO fixture MUST exist alongside `@suaveplan/testing-integration-garage-s3` rather than replacing it. The two fixtures expose deliberately different surfaces: Garage masks several AWS-S3 quirks (admin-port health, layout-assignment init), while MinIO mirrors AWS-S3 surface much more closely. Tests that need AWS-API parity MUST target the MinIO fixture; tests covering the Garage init dance MUST target the Garage fixture.
#### Scenario: Both fixtures coexist
- **WHEN** a test environment registers both `MinioFixture` and `GarageS3Fixture`
- **THEN** both fixtures start in parallel with no shared port or env-var conflicts
### Requirement: Image-pinning policy
The default image MUST be a pinned `minio/minio:RELEASE.<date>` tag (latest stable RELEASE at implementation time). The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:<digest>` comment until the digest is recorded.
#### Scenario: Pinned RELEASE tag
- **WHEN** the constants file is read
- **THEN** the image string starts with `minio/minio:RELEASE.` AND a sha256 TODO line is present
### Requirement: Env-bridge passthrough
The fixture MUST consult `MINIO_S3_ENDPOINT`, `MINIO_S3_ACCESS_KEY_ID`, `MINIO_S3_SECRET_ACCESS_KEY`, `MINIO_S3_REGION`, and `MINIO_S3_BUCKET`. When the first four are present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live MinIO. `MINIO_S3_BUCKET` is optional in passthrough mode.
#### Scenario: Live MinIO present
- **WHEN** the four required env vars are set
- **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `endpoint` equals `MINIO_S3_ENDPOINT`
#### Scenario: Partial env triggers container
- **WHEN** `MINIO_S3_ENDPOINT` is set but `MINIO_S3_SECRET_ACCESS_KEY` is empty
- **THEN** `start()` falls back to container mode
### Requirement: Healthcheck
The fixture MUST poll `HTTP GET <endpoint>/minio/health/live` and treat a `200` response as ready. The default `healthTimeoutMs` MUST be `60000`. Callers MAY override.
#### Scenario: Healthcheck succeeds
- **WHEN** `/minio/health/live` returns 200
- **THEN** `start()` resolves
#### Scenario: Healthcheck times out
- **WHEN** the endpoint never returns 200 within `healthTimeoutMs`
- **THEN** `start()` rejects with code `INTEGRATION.MINIO.HEALTHCHECK_TIMEOUT`
### Requirement: Random keys per run
In container mode, when `accessKeyId`/`secretAccessKey` are not supplied, the fixture MUST generate cryptographically random keys for each run (≥ 20 chars, alphanumeric) and pass them as `MINIO_ROOT_USER` / `MINIO_ROOT_PASSWORD` env vars to the container.
#### Scenario: Random keys
- **WHEN** the fixture starts twice without explicit keys
- **THEN** the two runs use different `accessKeyId` values
### Requirement: Bucket pre-seeding
When `bucket` is supplied, the fixture MUST create the bucket via the MinIO admin API after the healthcheck passes, using the injectable `fetchFn`. The fixture MUST NOT resolve `start()` until the bucket exists (verified via `HEAD <endpoint>/<bucket>` returning `200`).
#### Scenario: Bucket created
- **WHEN** `start({ bucket: 'app-uploads' })` resolves
- **THEN** `HEAD /app-uploads` returns 200 against the returned endpoint
### Requirement: Config shape
The exported `MinioConfig` interface MUST include `accessKeyId`, `secretAccessKey`, `region`, optional `bucket?: string`, optional `console?: boolean`, and the resolved `endpoint`. A sibling `minio-fixture.schemas.ts` MUST export a Zod `MinioConfigSchema` that validates the same shape.
#### Scenario: Schema parity
- **WHEN** `MinioConfigSchema.safeParse` runs against a `MinioConfig` value
- **THEN** valid configs parse OK AND missing required fields fail with `ZodError`
### Requirement: Error-code prefix
All errors thrown from the fixture MUST carry a `code` matching `^INTEGRATION\.MINIO\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any fixture method throws
- **THEN** `code` starts with `INTEGRATION.MINIO.`
### Requirement: Injectable fakes
The fixture constructor MUST accept `containerFactory?: () => GenericContainer` and `fetchFn?: typeof fetch`. Unit tests MUST drive 100% Istanbul branch coverage without Docker or network.
#### Scenario: Fake injection
- **WHEN** the fixture is constructed with both fakes
- **THEN** real Docker / real `fetch` are never called during the test
### Requirement: Plugin scenarios — AWS-S3 parity
The fixture MUST support, end-to-end against a real container with an AWS S3 SDK client (`@aws-sdk/client-s3`), the following flows in its smoke test:
#### Scenario: PutObject and GetObject round-trip
- **WHEN** an SDK client puts an object then gets it back
- **THEN** the retrieved body bytes equal the put body bytes AND the `ETag` header is present
#### Scenario: Presigned GET URL works
- **WHEN** a presigned GET URL is created via `@aws-sdk/s3-request-presigner` and fetched directly with no SDK signing
- **THEN** the response is `200` with the object body
#### Scenario: Multipart upload works
- **WHEN** an object larger than the part-size threshold (≥ 5 MB) is uploaded via `CreateMultipartUpload` + `UploadPart` + `CompleteMultipartUpload`
- **THEN** the assembled object's body equals the concatenated source AND `HeadObject` reports the full content length
#### Scenario: Bucket policy attach
- **WHEN** a `PutBucketPolicy` request attaches a public-read policy
- **THEN** `GetBucketPolicy` returns the same JSON document

View File

@@ -0,0 +1,106 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-integration-mongodb` package's `dependencies` MUST be exactly `@suaveplan/testing-integration` (`workspace:^`) and `testcontainers` (`catalog:integration`). No other `@suaveplan/*` runtime dep is permitted. `devDependencies` MAY include `@suaveplan/testing` for the package's own tests.
#### Scenario: Runtime closure clean
- **WHEN** `bunx turbo ls --filter=@suaveplan/testing-integration-mongodb...` is run
- **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration`
### Requirement: Image-pinning policy
The default image MUST be `mongo:7.0`. The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:<digest>` comment until the digest is recorded.
#### Scenario: Pinned tag
- **WHEN** the constants file is read
- **THEN** the image string equals `mongo:7.0` AND a sha256 TODO line is present
### Requirement: Env-bridge passthrough
The fixture MUST consult `MONGODB_URL` and `MONGODB_DATABASE`. When both are present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live MongoDB.
#### Scenario: Live MongoDB present
- **WHEN** both env vars are set
- **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `url` equals `MONGODB_URL`
#### Scenario: Partial env triggers container
- **WHEN** `MONGODB_URL` is set but `MONGODB_DATABASE` is empty
- **THEN** `start()` falls back to container mode
### Requirement: Healthcheck
The fixture MUST run `mongosh --eval "db.runCommand('ping')"` in-container via the injectable `execFn` and treat exit code `0` with stdout containing `ok` as ready. The default `healthTimeoutMs` MUST be `60000`. Callers MAY override.
#### Scenario: Healthcheck succeeds
- **WHEN** the in-container `mongosh` ping returns exit code 0 with `{ ok: 1 }` stdout
- **THEN** `start()` resolves
#### Scenario: Healthcheck times out
- **WHEN** the ping never succeeds within `healthTimeoutMs`
- **THEN** `start()` rejects with code `INTEGRATION.MONGODB.HEALTHCHECK_TIMEOUT`
### Requirement: Replica-set initialisation
When `replicaSet: true` is supplied, the fixture MUST start `mongod` with `--replSet rs0` and run `rs.initiate()` via `execFn` after the ping healthcheck passes. The fixture MUST NOT resolve `start()` until `rs.status().ok === 1`.
#### Scenario: Replica-set ready
- **WHEN** `start({ database: 'app', replicaSet: true })` resolves
- **THEN** a subsequent `rs.status()` call reports `ok: 1` AND a multi-document transaction succeeds against the returned `url`
### Requirement: Authentication mode
When `authentication: { username, password }` is supplied, the fixture MUST start `mongod` with `--auth`, create the supplied user via `execFn` against the `admin` database, and embed the credentials in the returned `url` (`mongodb://user:pass@host:port/database`).
#### Scenario: Auth rejects bad credentials
- **WHEN** the fixture starts in auth mode and a client connects with an incorrect password
- **THEN** the connection fails with a MongoDB authentication error
### Requirement: Config shape
The exported `MongoDbConfig` interface MUST include `database`, optional `replicaSet?: boolean`, optional `authentication?: { username: string; password: string }`, and the resolved `url`. A sibling `mongodb-fixture.schemas.ts` MUST export a Zod `MongoDbConfigSchema` that validates the same shape.
#### Scenario: Schema parity
- **WHEN** `MongoDbConfigSchema.safeParse` is run against a `MongoDbConfig` value
- **THEN** valid configs parse OK AND missing required fields fail with `ZodError`
### Requirement: Error-code prefix
All errors thrown from the fixture MUST carry a `code` matching `^INTEGRATION\.MONGODB\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any fixture method throws
- **THEN** `code` starts with `INTEGRATION.MONGODB.`
### Requirement: Injectable fakes
The fixture constructor MUST accept `containerFactory?: () => GenericContainer` and `execFn?: (cmd: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>`. Unit tests MUST be able to drive 100% Istanbul branch coverage without Docker.
#### Scenario: Fake injection
- **WHEN** the fixture is constructed with both fakes
- **THEN** real Docker is never invoked during the test
### Requirement: Plugin scenarios
The fixture MUST support, end-to-end against a real container, the following flows in its smoke test:
#### Scenario: Plain mode write/read round-trip
- **WHEN** the fixture is started in plain mode and an `insertOne` followed by a `findOne` is performed
- **THEN** the inserted document is read back unchanged
#### Scenario: Replica-set transaction round-trip
- **WHEN** the fixture is started with `replicaSet: true` and a multi-document transaction is committed
- **THEN** all inserts are visible after `commitTransaction()` AND none are visible if `abortTransaction()` is called instead

View File

@@ -0,0 +1,102 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-integration-mysql` package's `dependencies` MUST be exactly `@suaveplan/testing-integration` (`workspace:^`) and `testcontainers` (`catalog:integration`). No other `@suaveplan/*` runtime dep is permitted. `devDependencies` MAY include `@suaveplan/testing` for the package's own tests.
#### Scenario: Runtime closure clean
- **WHEN** `bunx turbo ls --filter=@suaveplan/testing-integration-mysql...` is run
- **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration`
### Requirement: Image-pinning policy
The default image MUST be `mysql:8.4`. The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:<digest>` comment until the digest is recorded.
#### Scenario: Pinned tag
- **WHEN** the constants file is read
- **THEN** the image string equals `mysql:8.4` AND a sha256 TODO line is present
### Requirement: Env-bridge passthrough
The fixture MUST consult `MYSQL_URL`, `MYSQL_DATABASE`, `MYSQL_USER`, `MYSQL_PASSWORD`, and `MYSQL_ROOT_PASSWORD`. When all five are present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live MySQL.
#### Scenario: Live MySQL present
- **WHEN** all five env vars are set
- **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `url` equals `MYSQL_URL`
#### Scenario: Partial env triggers container
- **WHEN** `MYSQL_URL` is set but `MYSQL_ROOT_PASSWORD` is empty
- **THEN** `start()` falls back to container mode
### Requirement: Healthcheck
The fixture MUST run `mysqladmin ping -h localhost -p$MYSQL_ROOT_PASSWORD` in-container via the injectable `execFn` and treat exit code `0` with stdout containing `mysqld is alive` as ready. The default `healthTimeoutMs` MUST be `90000` (initial DB init can be slow on cold image pulls). Callers MAY override.
#### Scenario: Healthcheck succeeds
- **WHEN** in-container `mysqladmin ping` returns exit code 0 with `mysqld is alive`
- **THEN** `start()` resolves
#### Scenario: Healthcheck times out
- **WHEN** the ping never succeeds within `healthTimeoutMs`
- **THEN** `start()` rejects with code `INTEGRATION.MYSQL.HEALTHCHECK_TIMEOUT`
### Requirement: Charset and collation overrides
The fixture MUST default to `charset: 'utf8mb4'` and `collation: 'utf8mb4_0900_ai_ci'` and MUST pass `--character-set-server=<charset>` and `--collation-server=<collation>` to `mysqld` when overrides are supplied via config.
#### Scenario: UTF-8mb4 default
- **WHEN** the fixture is started with no charset override
- **THEN** `SHOW VARIABLES LIKE 'character_set_server'` returns `utf8mb4`
#### Scenario: Charset override applied
- **WHEN** the fixture is started with `charset: 'utf8'`
- **THEN** `SHOW VARIABLES LIKE 'character_set_server'` returns `utf8`
### Requirement: Config shape
The exported `MySqlConfig` interface MUST include `database`, `user`, `password`, `rootPassword`, optional `charset?: string`, optional `collation?: string`, and the resolved `url`. A sibling `mysql-fixture.schemas.ts` MUST export a Zod `MySqlConfigSchema` that validates the same shape.
#### Scenario: Schema parity
- **WHEN** `MySqlConfigSchema.safeParse` is run against a `MySqlConfig` value
- **THEN** valid configs parse OK AND missing required fields fail with `ZodError`
### Requirement: Error-code prefix
All errors thrown from the fixture MUST carry a `code` matching `^INTEGRATION\.MYSQL\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any fixture method throws
- **THEN** `code` starts with `INTEGRATION.MYSQL.`
### Requirement: Injectable fakes
The fixture constructor MUST accept `containerFactory?: () => GenericContainer` and `execFn?: (cmd: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>`. Unit tests MUST drive 100% Istanbul branch coverage without Docker.
#### Scenario: Fake injection
- **WHEN** the fixture is constructed with both fakes
- **THEN** real Docker is never invoked during the test
### Requirement: Plugin scenarios
The fixture MUST support, end-to-end against a real container, the following flows in its smoke test:
#### Scenario: Transaction round-trip
- **WHEN** a transaction inserts rows and commits
- **THEN** the rows are visible to a subsequent connection AND a rolled-back transaction's rows are not visible
#### Scenario: DDL post-start
- **WHEN** `CREATE TABLE`, `ALTER TABLE ADD COLUMN`, and `DROP TABLE` statements run after `start()` resolves
- **THEN** each statement succeeds AND `INFORMATION_SCHEMA.TABLES` reflects the change

View File

@@ -0,0 +1,120 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-integration-nats` package's `dependencies` MUST be exactly `@suaveplan/testing-integration` (`workspace:^`) and `testcontainers` (`catalog:integration`). No other `@suaveplan/*` runtime dep is permitted. `devDependencies` MAY include `@suaveplan/testing` for the package's own tests.
#### Scenario: Runtime closure clean
- **WHEN** `bunx turbo ls --filter=@suaveplan/testing-integration-nats...` is run
- **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration`
### Requirement: Image-pinning policy
The default image MUST be `nats:2.10-alpine`. The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:<digest>` comment until the digest is recorded.
#### Scenario: Pinned tag
- **WHEN** the constants file is read
- **THEN** the image string equals `nats:2.10-alpine` AND a sha256 TODO line is present
### Requirement: Env-bridge passthrough
The fixture MUST consult `NATS_URL`, optional `NATS_CLUSTER_NAME`, and optional `NATS_JETSTREAM_DOMAIN`. When `NATS_URL` is present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live NATS server. Optional vars influence the returned config but are not required.
#### Scenario: Live NATS present
- **WHEN** `NATS_URL` is set
- **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `url` equals `NATS_URL`
#### Scenario: No env triggers container
- **WHEN** `NATS_URL` is unset or empty
- **THEN** `start()` falls back to container mode
### Requirement: Healthcheck
The fixture MUST perform a TCP PING on the mapped client port (`4222` inside the container) via the injectable `tcpPingFn`. When the container exposes the monitoring endpoint (`-m 8222`), the fixture MUST also poll `HTTP GET /healthz` and treat a `200` response as ready. The default `healthTimeoutMs` MUST be `60000`. Callers MAY override.
#### Scenario: TCP ping succeeds
- **WHEN** `tcpPingFn(host, mappedClientPort)` returns true
- **THEN** `start()` resolves once any pre-seeding has completed
#### Scenario: Healthcheck times out
- **WHEN** the TCP ping never succeeds within `healthTimeoutMs`
- **THEN** `start()` rejects with code `INTEGRATION.NATS.HEALTHCHECK_TIMEOUT`
### Requirement: JetStream initialisation
When `jetstream: true` is supplied, the fixture MUST start `nats-server` with `-js` and pre-seed any supplied streams and durable consumers via `nats stream add` / `nats consumer add` invoked through the injectable `execFn` after the TCP-ping healthcheck passes. The fixture MUST NOT resolve `start()` until each declared stream reports `state.messages >= 0` via `nats stream info`.
#### Scenario: JetStream stream seeded
- **WHEN** `start({ jetstream: true, streams: [{ name: 'EVENTS', subjects: ['events.>'] }] })` resolves
- **THEN** `nats stream info EVENTS` reports the stream exists with the declared subjects
### Requirement: Cluster mode
When `cluster: { name, routes }` is supplied, the fixture MUST start `nats-server` with `--cluster_name <name>` and the supplied `--routes` list. The fixture MUST verify cluster formation by calling `nats server list` via `execFn` and checking the cluster name matches.
#### Scenario: Cluster formed
- **WHEN** the fixture is started with a cluster config
- **THEN** `nats server list` reports `cluster: <name>`
### Requirement: Authentication mode
The fixture MUST accept `auth: { user: string; password: string }` OR `auth: { token: string }` (mutually exclusive). When supplied, the fixture MUST start `nats-server` with the corresponding `--user`/`--pass` or `--auth` flags AND embed credentials in the returned `url` (`nats://user:pass@host:port` or `nats://token@host:port`).
#### Scenario: Token auth rejects bad token
- **WHEN** the fixture starts with `auth: { token: 'good' }` and a client connects with token `bad`
- **THEN** the client connection is refused with a NATS authorization error
### Requirement: Config shape
The exported `NatsConfig` interface MUST include optional `jetstream?: boolean`, optional `cluster?: { name: string; routes: string[] }`, optional `auth?: { user: string; password: string } | { token: string }`, optional `streams?: Array<{ name: string; subjects: string[] }>`, optional `consumers?: Array<{ stream: string; durable: string }>`, and the resolved `url`. A sibling `nats-fixture.schemas.ts` MUST export a Zod `NatsConfigSchema` that validates the same shape including the `auth` discriminated union.
#### Scenario: Schema parity
- **WHEN** `NatsConfigSchema.safeParse` runs against a `NatsConfig` value
- **THEN** valid configs parse OK AND mixing `user`/`password` with `token` in the same `auth` object fails with `ZodError`
### Requirement: Error-code prefix
All errors thrown from the fixture MUST carry a `code` matching `^INTEGRATION\.NATS\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any fixture method throws
- **THEN** `code` starts with `INTEGRATION.NATS.`
### Requirement: Injectable fakes
The fixture constructor MUST accept `containerFactory?: () => GenericContainer`, `tcpPingFn?: (host: string, port: number) => Promise<boolean>`, and `execFn?: (cmd: string[]) => Promise<{ stdout: string; stderr: string; exitCode: number }>`. Unit tests MUST drive 100% Istanbul branch coverage without Docker.
#### Scenario: Fake injection
- **WHEN** the fixture is constructed with all three fakes
- **THEN** real Docker / real TCP / real exec are never invoked during the test
### Requirement: Plugin scenarios
The fixture MUST support, end-to-end against a real container, the following flows in its smoke test:
#### Scenario: Core pub/sub round-trip
- **WHEN** a subscriber subscribes to `test.>` and a publisher publishes to `test.hello`
- **THEN** the subscriber receives the message within 1s
#### Scenario: JetStream durable consumer
- **WHEN** the fixture seeds `EVENTS` stream + `worker` durable consumer, a publisher publishes 10 messages, and the consumer pulls
- **THEN** the consumer acks all 10 messages AND `nats consumer info EVENTS worker` reports `num_pending: 0`
#### Scenario: Request/reply
- **WHEN** a responder subscribes to `svc.echo` and replies with the request payload, and a client issues a request
- **THEN** the client receives the original payload within the request timeout

View File

@@ -0,0 +1,107 @@
## ADDED Requirements
### Requirement: Layer 0 dep-graph isolation
The `@suaveplan/testing-integration-oidc` package's `dependencies` MUST be exactly `@suaveplan/testing-integration` (`workspace:^`) and `testcontainers` (`catalog:integration`). No other `@suaveplan/*` runtime dep is permitted. `devDependencies` MAY include `@suaveplan/testing` for the package's own tests.
#### Scenario: Runtime closure clean
- **WHEN** `bunx turbo ls --filter=@suaveplan/testing-integration-oidc...` is run
- **THEN** the only `@suaveplan/*` package in the runtime closure is `@suaveplan/testing-integration`
### Requirement: Pluggable provider selection
The fixture SHALL accept a `provider: 'keycloak' | 'oauth2-mock-server'` config field. Both providers MUST satisfy the same `ServiceFixture<OidcConfig>` contract — only the underlying image, init sequence, and admin-API endpoints differ.
#### Scenario: Keycloak provider selected
- **WHEN** `start({ provider: 'keycloak', realm: 'test' })` is called
- **THEN** the fixture pulls `quay.io/keycloak/keycloak:26.0` AND seeds the `test` realm via the Keycloak admin REST API before resolving
#### Scenario: oauth2-mock-server provider selected
- **WHEN** `start({ provider: 'oauth2-mock-server' })` is called
- **THEN** the fixture pulls `ghcr.io/navikt/mock-oauth2-server:2` AND seeds clients/users via its JSON config-file mount before resolving
### Requirement: Image-pinning policy
Default images MUST be pinned to a minor tag (`quay.io/keycloak/keycloak:26.0`, `ghcr.io/navikt/mock-oauth2-server:2`). The `constants/index.ts` file MUST carry a `# TODO: replace with sha256:<digest>` comment for each image until the digest is recorded.
#### Scenario: Pinned tag
- **WHEN** the constants file is read
- **THEN** both image strings include an explicit minor tag AND a sha256 TODO line is present
### Requirement: Env-bridge passthrough
The fixture MUST consult `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, `OIDC_REDIRECT_URI`, and optional `OIDC_USERS_FILE`. When `OIDC_ISSUER_URL`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`, and `OIDC_REDIRECT_URI` are all present and non-empty, `start()` MUST skip container creation and return a handle whose config points at the live issuer.
#### Scenario: Live issuer present
- **WHEN** all four required env vars are set
- **THEN** `start()` returns without invoking `containerFactory` AND the returned config's `issuerUrl` equals `OIDC_ISSUER_URL`
#### Scenario: Partial env triggers container
- **WHEN** `OIDC_ISSUER_URL` is set but `OIDC_CLIENT_SECRET` is empty
- **THEN** `start()` falls back to container mode
### Requirement: Healthcheck
The fixture MUST poll `HTTP GET <issuerUrl>/.well-known/openid-configuration` and treat a `200` JSON response containing `issuer`, `authorization_endpoint`, `token_endpoint`, `jwks_uri` as ready. The default `healthTimeoutMs` MUST be `90000` (Keycloak slow-start). Callers MAY override.
#### Scenario: Healthcheck succeeds
- **WHEN** `/.well-known/openid-configuration` returns 200 with the four required fields
- **THEN** `start()` resolves
#### Scenario: Healthcheck times out
- **WHEN** the discovery endpoint never returns within `healthTimeoutMs`
- **THEN** `start()` rejects with code `INTEGRATION.OIDC.HEALTHCHECK_TIMEOUT`
### Requirement: Config shape
The exported `OidcConfig` interface MUST include `provider`, optional `realm`, optional `users: Array<{ username: string; password: string; email: string }>`, optional `clients: Array<{ id: string; secret: string; redirectUris: string[]; grantTypes: string[] }>`, and the resolved `issuerUrl`. A sibling `oidc-fixture.schemas.ts` MUST export a Zod schema (`OidcConfigSchema`) that validates the same shape.
#### Scenario: Schema parity
- **WHEN** `OidcConfigSchema.safeParse` is run against an `OidcConfig` value
- **THEN** the parse succeeds for valid configs AND fails with a `ZodError` for missing required fields
### Requirement: Error-code prefix
All errors thrown from the fixture MUST be subclasses of `TestingIntegrationError` carrying `{ code, cause?, context? }` where `code` matches `^INTEGRATION\.OIDC\.[A-Z_]+$`.
#### Scenario: Coded error
- **WHEN** any fixture method throws
- **THEN** the thrown value's `code` starts with `INTEGRATION.OIDC.`
### Requirement: Injectable fakes
The fixture constructor MUST accept `containerFactory?: () => GenericContainer` and `fetchFn?: typeof fetch`. Unit tests MUST be able to drive 100% Istanbul branch coverage without invoking Docker or the network.
#### Scenario: Fake injection
- **WHEN** the fixture is constructed with both fakes
- **THEN** real Docker / real `fetch` are never called during the test
### Requirement: Plugin scenarios
The fixture MUST support, end-to-end against a real container, the following flows used by `@suaveplan/oauth-client` and exercised in the plugin's smoke test:
#### Scenario: Authorization Code + PKCE returns valid tokens
- **WHEN** `@suaveplan/oauth-client` runs an Authorization Code + PKCE flow against the fixture's seeded client
- **THEN** the issuer returns an `id_token` AND `access_token` whose JWT signature verifies against the `jwks_uri`
#### Scenario: Client Credentials grant succeeds
- **WHEN** a `grant_type=client_credentials` request is made with the seeded client id and secret
- **THEN** the response is `200 OK` with an `access_token` JWT
#### Scenario: Revoked user introspection
- **WHEN** a seeded user is removed (Keycloak admin REST or oauth2-mock-server config reload) and a previously issued token is introspected
- **THEN** the introspection response has `active: false`

View File

@@ -0,0 +1,49 @@
## 0. Pre-flight (non-negotiable)
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
## 1. Pre-conditions
- [ ] 1.1 Confirm `@suaveplan/testing-integration` is published to Verdaccio at `https://npmjs.fq.io/` and resolves as `workspace:^`.
- [ ] 1.2 Confirm `testcontainers` exists in the root `package.json` `catalog.integration` block.
- [ ] 1.3 Confirm the existing six `testing-integration-*` plugins still pass `bunx turbo lint typecheck test --filter=@suaveplan/testing-integration-*`.
- [ ] 1.4 Confirm the audit doc at `/mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md` is current and §1 still tracks these five plugins.
## 2. Spec deltas (this change)
- [ ] 2.1 Land `specs/testing-integration-oidc/spec.md` with image policy, env-bridge, healthcheck, config, error prefix, fakes, scenarios.
- [ ] 2.2 Land `specs/testing-integration-mongodb/spec.md` with replica-set + auth modes.
- [ ] 2.3 Land `specs/testing-integration-mysql/spec.md` with charset/collation overrides.
- [ ] 2.4 Land `specs/testing-integration-minio/spec.md` with AWS-S3 parity scenarios.
- [ ] 2.5 Land `specs/testing-integration-nats/spec.md` with core + JetStream + cluster + auth modes.
## 3. Capability registration
- [ ] 3.1 On archive, fold each delta into `openspec/specs/<capability>/spec.md` (five new capability folders).
- [ ] 3.2 Move this change folder to `openspec/archive/<yyyy-mm-dd>-enterprise-testing-fixtures-expansion/`.
## 4. Implementation hand-off (separate plans, not part of this change)
- [ ] 4.1 Author `docs/superpowers/plans/<date>-testing-integration-oidc.md` mirroring the six-plugin plan structure.
- [ ] 4.2 Author corresponding plans for mongodb, mysql, minio, nats.
- [ ] 4.3 Each plan must include: package skeleton, fixture impl, fakes, integration smoke test guarded by `skipIfUnavailable()`, README, sibling `.md` per implementation file, 100% Istanbul coverage, root `README.md` table update, `CLAUDE.md` package-list entry.
## 5. Pre-publish gate (per plugin, executed inside the implementation plan)
- [ ] 5.1 `bunx turbo lint typecheck test --filter=@suaveplan/testing-integration-<name>` green.
- [ ] 5.2 `bunx turbo test:coverage --filter=@suaveplan/testing-integration-<name>` at 100/100/100/100 (Istanbul).
- [ ] 5.3 `bunx turbo build --filter=@suaveplan/testing-integration-<name>` green.
- [ ] 5.4 Banned-pattern scan zero (per `openspec/AGENTS.md` §2).
- [ ] 5.5 Sibling-`.md` scan zero misses (per `AGENTS.md` §4).
- [ ] 5.6 `bun scripts/publish.ts --package @suaveplan/testing-integration-<name>` after gates green.
## 6. Documentation propagation
- [ ] 6.1 Add each new package to the `CLAUDE.md` package list with one-paragraph blurb (image, env vars, healthcheck, error prefix, deps).
- [ ] 6.2 Add each new package row to the root `README.md` package table.
- [ ] 6.3 Update `docs/superpowers/specs/2026-04-24-enterprise-testbed-design.md` §4 fixture inventory.
- [ ] 6.4 Cross-reference each new plugin from the relevant consumer package's README (`oauth-client`, `events`, future `db-mongodb`/`db-mysql`).