diff --git a/backend/__tests__/unit/services/pgBootService.test.js b/backend/__tests__/unit/services/pgBootService.test.js index 8fe138bee..ceaa033a5 100644 --- a/backend/__tests__/unit/services/pgBootService.test.js +++ b/backend/__tests__/unit/services/pgBootService.test.js @@ -203,6 +203,42 @@ describe('pgBootService — the route-table probe (TASK-168)', () => { expect(routerIsMounted(appWith(layer(pgMessages)), pgMessages)).toBe(true); }); + // Production runs @sentry/node, which wraps every Express layer handle: the + // wrapper is a new function that exposes the original's properties. Identity + // on the handle then fails on a pod that has the route mounted (deploy + // 4e60f240, 2026-09-27). This builds a real Express app and wraps the layer + // the same way. + const wrapLikeSentry = (app, router) => { + const target = app._router.stack.find((l) => l.handle === router); + const original = target.handle; + const wrapper = function wrapped(...args) { return original.apply(this, args); }; + Object.defineProperty(wrapper, 'stack', { get: () => original.stack }); + target.handle = wrapper; + return target; + }; + + it('finds the mounted router when instrumentation has wrapped its layer handle', () => { + const express = require('express'); + const app = express(); + const pgMessages = express.Router(); + pgMessages.get('/:id', (req, res) => res.sendStatus(401)); + app.use('/api/pg/messages', pgMessages); + const target = wrapLikeSentry(app, pgMessages); + expect(target.handle).not.toBe(pgMessages); + expect(routerIsMounted(app, pgMessages)).toBe(true); + }); + + it('does not mistake a different wrapped router for this one', () => { + const express = require('express'); + const app = express(); + const other = express.Router(); + other.get('/', (req, res) => res.sendStatus(200)); + app.use('/api/pg/status', other); + wrapLikeSentry(app, other); + const pgMessages = express.Router(); + expect(routerIsMounted(app, pgMessages)).toBe(false); + }); + it('returns false when that router is absent, which is the incident', () => { const pgMessages = { name: 'pg-messages' }; const pgStatus = { name: 'pg-status' }; diff --git a/backend/services/pgBootService.ts b/backend/services/pgBootService.ts index 23b1c4be3..85d7afc8d 100644 --- a/backend/services/pgBootService.ts +++ b/backend/services/pgBootService.ts @@ -125,13 +125,24 @@ export const PG_MESSAGE_PATH = '/api/pg/messages'; * IS the thing being mounted, so asking whether that object is in the stack is * the same question with no translation step — and it stays correct if the path * ever changes. + * + * The router's own `stack` array is the identity that survives instrumentation. + * In production `@sentry/node` (10.65) wraps every Express layer handle, so + * `layer.handle === router` is false on a pod that has the route mounted. The + * wrapper exposes the original's properties, so `layer.handle.stack` is still + * the router's stack. 2026-09-27: deploy 4e60f240 hung, because the new pod + * answered /api/pg/messages with 401 while /api/health/ready said "not mounted". + * A local reproduction with Sentry initialised gives identity false and stack + * true; without Sentry, both are true. */ export const routerIsMounted = (app: unknown, router: unknown): boolean => { if (!router) return false; + const routerStack = (router as { stack?: unknown }).stack; const walk = (candidate: any): boolean => { const stack = (candidate && candidate.stack) || []; return stack.some((layer: any) => { if (layer.handle === router) return true; + if (routerStack && layer.handle && layer.handle.stack === routerStack) return true; return Boolean(layer.handle && layer.handle.stack) && walk(layer.handle); }); };