Skip to content

Commit 951714a

Browse files
committed
test(runtime): measure inbound HTTP-counter coverage and both candidate seams (#9650)
Measurement only, no fix: the seam choice is a design fork and is being escalated rather than picked. - section 1 pins what http_requests_total observes on the current wiring: the dispatcher's own routes yes, the auth getRawApp() mount no, the REST data route mounted through the real RouteManager no. - section 2 measures seam A (register the instrumented proxy back as the http.server service): getRawApp passes through unwrapped, only get/post/delete are trapped, registerService refuses a second registration, and a consumer resolving in an earlier start() keeps the raw handle. - section 3 measures the raw-app middleware seam at three install points and shows the framework-agnostic use() seam cannot observe status. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4d6ba8b commit 951714a

1 file changed

Lines changed: 184 additions & 48 deletions

File tree

‎packages/runtime/src/http-metrics-inbound-coverage.hono.integration.test.ts‎

Lines changed: 184 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ import { createDispatcherPlugin } from './dispatcher-plugin.js';
4242
* (`rest-server.ts:834` constructs it; `route-manager.ts:191-213` calls
4343
* `server.get/post/put/delete/patch`). Modelling the mount would have been
4444
* weaker: this is the production registrar itself.
45+
*
46+
* ## What a fix has to invert
47+
*
48+
* The two §1 assertions spelled "does NOT count" are this card's acceptance
49+
* criterion in executable form. Whichever seam is chosen, BOTH have to flip
50+
* together — a change that flips one and leaves the other reproduces the
51+
* defect one surface over, which is the thing this card exists about.
4552
*/
4653

4754
const AUTH_BASE = '/api/v1/auth';
@@ -212,97 +219,226 @@ describe('#9650 §2 — SEAM A: registering the instrumented proxy back as `http
212219
expect(calls).toContain('patch:/q');
213220
});
214221

222+
it('MEASURED: `registerService` REFUSES a second `http.server`; the seam needs `replaceService`', async () => {
223+
const kernel = new LiteKernel();
224+
let registerError: string | undefined;
225+
kernel.use({
226+
name: 'com.objectstack.test.provider',
227+
version: '1.0.0',
228+
providesServices: ['http.server'],
229+
init: async (ctx: PluginContext) => { ctx.registerService('http.server', { id: 'raw' }); },
230+
} as Plugin);
231+
kernel.use({
232+
name: 'com.objectstack.test.reregistrar',
233+
version: '1.0.0',
234+
init: async () => {},
235+
start: async (ctx: PluginContext) => {
236+
try {
237+
ctx.registerService('http.server', { id: 'proxy' });
238+
} catch (err: any) {
239+
registerError = String(err?.message ?? err);
240+
}
241+
},
242+
} as Plugin);
243+
await kernel.bootstrap();
244+
await kernel.shutdown();
245+
246+
// Not a detail: "register the proxy back as the service" cannot be
247+
// spelled with registerService at all (kernel-base.ts:79-81).
248+
expect(registerError).toContain("already registered");
249+
});
250+
215251
it('MEASURED: a consumer that resolved `http.server` in an EARLIER start() keeps the raw handle', async () => {
216252
const resolved: string[] = [];
217-
const kernel = new LiteKernel();
218253
const raw = { id: 'raw' };
219-
const providerPlugin: Plugin = {
254+
const kernel = new LiteKernel();
255+
kernel.use({
220256
name: 'com.objectstack.test.provider',
221257
version: '1.0.0',
222258
providesServices: ['http.server'],
223259
init: async (ctx: PluginContext) => { ctx.registerService('http.server', raw); },
224-
};
225-
const earlyConsumer: Plugin = {
260+
} as Plugin);
261+
kernel.use({
226262
name: 'com.objectstack.test.early-consumer',
227263
version: '1.0.0',
228264
init: async () => {},
229265
start: async (ctx: PluginContext) => {
230-
resolved.push((ctx.getService<any>('http.server') as any).id ?? 'proxy');
266+
resolved.push(ctx.getService<any>('http.server').id);
231267
},
232-
};
233-
const lateRegistrar: Plugin = {
268+
} as Plugin);
269+
kernel.use({
234270
name: 'com.objectstack.test.late-registrar',
235271
version: '1.0.0',
236272
init: async () => {},
237273
start: async (ctx: PluginContext) => {
238274
const proxy = new Proxy(raw, {
239275
get: (t, p, r) => (p === 'id' ? 'proxy' : Reflect.get(t, p, r)),
240276
});
241-
ctx.registerService('http.server', proxy as any);
277+
ctx.replaceService('http.server', proxy);
242278
},
243-
};
244-
const lateConsumer: Plugin = {
279+
} as Plugin);
280+
kernel.use({
245281
name: 'com.objectstack.test.late-consumer',
246282
version: '1.0.0',
247283
init: async () => {},
248284
start: async (ctx: PluginContext) => {
249-
resolved.push((ctx.getService<any>('http.server') as any).id ?? 'proxy');
285+
resolved.push(ctx.getService<any>('http.server').id);
250286
},
251-
};
252-
kernel.use(providerPlugin);
253-
kernel.use(earlyConsumer);
254-
kernel.use(lateRegistrar);
255-
kernel.use(lateConsumer);
287+
} as Plugin);
256288
await kernel.bootstrap();
257289
await kernel.shutdown();
258290

259-
// This is ruling ②'s condition, measured: correctness of seam A is a
260-
// function of Phase-2 position, and in the shipped composition REST
261-
// (serve.ts:2548) starts BEFORE the dispatcher (serve.ts:2566).
291+
// Ruling ②'s condition, measured: seam A's correctness is a function of
292+
// Phase-2 position. In the shipped composition REST (serve.ts:2548)
293+
// starts BEFORE the dispatcher (serve.ts:2566), i.e. it is the 'raw'
294+
// row here — the unfavourable one.
262295
expect(resolved).toEqual(['raw', 'proxy']);
263296
});
264297
});
265298

266299
describe('#9650 §3 — SEAM B/C: a raw-app Hono middleware, and when it is installed', () => {
267-
it('MEASURED: a middleware installed AFTER a route does not observe that route (seam B)', async () => {
268-
const { Hono } = await import('hono');
269-
const app = new Hono();
300+
/**
301+
* Seam B — installed during the dispatcher's `start()`, i.e. AFTER the
302+
* plugins that mount auth and REST have already run their own `start()`.
303+
* Measured on the REAL booted app, not a synthetic Hono instance.
304+
*/
305+
it('MEASURED: a middleware installed AFTER the routes does not observe them (seam B)', async () => {
306+
const metrics = new InMemoryMetricsRegistry();
307+
const { kernel, baseUrl } = await bootMeasurementKernel(metrics);
308+
const httpServer = kernel.getService<IHttpServer>('http.server');
309+
const rawApp = (httpServer as unknown as { getRawApp(): any }).getRawApp();
310+
270311
const seen: string[] = [];
271-
app.get('/early', (c: any) => c.json({ ok: true }));
272-
app.use('*', async (c: any, next: any) => {
312+
// Registered now = after every plugin's start() mounted its routes.
313+
rawApp.use('*', async (c: any, next: any) => {
273314
await next();
274315
seen.push(c.req.path);
275316
});
276-
app.get('/late', (c: any) => c.json({ ok: true }));
317+
// A route mounted after the middleware, as the positive control.
318+
rawApp.get('/late-probe', (c: any) => c.json({ ok: true }));
277319

278-
await app.request('/early');
279-
await app.request('/late');
320+
await fetch(`${baseUrl}${AUTH_PROBE}`, { method: 'POST' });
321+
await fetch(`${baseUrl}${REST_PROBE}`);
322+
await fetch(`${baseUrl}${DISPATCHER_PROBE}`);
323+
await fetch(`${baseUrl}/late-probe`);
324+
await kernel.shutdown();
280325

281-
// Hono composes the handlers that matched in REGISTRATION order; the
326+
// Hono composes the handlers that matched in REGISTRATION order, so a
282327
// route registered first answers and never calls next().
283-
expect(seen).toEqual(['/late']);
284-
});
328+
expect(seen).toContain('/late-probe');
329+
expect(seen).not.toContain(AUTH_PROBE);
330+
expect(seen).not.toContain(REST_PROBE);
331+
expect(seen).not.toContain(DISPATCHER_PROBE);
332+
}, 30_000);
285333

286-
it('MEASURED: a middleware installed BEFORE every route observes all of them, incl. status (seam C)', async () => {
287-
const { Hono } = await import('hono');
288-
const app = new Hono();
334+
/**
335+
* Seam C — installed during Phase 1 (`init()`), before any route exists,
336+
* which is exactly where the adapter puts its own `installMiddlewareSeam()`.
337+
*/
338+
it('MEASURED: a middleware installed in Phase 1 observes BOTH mounts, with status (seam C)', async () => {
289339
const seen: Array<{ route: string; status: number }> = [];
290-
app.use('*', async (c: any, next: any) => {
291-
await next();
292-
seen.push({ route: c.req.routePath, status: c.res.status });
293-
});
294-
// Both mount styles, on the one app, after the middleware.
295-
app.all(`${AUTH_BASE}/*`, (c: any) => c.json({ ok: true }, 200));
296-
app.get(REST_PROBE, (c: any) => c.json({ ok: true }, 201));
340+
const probePlugin: Plugin = {
341+
name: 'com.objectstack.test.phase1-observer',
342+
version: '1.0.0',
343+
requiresServices: ['http.server'],
344+
init: async (ctx: PluginContext) => {
345+
const httpServer = ctx.getService<IHttpServer>('http.server');
346+
const rawApp = (httpServer as unknown as { getRawApp(): any }).getRawApp();
347+
rawApp.use('*', async (c: any, next: any) => {
348+
await next();
349+
seen.push({ route: c.req.routePath, status: c.res.status });
350+
});
351+
},
352+
};
297353

298-
await app.request(AUTH_PROBE, { method: 'POST' });
299-
await app.request(REST_PROBE);
354+
const kernel = new LiteKernel();
355+
kernel.use(new HonoServerPlugin({ port: 0, cors: false }));
356+
kernel.use(probePlugin);
357+
kernel.use(authLikePlugin());
358+
kernel.use(restLikePlugin());
359+
kernel.use(
360+
createDispatcherPlugin({
361+
prefix: '/api/v1',
362+
securityHeaders: false,
363+
observability: { metrics: new InMemoryMetricsRegistry() },
364+
} as any),
365+
);
366+
await kernel.bootstrap();
367+
const httpServer = kernel.getService<IHttpServer>('http.server');
368+
const baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
300369

301-
expect(seen).toEqual([
302-
{ route: `${AUTH_BASE}/*`, status: 200 },
303-
{ route: REST_PROBE, status: 201 },
304-
]);
305-
});
370+
await fetch(`${baseUrl}${AUTH_PROBE}`, { method: 'POST' });
371+
await fetch(`${baseUrl}${REST_PROBE}`);
372+
await fetch(`${baseUrl}${DISPATCHER_PROBE}`);
373+
await kernel.shutdown();
374+
375+
const routes = seen.map((s) => s.route);
376+
expect(routes).toContain(`${AUTH_BASE}/*`);
377+
expect(routes).toContain(REST_PROBE);
378+
expect(routes).toContain(DISPATCHER_PROBE);
379+
// The status label the counter needs is readable here, which is what
380+
// the framework-agnostic `use()` seam cannot offer.
381+
expect(seen.every((s) => typeof s.status === 'number')).toBe(true);
382+
}, 30_000);
383+
384+
/**
385+
* The exact boundary `serve.ts:3429-3435` claims is sufficient. That
386+
* comment governs how the unknown-hostname guard (and anything copied
387+
* from it) is installed, so the claim is measured rather than trusted:
388+
*
389+
* "Hono's `app.use('*')` is order-independent for matching, so as long
390+
* as the middleware is added before kernel:listening fires, it
391+
* intercepts every request regardless of which plugin registered its
392+
* handler."
393+
*
394+
* Every route in the platform is mounted in a plugin's `start()`, i.e.
395+
* in Phase 2 — strictly BEFORE `kernel:bootstrapped` and `kernel:listening`.
396+
*/
397+
it('MEASURED: a middleware installed at `kernel:bootstrapped` — before kernel:listening — still observes nothing', async () => {
398+
const seen: string[] = [];
399+
const lateObserver: Plugin = {
400+
name: 'com.objectstack.test.late-hook-observer',
401+
version: '1.0.0',
402+
requiresServices: ['http.server'],
403+
init: async (ctx: PluginContext) => {
404+
const httpServer = ctx.getService<IHttpServer>('http.server');
405+
const rawApp = (httpServer as unknown as { getRawApp(): any }).getRawApp();
406+
// Subscribed in init(), but the middleware is INSTALLED from
407+
// the hook — after every plugin's start() has mounted routes,
408+
// and still before kernel:listening.
409+
(ctx as any).hook('kernel:bootstrapped', async () => {
410+
rawApp.use('*', async (c: any, next: any) => {
411+
await next();
412+
seen.push(c.req.path);
413+
});
414+
});
415+
},
416+
};
417+
418+
const kernel = new LiteKernel();
419+
kernel.use(new HonoServerPlugin({ port: 0, cors: false }));
420+
kernel.use(lateObserver);
421+
kernel.use(authLikePlugin());
422+
kernel.use(restLikePlugin());
423+
kernel.use(
424+
createDispatcherPlugin({
425+
prefix: '/api/v1',
426+
securityHeaders: false,
427+
observability: { metrics: new InMemoryMetricsRegistry() },
428+
} as any),
429+
);
430+
await kernel.bootstrap();
431+
const httpServer = kernel.getService<IHttpServer>('http.server');
432+
const baseUrl = `http://127.0.0.1:${httpServer.getPort!()}`;
433+
434+
await fetch(`${baseUrl}${AUTH_PROBE}`, { method: 'POST' });
435+
await fetch(`${baseUrl}${REST_PROBE}`);
436+
await fetch(`${baseUrl}${DISPATCHER_PROBE}`);
437+
await kernel.shutdown();
438+
439+
// "Before kernel:listening" is NOT sufficient — Phase 1 is.
440+
expect(seen).toEqual([]);
441+
}, 30_000);
306442

307443
it('MEASURED: the IHttpServer `use()` seam cannot observe status — it runs BEFORE dispatch', async () => {
308444
const metrics = new InMemoryMetricsRegistry();
@@ -323,5 +459,5 @@ describe('#9650 §3 — SEAM B/C: a raw-app Hono middleware, and when it is inst
323459
// order-independent seam cannot carry a `{status}` counter.
324460
expect(observed[0]).not.toHaveProperty('status');
325461
expect(observed[0].body).toBeUndefined();
326-
});
462+
}, 30_000);
327463
});

0 commit comments

Comments
 (0)