test(display): run the real display controller against the real viewer routes

Every display test answered the controller with a hand-written stub, and
every viewer test called the service with a hand-written object, so
neither side was ever tested against the other. In production every
heartbeat was refused with a 400.

This suite answers the real controller's requests with the real viewer
route handlers over the in-memory store. The routes gain an optional
store factory for that; production still uses the module database.

Fails today: POST /viewers/:viewerId/heartbeat -> 400
SIGNAGE_INVALID_REQUEST, because the display sends
reportedContributionId: null and the route accepts only a missing field.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-05 21:25:21 -05:00
co-authored by Claude Opus 5.5
parent 914034dda0
commit 458043280a
2 changed files with 229 additions and 3 deletions
+6 -3
View File
@@ -23,9 +23,11 @@ import {
const OPAQUE_ID = /^[\w][\w.:-]{0,199}$/u;
function store(context: ModuleRouteHandlerContext): SignageStore {
return createSignageStore(context.services.database);
}
/** How a route reaches Signage's tables. Production uses the module database; the display contract
* test supplies the in-memory store so the real handlers can answer the real display controller. */
export type ViewerRouteStore = (context: ModuleRouteHandlerContext) => SignageStore;
const databaseStore: ViewerRouteStore = (context) => createSignageStore(context.services.database);
/**
* The safe Core device identifier, when the caller is a paired display.
@@ -57,6 +59,7 @@ export function registerViewerRoutes(
http: ModuleHttpRouteRegistrar,
playback: () => SignagePlaybackService,
registry: () => SignageProviderRegistry,
store: ViewerRouteStore = databaseStore,
): void {
http.route({
method: "POST",