## ADDED Requirements ### Requirement: Mailpit image policy The `@suaveplan/testing-integration-mailpit` plugin SHALL pin to `axllent/mailpit` at a fixed minor tag (e.g. `axllent/mailpit:v1.20`). The plugin MUST NOT use the `latest` tag. A follow-up operational change tracked at `docs/superpowers/specs/2026-04-25-test-suite-audit.md §5` will pin to `sha256:`. #### Scenario: Image tag pinned to minor - **WHEN** the container factory is invoked without injection - **THEN** the resulting container uses an `axllent/mailpit:v.` tag — never `axllent/mailpit:latest` ### Requirement: Env-bridge contract The plugin SHALL declare and forward both `MAILPIT_SMTP_URL` and `MAILPIT_API_URL`. When BOTH are set on the parent process at construction time, the fixture MUST enter env-bridge passthrough mode. A partial-set (only one of the two) MUST throw `INTEGRATION.MAILPIT.PARTIAL_ENV` at construction. #### Scenario: env() returns the full set - **WHEN** the fixture has started in container mode - **THEN** `fixture.env()` returns a record with exactly the keys `MAILPIT_SMTP_URL` and `MAILPIT_API_URL` #### Scenario: Partial env-bridge rejected - **WHEN** only `MAILPIT_SMTP_URL` is set on `process.env` at construction - **THEN** the constructor throws with `code === "INTEGRATION.MAILPIT.PARTIAL_ENV"` ### Requirement: Healthcheck contract — API ping `isHealthy()` SHALL issue an HTTP `GET` to the Mailpit API health endpoint and treat a 2xx response as healthy. The fixture constructor SHALL accept an optional `fetch` parameter for test injection. #### Scenario: 2xx is healthy - **WHEN** the API health endpoint returns 200 - **THEN** `isHealthy()` resolves `true` ### Requirement: assertMail() helper The package SHALL export `assertMail(opts: { to?: string | string[]; from?: string; subject?: string; subjectMatch?: "exact" | "partial" })` that queries the Mailpit API and asserts at least one matching message exists. **Address matching MUST be exact.** **Subject matching defaults to `"partial"`** (substring on `subject`); callers MAY opt into exact matching. On no match, the helper MUST throw with `code === "INTEGRATION.MAILPIT.NO_MATCH"` and `context` listing the candidates that were considered. #### Scenario: Partial subject match by default - **WHEN** Mailpit holds a message with subject `"Welcome to Suaveplan, please confirm your email"` and `assertMail({ subject: "Welcome to Suaveplan" })` is invoked - **THEN** the helper resolves without throwing #### Scenario: Exact address match required - **WHEN** Mailpit holds a message addressed to `user@example.com` and `assertMail({ to: "USER@example.com" })` is invoked without case-insensitivity opt-in - **THEN** the helper rejects with `INTEGRATION.MAILPIT.NO_MATCH` ### Requirement: Error-code prefix Errors thrown from the plugin SHALL carry codes matching `^INTEGRATION\.MAILPIT\.[A-Z_]+$`. #### Scenario: Coded helper failure - **WHEN** `assertMail()` finds no match - **THEN** the rejection's `code === "INTEGRATION.MAILPIT.NO_MATCH"` AND `context.candidates` is an array