## 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:` 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 ` 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: ` ### 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`, 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