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,22 @@
# api-host-testing-kit — Populate Stage-5 ./testing subpath
## Why
`@suaveplan/api-host/testing` has shipped a `STAGE_2_RESERVED` sentinel since Stage 2. Stage 5 in-memory adapters (`api-router-in-memory`, `api-auth-static-principal` / `api-auth-anonymous`, `api-db-sqlite-memory`, `api-events-in-memory`, `api-ws-broker-in-memory`, `api-tenant-provider-memory`, `api-secrets-env`) have all shipped, but no host-side composition surface drives them together. Plugin authors writing integration tests have to hand-wire every adapter slot at the call site — the same boilerplate observed in `e2e/foundation-stack/src/server.ts`. The keystone for the Stage-5 testing kit is a `createTestHost(config?)` factory that composes every default in one line.
## What changes
- Replace `STAGE_2_RESERVED` placeholder with the full `createTestHost(config?)` factory under `src/testing/create-test-host/`.
- Surface flips: the sentinel constant changes from `true` to `false` (back-compat preserved — consumers that branched on `=== true` still compile; the populated helpers ship alongside).
- New types: `TestHostConfig`, `TestHostSeed`, `TestHostAdapters`, `TestHost`, `FixturePluginInstall`, `FixtureRouteRegistration`.
- `TestHost` extends `Host` with `installFixturePlugin`, `fetchAs`, `waitForEvent`, `dispose`.
- Default adapter resolution per-slot, override-beats-default semantics.
- `seed.principal` / `seed.tenants` / `seed.secrets` populate the default adapter inputs.
- Companion smoke at `e2e/foundation-stack-test-host/` boots a Bun.serve over `createTestHost(...)` and drives every wired slot through real HTTP with Playwright.
- `suaveplan.conformanceSkip` for api-lifecycle / api-loader / api-manifest / api-secrets retained (api-host is the consumer/driver, not the implementer); the gap is documented in README + this proposal.
## Impact
- Affected specs: `api-host`
- Affected code: `packages/api-stack/api/api-host/src/testing/*`, `packages/api-stack/api/api-host/package.json` (peerDependenciesMeta + new devDependencies for the in-memory adapters), `e2e/foundation-stack-test-host/*` (new workspace).
- Affected downstream: every package previously branching on `STAGE_2_RESERVED === true` can now opt into the populated helper; no breaking change.

View File

@@ -0,0 +1,71 @@
# api-host — Stage-5 testing-kit population
## MODIFIED Requirements
### Requirement: `./testing` subpath populates the Stage-5 keystone
The package SHALL declare `./testing` as a subpath export AND ship:
1. `createTestHost(config?: TestHostConfig): Promise<TestHost>` — Stage-5 keystone composing every in-memory adapter into a single line of consumer code.
2. `runPipelineRouterConformance(factory)` plus the `PipelineRouterConformanceFactory` + `PipelineRouteRegistration` types (kept from the prior requirement).
3. The legacy `STAGE_2_RESERVED` sentinel, retained for back-compat; its value SHALL flip from `true` to `false` in this release so consumers that branched on `=== true` can detect the populated surface.
`createTestHost` SHALL resolve each adapter slot in override-beats-default order:
- `router` → default `createInMemoryRouter()` from `@suaveplan/api-router-in-memory` (loaded via dynamic specifier to side-step the build-graph cycle the package's runtime dep on `api-host` introduces).
- `auth` → default `SessionResolverChain([createStaticPrincipalResolver({principal: seed.principal})])` when `seed.principal` is set, else `SessionResolverChain([createAnonymousResolver()])`.
- `tenant` → default `tenantResolverChainFromProvider(createMemoryTenantProvider({ tenants: seed.tenants ?? [] }), { headerName: "X-Tenant-Id" })`.
- `secrets` → `createEnvSecrets({ allowedKeys: Object.keys(seed.secrets), source: () => seed.secrets })` when `seed.secrets` is set, else `new EmptySecrets()`.
- `db` → `createSqliteMemoryAdapter()`.
- `ledgerDb` → a separate `bun:sqlite` `:memory:` instance, loaded via dynamic specifier; disjoint from `db`.
- `capabilities` / `rateLimit` / `idempotency` / `csrf` / `audit` / `span` / `adminPrincipal` — minimal in-process accept-all stubs.
`TestHost` SHALL extend `Host` with `installFixturePlugin(fixture)`, `fetchAs(principal, request)`, `waitForEvent(topic, timeoutMs?)`, `dispose()`, and a `readonly adapters: TestHostAdapters` surface exposing the wired `HostAdapters` bag, the `InMemoryEventsBus`, the `LifecycleEventMap` event bus, and the materialised fixture-route registrations.
`waitForEvent` SHALL first inspect the in-memory bus's recorded history (so events emitted BEFORE the call still resolve) then subscribe to future emissions; it SHALL reject the returned promise when `timeoutMs` elapses.
`dispose` SHALL stop the wrapped host, dispose the event bus, dispose the ws-broker, close the admin db (when `close` is available), and close the ledger-db handle. Calling `dispose` more than once is a no-op.
The package's vite build SHALL emit `dist/testing/testing.js` AND `dist/testing/create-test-host/create-test-host.js` AND `dist/testing/pipeline-router-conformance/pipeline-router-conformance.js` so the subpath resolves at runtime.
#### Scenario: zero-config boot yields a usable host with every default adapter
- **WHEN** a test calls `await createTestHost()` with no arguments
- **THEN** the returned `TestHost.plugins()` SHALL be empty AND `host.adapters.host.router` SHALL be defined AND `host.adapters.eventBus` SHALL be defined AND `host.fetch(new Request("http://t/internal/livez"))` SHALL resolve with status 200
#### Scenario: seed.principal reaches `ctx.principal`
- **WHEN** a test calls `createTestHost({ seed: { principal } })` then installs a fixture plugin with a route reading `ctx.principal`
- **THEN** the handler SHALL observe `ctx.principal.id === principal.id` on every request
#### Scenario: seed.tenants + X-Tenant-Id reaches `ctx.tenant`
- **WHEN** a test seeds `tenants: [{ id: "t-a", slug: "tenant-a", ... }]` AND issues `fetch(new Request(url, { headers: { "X-Tenant-Id": "t-a" } }))`
- **THEN** the matched route handler SHALL observe `ctx.tenant.id === "t-a"` AND `ctx.tenant.slug === "tenant-a"`
#### Scenario: seed.secrets exposes `ctx.secrets.require`
- **WHEN** a test seeds `secrets: { FOO: "bar" }` AND a route handler calls `await ctx.secrets.require("FOO")`
- **THEN** the handler SHALL receive the string `"bar"`
#### Scenario: ctx.db round-trips a real bun:sqlite write/read
- **WHEN** the smoke server provisions a tenant-aware table via the admin db AND the per-request handler INSERTs + SELECTs through `ctx.db` under an `X-Tenant-Id` header
- **THEN** the SELECT SHALL return the inserted row
#### Scenario: waitForEvent resolves on bus emission
- **WHEN** a test calls `host.waitForEvent("topic.x", 2000)` AND a route handler (or test) calls `host.adapters.eventBus.publish("topic.x", payload)`
- **THEN** the `waitForEvent` promise SHALL resolve with the payload
#### Scenario: waitForEvent rejects on timeout
- **WHEN** a test calls `host.waitForEvent("topic.never", 25)` AND no emission lands within 25ms
- **THEN** the promise SHALL reject with an `Error` whose `message` contains `"timed out"`
#### Scenario: dispose is idempotent
- **WHEN** a test calls `host.dispose()` twice in succession
- **THEN** the second call SHALL be a no-op (no thrown error, no double-close on the ledger handle)
**Verified by:** `src/testing/create-test-host/create-test-host.test.ts` (13 unit tests against a hand-rolled router adapter) AND `e2e/foundation-stack-test-host/tests/test-host.spec.ts` (8 Playwright smoke groups against a Bun.serve listener).

View File

@@ -0,0 +1,10 @@
# Tasks
- [x] 1. Replace `STAGE_2_RESERVED` sentinel with the populated `./testing` surface (`createTestHost` + helper types).
- [x] 2. Wire per-slot default adapters (router, auth, tenant, db, secrets, ws-broker, ledger-db, event-bus).
- [x] 3. Implement `installFixturePlugin`, `fetchAs`, `waitForEvent`, `dispose` helpers on `TestHost`.
- [x] 4. Add 13 unit tests under `src/testing/create-test-host/create-test-host.test.ts` covering each helper + adapter slot using a hand-rolled router adapter (avoids the api-host ↔ api-router-in-memory dep cycle).
- [x] 5. Author the companion smoke at `e2e/foundation-stack-test-host/` — Bun.serve over a `createTestHost(...)` boot, Playwright spec covering 8 behaviour groups (plugin install, router resolution, principal seed, tenant seed, secret seed, db round-trip, event emission, livez).
- [x] 6. Update `STATUS.md` row for `@suaveplan/api-host`.
- [x] 7. Tick `5.6 api-host testing-kit subpath` in `openspec/changes/api-plugin-foundation/tasks.md`.
- [x] 8. Document the retained `conformanceSkip` posture in `api-host/README.md`.