3.7 KiB
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 againstmatchPipeline - 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
aandbare registered andunregisterPipelinePlugin("a")is called - THEN plugin
b's entries remain matchable and plugina's entries return null
Scenario: cross-plugin collision throws ROUTE_COLLISION
- WHEN plugin
battempts to register a method+path-shape already owned by plugina - THEN
RouterInMemoryErrorwith codeROUTER_IN_MEMORY.ROUTE_COLLISIONis 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"