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

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