Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
# @suaveplan/testing-integration-mailpit - Mailpit testcontainers fixture + assertMail helper for @suaveplan/testing-integration
## Summary
Mailpit testcontainers fixture + `assertMail()` helper for `@suaveplan/testing-integration`.
Implements `ServiceFixture<MailpitConfig>` — manages a Mailpit container lifecycle with SMTP and API ports, env-bridge passthrough, and API health checks.
<!-- begin:auto-docs -->
## Documentation tree
- [fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_- [mailpit-fixture](./src/fixture/mailpit-fixture.md) — <!-- begin:auto-docs --> _(no submodules)_- [helpers](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_ - [assert-mail](./src/helpers/assert-mail.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add -D @suaveplan/testing-integration-mailpit
```
## Quick Start
```typescript
import { MailpitFixture, assertMail } from "@suaveplan/testing-integration-mailpit";
const fixture = new MailpitFixture();
const config = await fixture.start();
// config.smtpUrl, config.apiUrl
// Send email to config.smtpUrl...
await assertMail(config.apiUrl, { to: "user@example.com", subject: "Welcome" });
await fixture.stop();
```
## Env-bridge passthrough
Set both env vars to skip container startup:
```text
MAILPIT_SMTP_URL=smtp://localhost:1025
MAILPIT_API_URL=http://localhost:8025
```
## Docs
- [MailpitFixture](src/fixture/mailpit-fixture.md)
- [assertMail](src/helpers/assert-mail.md)

View File

@@ -0,0 +1,110 @@
# MailpitFixture
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`MailpitFixture` is an integration test fixture that manages the full lifecycle of a Mailpit container using testcontainers. It implements the `ServiceFixture<MailpitConfig>` interface from `@suaveplan/testing-integration`, providing an SMTP server and REST API for email testing. The fixture binds two ports: SMTP on 1025 and the web/API on 8025.
The fixture integrates with the `assertMail()` helper exported from the same package, enabling tests to send emails via SMTP and then verify delivery through the Mailpit REST API.
## Features
- **Env-bridge passthrough**: when both `MAILPIT_SMTP_URL` and `MAILPIT_API_URL` are set and non-empty, the fixture returns them directly. Partial env sets (only one variable) fall through to container mode.
- **Dual-port binding**: exposes both SMTP (1025) and API (8025) ports, mapping each to an ephemeral host port.
- **API health check**: polls `GET /api/v1/messages` on the web port. Accepts a 200 response as healthy.
- **Injectable fetch**: the `fetchFn` option replaces `globalThis.fetch` in health checks for unit testing.
- **Injectable container factory**: the `containerFactory` option allows unit tests to swap in a fake container without Docker.
- **Idempotent stop**: safe to call on an already-stopped or passthrough fixture.
## Usage: Basic Example
```typescript
import { MailpitFixture } from "@suaveplan/testing-integration-mailpit";
const fixture = new MailpitFixture();
const config = await fixture.start();
// config.smtpUrl → "smtp://localhost:<randomSmtpPort>"
// config.apiUrl → "http://localhost:<randomApiPort>"
// Send an email to SMTP, then use assertMail() to verify it arrived.
await fixture.stop();
```
## Usage: Advanced Example
```typescript
import {
MailpitFixture,
assertMail,
} from "@suaveplan/testing-integration-mailpit";
import nodemailer from "nodemailer";
const fixture = new MailpitFixture({ image: "axllent/mailpit:latest" });
const config = await fixture.start();
const transporter = nodemailer.createTransport({ url: config.smtpUrl });
await transporter.sendMail({
from: "noreply@example.com",
to: "user@example.com",
subject: "Welcome to SuavePlan",
text: "Hello!",
});
await assertMail(config.apiUrl, {
to: "user@example.com",
subject: "Welcome",
});
await fixture.stop();
```
## API Reference
### `class MailpitFixture`
Implements `ServiceFixture<MailpitConfig>`.
#### `constructor(options?: MailpitFixtureOptions)`
Creates a new fixture instance. All options are optional.
#### `name: string`
Always `"mailpit"`. Used by `TestEnvironment` as the registry key.
#### `start(): Promise<MailpitConfig>`
Starts the service. Returns the runtime configuration. In passthrough mode, returns immediately with env var values. Otherwise starts a testcontainers container.
#### `stop(): Promise<void>`
Stops the container. No-op in passthrough mode or when not started.
#### `healthCheck(): Promise<boolean>`
Returns `true` when `GET <apiUrl>/api/v1/messages` returns an OK response. Returns `false` if no container is active or the request fails.
### `interface MailpitConfig`
| Field | Type | Description |
|-------|------|-------------|
| `smtpUrl` | `string` | SMTP URL: `smtp://localhost:<port>` |
| `apiUrl` | `string` | API URL: `http://localhost:<port>` |
### `interface MailpitFixtureOptions`
| Field | Type | Description |
|-------|------|-------------|
| `image?` | `string` | Docker image override. Defaults to `axllent/mailpit:latest`. |
| `containerFactory?` | `(image: string) => GenericContainer` | Injectable factory for unit testing. |
| `fetchFn?` | `typeof fetch` | Injectable fetch for health check unit testing. |
## Implementation Notes
- **Why API health check over SMTP**: the Mailpit REST API provides a reliable readiness signal. Attempting an SMTP connect and transaction would be heavier and require parsing SMTP protocol responses.
- **Partial passthrough handling**: the env-bridge only activates when BOTH `MAILPIT_SMTP_URL` and `MAILPIT_API_URL` are set. This prevents silently returning an incomplete config if one variable is set accidentally.
- **Dynamic import of testcontainers**: deferred to `start()` time so unit tests that provide a `containerFactory` do not trigger testcontainers initialization.

View File

@@ -0,0 +1,104 @@
# assertMail
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`assertMail()` is a test assertion helper that polls the Mailpit REST API and fails tests that do not receive an expected email within a configurable timeout. It bridges the gap between sending an email via SMTP and verifying delivery in integration tests.
The helper is designed for use with `MailpitFixture` but works with any Mailpit instance accessible at a known HTTP URL. Its injectable `fetchFn` option makes it fully unit-testable without a live Mailpit server.
## Features
- **Polling-based**: retries at a configurable interval until the deadline, tolerating normal async email delivery latency.
- **Exact address matching**: verifies `To[].Address` exactly matches the provided `to` address.
- **Substring subject matching**: checks that the message `Subject` includes the provided `subject` string, allowing partial matches.
- **Configurable timeout and interval**: `timeoutMs` and `pollIntervalMs` options allow fast-failing in unit tests (e.g. 50ms timeout) and longer waits in real environments.
- **Injectable fetch**: the `fetchFn` option replaces `globalThis.fetch` for deterministic unit tests with no live network dependency.
- **Structured error message**: the timeout error includes the `to` and `subject` criteria and timeout value for easy debugging.
## Usage: Basic Example
```typescript
import { assertMail } from "@suaveplan/testing-integration-mailpit";
// After sending an email via SMTP...
await assertMail("http://localhost:8025", {
to: "user@example.com",
subject: "Welcome",
});
// Resolves when a matching message is found; throws after 10s otherwise.
```
## Usage: Advanced Example
```typescript
import {
MailpitFixture,
assertMail,
} from "@suaveplan/testing-integration-mailpit";
import nodemailer from "nodemailer";
const fixture = new MailpitFixture();
const config = await fixture.start();
const transporter = nodemailer.createTransport({ url: config.smtpUrl });
await transporter.sendMail({
from: "noreply@example.com",
to: "alice@example.com",
subject: "Password Reset Request",
text: "Click here to reset your password.",
});
// Custom timeout: wait up to 5 seconds, polling every 200ms
await assertMail(config.apiUrl, {
to: "alice@example.com",
subject: "Password Reset",
}, {
timeoutMs: 5000,
pollIntervalMs: 200,
});
await fixture.stop();
```
## API Reference
### `assertMail(apiUrl, criteria, options?): Promise<void>`
Polls the Mailpit API until a matching message is found or the timeout elapses.
#### Parameters
| Name | Type | Description |
|------|------|-------------|
| `apiUrl` | `string` | Mailpit API base URL (e.g. `http://localhost:8025`). |
| `criteria` | `AssertMailCriteria` | Matching criteria. |
| `options?` | `AssertMailOptions` | Optional configuration. |
#### Throws
`Error` when no matching message is found within `timeoutMs`. The message includes the `to` and `subject` values and elapsed timeout.
### `interface AssertMailCriteria`
| Field | Type | Description |
|-------|------|-------------|
| `to` | `string` | Exact recipient address to match in `To[].Address`. |
| `subject` | `string` | Subject substring to match via `String.includes()`. |
### `interface AssertMailOptions`
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `timeoutMs?` | `number` | `10_000` | Total wait time in ms. |
| `pollIntervalMs?` | `number` | `500` | Polling interval in ms. |
| `fetchFn?` | `typeof fetch` | `globalThis.fetch` | Injectable fetch for unit tests. |
## Implementation Notes
- **Polling strategy**: the helper uses a `while (Date.now() < deadline)` loop with `setTimeout` between iterations. This provides predictable behavior without relying on `setInterval` side effects.
- **Mailpit API shape**: the `/api/v1/messages` endpoint returns `{ messages: Array<{ To: [{Address: string}], Subject: string, ... }> }`. The `messages` key may be absent on an empty inbox; the helper defaults to `[]`.
- **Partial subject matching rationale**: email subjects often contain dynamic content (timestamps, user names) appended to a fixed prefix. Substring matching lets tests assert on the stable prefix without hardcoding dynamic parts.
- **Exact address matching rationale**: recipient addresses are authoritative identifiers. Substring matching on addresses would be error-prone (e.g. `user@example.com` matching `user@example.company`).