# 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"`