82 lines
4.8 KiB
Markdown
82 lines
4.8 KiB
Markdown
# Capability Spec Delta: api-host — Pipeline per-stage observability
|
|
|
|
**Package**: `@suaveplan/api-host`
|
|
**Status**: PROPOSED
|
|
|
|
## ADDED Requirements
|
|
|
|
### Requirement: Host emits a structured event at every §6 pipeline stage boundary
|
|
|
|
`@suaveplan/api-host`'s `RequestPipeline` SHALL accept a `pipelineEvents: PipelineEventsBus` adapter slot on `HostAdapters` (from `@suaveplan/api-types/pipeline-events`). For every request processed, the pipeline MUST emit `PipelineStageEvent { stage, outcome: "entered", requestId, at }` on entry to each of the 11 stages defined in §6 of the foundation design doc, and MUST emit a matching `completed` / `rejected` / `skipped` event on exit.
|
|
|
|
#### Scenario: a successful request emits 22 events (entered + completed for each of 11 stages)
|
|
|
|
- **WHEN** a request that satisfies every stage's gates is processed
|
|
- **THEN** `pipelineEvents.subscribe` observes exactly 22 events: `entered` followed by `completed` for each of the 11 stages in §6 order
|
|
|
|
#### Scenario: a stage rejection halts subsequent stages
|
|
|
|
- **WHEN** the auth stage rejects a request (`outcome: "rejected"`)
|
|
- **THEN** no subsequent stage emits `entered` for that request id
|
|
|
|
#### Scenario: conditionally-skipped stages emit `skipped`, not silent omission
|
|
|
|
- **WHEN** an anonymous request bypasses idempotency-by-principal (no principal to key by)
|
|
- **THEN** the idempotency stage emits `{ stage: "idempotency", outcome: "skipped", requestId }` (NOT silent omission)
|
|
|
|
**Verified by:** `src/request-pipeline/request-pipeline.test.ts::"successful request emits entered+completed for all 11 stages in order"` AND `src/request-pipeline/request-pipeline.test.ts::"rejected stage halts subsequent stage emission"` AND `src/request-pipeline/request-pipeline.test.ts::"skipped stage emits skipped outcome explicitly"`
|
|
|
|
---
|
|
|
|
### Requirement: Default `pipelineEvents` is a zero-cost noop bus
|
|
|
|
When no `pipelineEvents` adapter is configured, the host MUST install a `NoopPipelineEventsBus` whose `emit` and `subscribe` methods are zero-cost (no allocation per call, no listener-list iteration). Production deployments that don't need per-stage observability incur no overhead.
|
|
|
|
#### Scenario: NoopBus emit allocates no objects
|
|
|
|
- **WHEN** a request flows through a host configured with NoopBus
|
|
- **THEN** `emit` returns immediately AND a microbench (`bench:emit-noop`) confirms zero allocation per call
|
|
|
|
#### Scenario: NoopBus subscribe is a no-op returning an unsubscribe noop
|
|
|
|
- **WHEN** a consumer calls `noopBus.subscribe(listener)`
|
|
- **THEN** the listener is never invoked AND the returned unsubscribe function is callable without effect
|
|
|
|
**Verified by:** `src/pipeline-events-bus/noop-bus.test.ts::"emit on noop bus is a no-op"` AND `src/pipeline-events-bus/noop-bus.test.ts::"subscribe on noop bus returns a no-op unsubscribe"` AND `src/pipeline-events-bus/noop-bus.bench.ts::"emit-noop zero-allocation"`
|
|
|
|
---
|
|
|
|
### Requirement: `createTestHost` wires an `InMemoryPipelineEventsBus` by default
|
|
|
|
`@suaveplan/api-host/testing.createTestHost()` MUST default `pipelineEvents` to an `InMemoryPipelineEventsBus` (the canonical fixture). The bus is exposed on `host.adapters.pipelineEvents` for direct assertion in smoke tests.
|
|
|
|
#### Scenario: createTestHost exposes the bus for direct assertion
|
|
|
|
- **WHEN** a test calls `await createTestHost({ ... })`
|
|
- **THEN** `host.adapters.pipelineEvents` is an `InMemoryPipelineEventsBus` instance whose `events` array can be inspected
|
|
|
|
#### Scenario: explicit override is honoured
|
|
|
|
- **WHEN** a test calls `createTestHost({ adapters: { pipelineEvents: customBus } })`
|
|
- **THEN** `host.adapters.pipelineEvents === customBus`
|
|
|
|
**Verified by:** `src/testing/create-test-host/create-test-host.test.ts::"defaults pipelineEvents to InMemoryPipelineEventsBus"` AND `src/testing/create-test-host/create-test-host.test.ts::"explicit pipelineEvents override is honoured"`
|
|
|
|
---
|
|
|
|
### Requirement: Conformance harness `runPipelineEventsConformance`
|
|
|
|
The host SHALL ship `runPipelineEventsConformance(host)` from `@suaveplan/api-host/testing/pipeline-events-conformance`. The harness MUST assert four invariants:
|
|
|
|
1. **Per-stage emit** — every of the 11 stages emits `entered` AND (`completed` OR `rejected` OR `skipped`).
|
|
2. **Order** — stages emit in §6 order.
|
|
3. **Skip** — conditionally-skipped stages emit `skipped` (not silent omission).
|
|
4. **Rejection** — when a stage rejects, no subsequent stage emits `entered`.
|
|
|
|
#### Scenario: harness rejects a host whose pipeline omits a stage
|
|
|
|
- **WHEN** `runPipelineEventsConformance` runs against a stub host whose pipeline skips audit emission silently
|
|
- **THEN** the harness reports the "Per-stage emit" invariant failure pointing to the audit stage
|
|
|
|
**Verified by:** `src/testing/pipeline-events-conformance/pipeline-events-conformance.test.ts::"harness rejects host with missing stage emission"`
|