Files
suaveplan/openspec/specs/api-router-in-memory/spec.md
2026-05-27 23:29:53 +01:00

84 lines
3.7 KiB
Markdown

<!-- markdownlint-disable MD024 -->
# Capability Spec: api-router-in-memory
**Package**: `@suaveplan/api-router-in-memory`
**Status**: SHIPPED
## Requirements
### Requirement: Dispatch without a socket
The package SHALL implement `Router` such that match resolution runs the full request pipeline in-process and returns route data without binding any socket, port, or HTTP listener.
#### Scenario: match resolves without network I/O
- **WHEN** a route is registered AND `router.match("GET", "/x")` is called
- **THEN** the result is a route match AND no network socket is opened
**Verified by:** `src/adapter/adapter.test.ts::"dispatch sanity — no network sockets opened by match"`
---
### Requirement: Implements both Router and PipelineRouter contracts
`InMemoryRouter` SHALL satisfy both the abstract `Router` contract from `@suaveplan/api-router` (verb registration, mount, collision detection, param parsing) AND the `PipelineRouter` contract from `@suaveplan/api-host` (`matchPipeline` returning a structurally compatible `PipelineRoute`).
#### Scenario: every Router invariant passes
- **WHEN** `runRouterConformance({ label: "in-memory", router, slot })` runs
- **THEN** every invariant reports passing
#### Scenario: every PipelineRouter invariant passes
- **WHEN** `runPipelineRouterConformance({ label: "...", build })` runs against `matchPipeline`
- **THEN** every invariant reports passing
**Verified by:** `tests/conformance/api-router.test.ts` AND `tests/conformance/api-host.test.ts`
---
### Requirement: Pipeline facet supports hot-reload teardown
`InMemoryRouter` SHALL expose `registerPipelineRoute`, `unregisterPipelineRoute`, `unregisterPipelinePlugin`, `listPipelineRoutes`, and `matchPipeline`. `unregisterPipelinePlugin(plugin)` SHALL remove every route owned by `plugin` in one call. Same-plugin re-registration SHALL overwrite; cross-plugin collision SHALL throw `ROUTER_IN_MEMORY.ROUTE_COLLISION`.
#### Scenario: register/match/list/unregister/unregisterPlugin
- **WHEN** routes for plugins `a` and `b` are registered and `unregisterPipelinePlugin("a")` is called
- **THEN** plugin `b`'s entries remain matchable and plugin `a`'s entries return null
#### Scenario: cross-plugin collision throws ROUTE_COLLISION
- **WHEN** plugin `b` attempts to register a method+path-shape already owned by plugin `a`
- **THEN** `RouterInMemoryError` with code `ROUTER_IN_MEMORY.ROUTE_COLLISION` is thrown
**Verified by:** `src/adapter/adapter.test.ts::"pipeline facet — register/match/list/unregister/unregisterPlugin"` AND `src/pipeline/pipeline.test.ts::"cross-plugin collision throws ROUTE_COLLISION"`
---
### Requirement: Options validated at construction boundary
`InMemoryRouter` SHALL validate constructor options with a Zod schema. Invalid options SHALL throw `RouterInMemoryError` with code `ROUTER_IN_MEMORY.INVALID_PREFIX` carrying the offending value in `context`.
#### Scenario: invalid plugin option throws INVALID_PREFIX
- **WHEN** `new InMemoryRouter({ plugin: "bad/plugin" })` is called
- **THEN** `RouterInMemoryError(ROUTER_IN_MEMORY.INVALID_PREFIX, ...)` is thrown
**Verified by:** `src/adapter/adapter.test.ts::"invalid plugin option throws INVALID_PREFIX"`
---
### Requirement: Test harness for downstream consumers
The package SHALL expose `createInMemoryRouterHarness()` returning a fresh `InMemoryRouter` and a pinned `InMemoryBlueGreenSlot` factory, replacing the hand-rolled router shims across foundation + plugin tests.
#### Scenario: harness returns a fresh router + slot pair
- **WHEN** `createInMemoryRouterHarness()` is called twice
- **THEN** each call yields a distinct router AND distinct slot instances
**Verified by:** `src/testing/testing.test.ts::"slot factory produces fresh InMemoryBlueGreenSlot instances"`
<!-- markdownlint-enable MD024 -->