From 443b3d1a5db060af5de0eb0fffb9697756962c09 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 13:56:05 +0000 Subject: [PATCH 1/2] test(rest): measure identity resolution at GET /ui/view/:object/:type, at the seam and downstream Measurement only -- nothing is repaired. Drives the route on the real registerRoutes() table under an absent and an entitled execution context, records the resolveExecCtx call count, and continues past the seam into the real getUiView producer, which #13214 marks unmeasured. Every reading stands next to a positive control on the same instrument. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k --- ...ui-view-route-identity.measurement.test.ts | 536 ++++++++++++++++++ 1 file changed, 536 insertions(+) create mode 100644 packages/rest/src/ui-view-route-identity.measurement.test.ts diff --git a/packages/rest/src/ui-view-route-identity.measurement.test.ts b/packages/rest/src/ui-view-route-identity.measurement.test.ts new file mode 100644 index 0000000000..23b0b4fabf --- /dev/null +++ b/packages/rest/src/ui-view-route-identity.measurement.test.ts @@ -0,0 +1,536 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#13214] What `GET /api/v1/ui/view/:object/:type` decides about the caller — + * at the REST seam AND downstream in `getUiView`. + * + * ## What this file is + * + * ⛔ A MEASUREMENT file. It repairs nothing, gates nothing and proposes + * nothing. Access-control behaviour is a human floor in this repo: if a + * reading below is a problem, the repair is a card of its own with a human + * decision on it. What is committed here is the instrument and its readings, + * so the next person does not have to re-derive them from grep. + * + * ## The three questions, and why the second is the big one + * + * #13160 / PR #13213's census drove every `resolveExecCtx` consumer in + * `rest-server.ts` and found 52 of 52 bare sites refusing an absent context + * with 401. This route surfaced there precisely because it is NOT a consumer — + * the one metadata-touching route in the table that resolves no identity at + * all. That census stopped at the seam and said so. This file continues past + * it: + * + * 1. §1-§2 — the seam, reproduced INDEPENDENTLY of #13213 (its own harness, + * its own instrument), plus the exact argument object the seam hands the + * producer. + * 2. §3-§4 — ⭐ the half #13214 marks UNMEASURED: does `getUiView` apply + * authorization of its own? Driven against the REAL + * `ObjectStackProtocolImplementation`, not read off a grep. + * 3. §5 — `isAuthGateAllowlisted` does not name a `/ui` path, verified by + * driving it rather than by reading the array. + * 4. §6 — ⭐ the RATCHET reach. `authz-conformance.matrix.ts` carries no row + * for this route while its header says a new ungated route is + * UNCLASSIFIED and breaks CI. §6 measures the package-local half of why: + * how many routes `RestServer` mounts, and how they distribute over its + * registrars — only ONE of which (`registerMetadataEndpoints`) the + * ratchet's curated probe table names at all. + * + * ## ⚠️ Every "it refused" reading here stands next to a positive control + * + * An instrument that can only produce one answer has measured itself. That cuts + * BOTH ways on this card and the second direction is the one that is easy to + * get wrong: a 200 from a harness that could never have shown a refusal is not + * evidence of a fail-open either. So each section that reports "served + * identically" also drives a rival wiring on the SAME instrument that DOES + * refuse, and each section that reports a refusal also drives a serve. + * + * ## ⚠️ `@objectstack/metadata-protocol` resolves to `dist/` here + * + * This package's vitest config aliases `plugin-hono-server` and + * `service-datasource` to source; `metadata-protocol` is deliberately NOT + * aliased (it is registered in `KNOWN_UNALIASED_TEST_IMPORTS` for + * `@objectstack/rest`), so §3/§4 read the BUILT artifact. That is stated rather + * than assumed: a reading about the producer is only as current as the `dist/` + * it ran against, and §3 asserts a shape it would notice a stale build on (the + * `object` key #5948 relocated to the container). + */ + +import { describe, it, expect, vi } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { + ANONYMOUS_DENY_CODE, + ANONYMOUS_DENY_STATUS, + isAuthGateAllowlisted, +} from '@objectstack/core'; +import { ObjectStackProtocolImplementation } from '@objectstack/metadata-protocol'; +import { RestServer } from './rest-server.js'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const SOURCE = readFileSync(resolve(HERE, 'rest-server.ts'), 'utf8'); + +const BASE = '/api/v1'; +const UI_ROUTE = `GET ${BASE}/ui/view/:object/:type`; +const DATA_ROUTE = `GET ${BASE}/data/:object`; + +type Handler = (req: any, res: any) => any; + +// --------------------------------------------------------------------------- +// Harness — same shape as `execctx-consumer-census.test.ts`, rebuilt here so +// this file's readings do not inherit that file's fixtures (#13214 asks for an +// INDEPENDENT reproduction, not a citation). +// --------------------------------------------------------------------------- + +function recordingServer() { + const table = new Map(); + const on = (m: string) => (p: string, h: Handler) => { table.set(`${m} ${p}`, h); }; + return { + table, + get: on('GET'), post: on('POST'), put: on('PUT'), delete: on('DELETE'), patch: on('PATCH'), + use: () => {}, listen: async () => {}, close: async () => {}, + } as any; +} + +function anyService() { + return new Proxy({}, { + get: (_t, k: string) => (k === 'then' || k === 'constructor') + ? undefined + : vi.fn(async () => ({ ok: true, rows: [], data: [], total: 0 })), + }); +} + +function makeServer(protocol: any) { + const server = recordingServer(); + const provider = async () => anyService(); + const rs: any = new RestServer( + server, protocol as any, {} as any, undefined, undefined, () => 'env_13214', + provider, provider, provider, provider, provider, provider, provider, + provider, provider, provider, provider, provider, provider, provider, + provider, provider, provider, () => true, provider, undefined, provider, + ); + return { rs, table: server.table as Map }; +} + +/** + * Replace `resolveExecCtx` with the leg's value and COUNT the calls. The count + * is the observable this card turns on: "answers 200 either way" is compatible + * with a route that resolved identity and found it sufficient; "answers 200 + * either way AND never asked" is not. + */ +function instrument(ctxValue: any) { + const proto: any = (RestServer as any).prototype; + const original = proto.resolveExecCtx; + const calls = { n: 0 }; + proto.resolveExecCtx = async function () { + calls.n++; + return ctxValue === undefined ? undefined : { ...ctxValue }; + }; + return { calls, restore: () => { proto.resolveExecCtx = original; } }; +} + +interface Observed { status: number; code: unknown; body: unknown; threw?: string } + +async function call(handler: Handler, method: string, pattern: string, params: any): Promise { + let status = 0; let body: any; let sent = false; + const res: any = { + status(c: number) { status = c; return res; }, + json(b: any) { body = b; sent = true; }, + send() { sent = true; }, header() { return res; }, setHeader() { return res; }, + end() { sent = true; }, write() { return true; }, type() { return res; }, + }; + let threw: string | undefined; + try { + await handler({ + params, query: {}, body: {}, method, path: pattern, + headers: { host: 'measure.test' }, url: pattern, + } as any, res); + } catch (e: any) { threw = String(e?.message ?? e).slice(0, 160); } + return { status: status || (sent ? 200 : 0), code: body?.code ?? body?.error?.code ?? body?.error, body, threw }; +} + +const ENTITLED = { + userId: 'u_13214', + isSystem: false, + tenantId: 'org_13214', + systemPermissions: ['manage_metadata', 'studio.access', 'setup.access'], +}; + +/** Mount the REAL route table and drive one route under one wiring. */ +async function driveRoute( + route: string, + ctxValue: any, + protocol: any, + params: Record, +): Promise { + const probe = instrument(ctxValue); + const { rs, table } = makeServer(protocol); + rs.registerRoutes(); + const handler = table.get(route); + if (!handler) { probe.restore(); throw new Error(`route not mounted: ${route}`); } + const [method, pattern] = route.split(' '); + const observed = await call(handler, method, pattern, params); + const execCtxCalls = probe.calls.n; + probe.restore(); + return { ...observed, execCtxCalls }; +} + +const UI_PARAMS = { object: 'account', type: 'list' }; + +// The object the REAL producer is fed in §3/§4. `secret` is `hidden`, which the +// producer DOES drop — a declaration-driven omission, never an identity-driven +// one, and §3 turns on exactly that distinction. +const ACCOUNT_SCHEMA = { + name: 'account', + label: 'Account', + fields: { + id: { name: 'id', type: 'text' }, + name: { name: 'name', type: 'text', label: 'Name', required: true }, + status: { name: 'status', type: 'text', label: 'Status' }, + salary: { name: 'salary', type: 'number', label: 'Salary' }, + secret: { name: 'secret', type: 'text', hidden: true }, + created_at: { name: 'created_at', type: 'datetime' }, + }, +}; + +/** A protocol carrying the REAL producer behind this route. */ +function realProtocol(schema: unknown = ACCOUNT_SCHEMA) { + // ⚠️ Callers wanting "no such object" must pass `null`, never `undefined`: + // `undefined` re-triggers this default and hands back the account schema, + // which read as the producer serving an unregistered object (it does not). + const engine = { registry: { getObject: () => schema } }; + return new ObjectStackProtocolImplementation(engine as any) as any; +} + +/** A protocol that records what the seam hands `getUiView`. */ +function recordingProtocol() { + const seen: any[] = []; + return { + seen, + protocol: { + getUiView: vi.fn(async (request: any) => { seen.push(request); return { object: request.object, list: { type: 'grid', label: 'x', columns: [] } }; }), + } as any, + }; +} + +/** + * ⭐ The rival wiring. A producer that DOES gate: it refuses unless it is + * handed an identity. Nothing in the seam's contract prevents such a producer + * from existing, and if one were installed this instrument would report its + * refusal — which is what makes §3's "served identically" a reading about the + * shipped producer rather than about this harness. + */ +function gatingProtocol() { + return { + getUiView: vi.fn(async (request: any) => { + const identity = request?.context?.userId ?? request?.userId ?? request?.executionContext?.userId; + if (!identity) { + const err: any = new Error('gated: no identity reached the producer'); + err.status = 403; + err.code = 'PERMISSION_DENIED'; + throw err; + } + return { object: request.object, list: { type: 'grid', label: 'x', columns: [] } }; + }), + } as any; +} + +// --------------------------------------------------------------------------- +// 1. The seam, driven — independently reproduced +// --------------------------------------------------------------------------- + +describe('[#13214] §1 the REST seam — absent context vs entitled context', () => { + it('answers 200 under BOTH, with byte-identical bodies, having asked for identity ZERO times', async () => { + const absent = await driveRoute(UI_ROUTE, undefined, realProtocol(), UI_PARAMS); + const entitled = await driveRoute(UI_ROUTE, ENTITLED, realProtocol(), UI_PARAMS); + + expect(absent.status).toBe(200); + expect(entitled.status).toBe(200); + // Not merely "both 200": the same bytes. A route that resolved identity + // and narrowed on it would answer 200 twice with DIFFERENT bodies. + expect(JSON.stringify(absent.body)).toBe(JSON.stringify(entitled.body)); + + // ⭐ The third, independent observation. `resolveExecCtx` is patched on + // the prototype for the WHOLE server, so this counts every site the + // driven request reached — not just this handler's. + expect(absent.execCtxCalls).toBe(0); + expect(entitled.execCtxCalls).toBe(0); + }, 120_000); + + it('⭐ POSITIVE CONTROL — the same instrument, same boot, DOES refuse an absent context on a sibling route', async () => { + // Without this, "200 under an absent context" could be a harness that + // cannot express a refusal at all. `GET /data/:object` is mounted by + // the very next registrar in `registerRoutes` and carries the shared + // floor. + const absent = await driveRoute(DATA_ROUTE, undefined, realProtocol(), { object: 'account' }); + expect(absent.status).toBe(ANONYMOUS_DENY_STATUS); + expect(absent.code).toBe(ANONYMOUS_DENY_CODE); + expect(absent.execCtxCalls).toBeGreaterThan(0); + + // ...and it can also SERVE, so the 401 above is a decision and not a + // stuck needle. + const entitled = await driveRoute(DATA_ROUTE, ENTITLED, realProtocol(), { object: 'account' }); + expect(entitled.status).not.toBe(ANONYMOUS_DENY_STATUS); + expect(entitled.execCtxCalls).toBeGreaterThan(0); + }, 120_000); + + it('the handler calls `resolveProtocol` and NOT `enforceAuth` — read off the mounted registrar, not off a grep of the file', () => { + // The source assertion is scoped to the registrar body so it cannot be + // satisfied by a neighbour's guard, which is the trap the card warns + // about: `enforceAuth` IS present in this file (52 times over) and + // `registerUiEndpoints` sits directly above `registerCrudEndpoints`, + // whose handlers all carry it. + const start = SOURCE.indexOf('private registerUiEndpoints('); + const end = SOURCE.indexOf('private registerCrudEndpoints('); + expect(start).toBeGreaterThan(0); + expect(end).toBeGreaterThan(start); + const body = SOURCE.slice(start, end); + + expect(body).toContain('this.resolveProtocol('); + expect(body).toContain('p.getUiView('); + // ⛔ Reverse-checked zeros: the same two terms are counted over the + // WHOLE file below, so a zero here is "absent from this registrar", + // never "misspelled". + expect(body.includes('this.enforceAuth(')).toBe(false); + expect(body.includes('this.resolveExecCtx(')).toBe(false); + expect(SOURCE.split('this.enforceAuth(').length - 1).toBeGreaterThan(40); + expect(SOURCE.split('this.resolveExecCtx(').length - 1).toBeGreaterThan(40); + }); +}); + +// --------------------------------------------------------------------------- +// 2. What the seam hands the producer +// --------------------------------------------------------------------------- + +describe('[#13214] §2 the argument object — the producer cannot gate on what it is never told', () => { + it('the unscoped mount passes EXACTLY `{ object, type }` — no identity-bearing key of any spelling', async () => { + const rec = recordingProtocol(); + const observed = await driveRoute(UI_ROUTE, ENTITLED, rec.protocol, UI_PARAMS); + expect(observed.status).toBe(200); + expect(rec.seen.length).toBe(1); + + const arg = rec.seen[0]; + expect(Object.keys(arg).sort()).toEqual(['object', 'type']); + expect(arg).toEqual({ object: 'account', type: 'list' }); + + // Spelled out because "no identity" is the claim, and a claim about an + // ABSENCE is worth naming the candidates for. Driven with an ENTITLED + // context in scope, so this is not "there was no identity to pass". + for (const key of ['context', 'executionContext', 'ctx', 'userId', 'user', 'tenantId', 'organizationId', 'principal', 'req', 'request']) { + expect(arg[key], `argument carried \`${key}\``).toBeUndefined(); + } + }, 120_000); + + it('⭐ POSITIVE CONTROL — the recorder DOES capture a key when one is present', async () => { + // The assertion above is `Object.keys(...) === ['object','type']`. If + // the recorder silently dropped keys, that would pass no matter what + // the seam sent. Driving the SAME recorder directly with a richer + // argument shows it does not. + const rec = recordingProtocol(); + await rec.protocol.getUiView({ object: 'account', type: 'list', context: { userId: 'u' } }); + expect(Object.keys(rec.seen[0]).sort()).toEqual(['context', 'object', 'type']); + expect(rec.seen[0].context.userId).toBe('u'); + }); +}); + +// --------------------------------------------------------------------------- +// 3. ⭐ The question #13214 marks UNMEASURED — does the producer gate? +// --------------------------------------------------------------------------- + +describe('[#13214] §3 downstream — the REAL `getUiView`, driven', () => { + it('returns the SAME view to an absent and an entitled caller, field for field', async () => { + const absent = await driveRoute(UI_ROUTE, undefined, realProtocol(), UI_PARAMS); + const entitled = await driveRoute(UI_ROUTE, ENTITLED, realProtocol(), UI_PARAMS); + expect(absent.status).toBe(200); + expect(entitled.status).toBe(200); + + const columns = (b: any) => (b.list.columns as any[]).map((c) => c.field).sort(); + expect(columns(absent.body)).toEqual(columns(entitled.body)); + + // Freshness of the built artifact this reads (see the header note): + // #5948 relocated `object` onto the CONTAINER. A `dist/` from before + // that would put it on `list` instead and this would fail loudly rather + // than reporting a stale producer's behaviour as current. + expect((absent.body as any).object).toBe('account'); + expect((absent.body as any).list.object).toBeUndefined(); + }, 120_000); + + it('⚠️ the one field it DOES drop is dropped by DECLARATION, not by caller — `hidden` goes for everyone, `salary` stays for everyone', async () => { + // This is the distinction the whole question turns on. An FLS-style + // narrowing would differ BETWEEN the two callers. This narrowing is + // identical for both, and keyed on a property of the schema. + const absent = await driveRoute(UI_ROUTE, undefined, realProtocol(), UI_PARAMS); + const entitled = await driveRoute(UI_ROUTE, ENTITLED, realProtocol(), UI_PARAMS); + const cols = (b: any) => (b.list.columns as any[]).map((c) => c.field); + + expect(cols(absent.body)).not.toContain('secret'); + expect(cols(entitled.body)).not.toContain('secret'); + expect(cols(absent.body)).toContain('salary'); + expect(cols(entitled.body)).toContain('salary'); + }, 120_000); + + it('the form branch behaves the same way — this is not a list-only reading', async () => { + const absent = await driveRoute(UI_ROUTE, undefined, realProtocol(), { object: 'account', type: 'form' }); + const entitled = await driveRoute(UI_ROUTE, ENTITLED, realProtocol(), { object: 'account', type: 'form' }); + expect(absent.status).toBe(200); + expect(JSON.stringify(absent.body)).toBe(JSON.stringify(entitled.body)); + }, 120_000); + + it('⭐ POSITIVE CONTROL A — the instrument REPORTS a downstream refusal when the producer makes one', async () => { + // The rival wiring. Same route, same boot, same driver: a producer that + // gates is visible as a refusal. So §3's "served identically" is a + // reading about the SHIPPED producer, not a property of this harness. + const absent = await driveRoute(UI_ROUTE, undefined, gatingProtocol(), UI_PARAMS); + expect(absent.status).toBe(403); + expect(absent.code).toBe('PERMISSION_DENIED'); + }, 120_000); + + it('⭐ POSITIVE CONTROL B — the REAL producer is reachable and CAN answer something other than 200', async () => { + // Guards the other way a green could be vacuous: a producer never + // actually invoked. An object absent from the registry makes the real + // implementation throw, and the seam converts that into a non-200. + const missing = await driveRoute(UI_ROUTE, ENTITLED, realProtocol(null), UI_PARAMS); + expect(missing.status).not.toBe(200); + }, 120_000); +}); + +// --------------------------------------------------------------------------- +// 4. Why the producer could not gate even if it wanted to +// --------------------------------------------------------------------------- + +describe('[#13214] §4 the producer instance is not per-request', () => { + it('`resolveProtocol` hands the SAME object to two different requests, so it can hold no per-caller identity', async () => { + const protocol = realProtocol(); + const { rs } = makeServer(protocol); + const reqA = { headers: { host: 'a.test' }, params: {} }; + const reqB = { headers: { host: 'b.test' }, params: {} }; + const a = await (rs as any).resolveProtocol(undefined, reqA); + const b = await (rs as any).resolveProtocol(undefined, reqB); + expect(a).toBe(b); + expect(a).toBe(protocol); + }); + + it('and the shipped `getUiView` declares ONE parameter — the request record §2 measured', () => { + const protocol = realProtocol(); + expect(typeof protocol.getUiView).toBe('function'); + expect(protocol.getUiView.length).toBe(1); + }); +}); + +// --------------------------------------------------------------------------- +// 5. The explanation that is NOT the explanation +// --------------------------------------------------------------------------- + +describe('[#13214] §5 `isAuthGateAllowlisted` does not name a `/ui` path', () => { + it('every spelling of this route reads as NOT allow-listed', () => { + for (const path of [ + '/api/v1/ui/view/account/list', + '/api/v1/ui/view/account/form', + '/api/v1/environments/env_1/ui/view/account/list', + '/api/v1/ui/view/account/list?x=1', + ]) { + expect(isAuthGateAllowlisted(path), path).toBe(false); + } + }); + + it('⭐ POSITIVE CONTROL — the same predicate DOES allow-list the control-plane paths, so `false` above is a decision', () => { + // ⛔ Without this, a predicate that returned `false` for everything + // (a rename, a broken import) would read exactly like the finding. + expect(isAuthGateAllowlisted('/api/v1/auth/sign-in')).toBe(true); + expect(isAuthGateAllowlisted(undefined)).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// 6. ⭐ The ratchet reach — the package-local half +// --------------------------------------------------------------------------- + +/** + * `packages/qa/dogfood/test/authz-conformance.test.ts` ratchets route + * completeness over a CURATED per-file probe table. For THIS file it carries + * exactly one non-tripwire probe — `private registerMetadataEndpoints(` — + * so the umbrella covers the `/meta` family and nothing else in this file can + * mint a discovery key at all. + * + * ⚠️ That cross-package fact is deliberately NOT asserted here: reading + * `packages/qa/**` from a `packages/rest` test would silently widen this + * package's real input set past its `CROSS_PACKAGE_TEST_INPUTS` declaration — + * the #7802 defect. It is measured in this card's PR body instead. What §6 + * commits is the half that lives in this package and that the number is a + * function of: how many routes `RestServer` mounts, and how they distribute + * over registrars. + */ +function routesByRegistrar(): Map { + const names = [...SOURCE.matchAll(/private\s+(register[A-Za-z]*Endpoints)\s*\(/g)].map((m) => m[1]); + const proto: any = (RestServer as any).prototype; + const originals = new Map(); + const attribution = new Map(); + const { rs, table } = makeServer(realProtocol()); + + for (const name of names) { + if (typeof proto[name] !== 'function') continue; + originals.set(name, proto[name]); + } + for (const [name, fn] of originals) { + proto[name] = function (this: any, bp: string) { + const before = new Set(table.keys()); + const out = fn.call(this, bp); + const added = [...table.keys()].filter((k) => !before.has(k)); + attribution.set(name, [...(attribution.get(name) ?? []), ...added]); + return out; + }; + } + try { rs.registerRoutes(); } finally { + for (const [name, fn] of originals) proto[name] = fn; + } + // Routes registered by `registerRoutes` itself, outside every registrar. + const attributed = new Set([...attribution.values()].flat()); + attribution.set('(registerRoutes, inline)', [...table.keys()].filter((k) => !attributed.has(k))); + return attribution; +} + +describe('[#13214] §6 the ratchet reach — how much of this file one probe key stands for', () => { + it('`registerUiEndpoints` mounts exactly the one route on this card', () => { + const byRegistrar = routesByRegistrar(); + expect(byRegistrar.get('registerUiEndpoints')).toEqual([UI_ROUTE]); + }, 120_000); + + it('⭐ the `registerMetadataEndpoints` umbrella — the ONE registrar the probe table names — is a small minority of what this file mounts', () => { + const byRegistrar = routesByRegistrar(); + const total = [...byRegistrar.values()].flat().length; + const meta = (byRegistrar.get('registerMetadataEndpoints') ?? []).length; + + // Measured on this harness at the time of writing: 17 registrars, 85 + // mounted routes, 19 of them under the `/meta` umbrella and 66 outside + // every key the ratchet can mint for this file. + // + // Deliberately inequalities, not pinned counts: this is a measurement + // of PROPORTION, and an exact number would churn on every route added + // for unrelated reasons. What must not silently change is the shape — + // one covered registrar, many uncovered ones. + expect(total).toBeGreaterThan(70); + expect(meta).toBeGreaterThanOrEqual(15); + expect(total - meta).toBeGreaterThan(50); + + // Fifteen-plus sibling registrars, none of which the probe table names. + // `registerUiEndpoints` is one of them; it is not special. + const registrars = [...byRegistrar.keys()].filter((k) => k !== '(registerRoutes, inline)'); + expect(registrars.length).toBeGreaterThanOrEqual(15); + expect(registrars).toContain('registerUiEndpoints'); + expect(registrars).toContain('registerMetadataEndpoints'); + }, 120_000); + + it('⭐ POSITIVE CONTROL — the attribution really attributes, and is not just bucketing everything into one key', () => { + const byRegistrar = routesByRegistrar(); + // At least three DISTINCT registrars each holding routes, and the + // `/meta` bucket holding only `/meta` routes: an attribution that had + // collapsed would fail both. + const nonEmpty = [...byRegistrar.entries()].filter(([, v]) => v.length > 0); + expect(nonEmpty.length).toBeGreaterThan(3); + for (const route of byRegistrar.get('registerMetadataEndpoints') ?? []) { + expect(route, 'meta bucket should hold only /meta routes').toContain('/meta'); + } + expect(byRegistrar.get('registerCrudEndpoints')?.some((r) => r.includes('/data'))).toBe(true); + }, 120_000); +}); From 0684c41b0a63f002a672b21b5639b578e5642c5e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 14:07:46 +0000 Subject: [PATCH 2/2] test(rest): pin the RestServer boot to the constructor's real arity The first draft passed 27 arguments to a 20-parameter constructor. It ran -- JS drops the surplus -- but silently shifted serviceExistsProvider, requestEnvResolver and metadataServiceProvider onto the wrong parameters, so the boot under measurement was not the boot the file describes. tsconfig.test.json covers this file, which is what turned it into a compile error. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TvqBFLRzXdSPcbusDoED9k --- .../rest/src/ui-view-route-identity.measurement.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/rest/src/ui-view-route-identity.measurement.test.ts b/packages/rest/src/ui-view-route-identity.measurement.test.ts index 23b0b4fabf..f6f5af0187 100644 --- a/packages/rest/src/ui-view-route-identity.measurement.test.ts +++ b/packages/rest/src/ui-view-route-identity.measurement.test.ts @@ -105,9 +105,14 @@ function makeServer(protocol: any) { const server = recordingServer(); const provider = async () => anyService(); const rs: any = new RestServer( + // ⚠️ Exactly 20 arguments — the constructor's arity. A longer list + // still RUNS (JS drops the surplus) but silently shifts every argument + // past the overflow point onto the wrong parameter, so the boot under + // measurement stops being the boot you wrote. `tsconfig.test.json` + // covers this file, which is what turned that into a compile error + // rather than a quietly different harness. server, protocol as any, {} as any, undefined, undefined, () => 'env_13214', provider, provider, provider, provider, provider, provider, provider, - provider, provider, provider, provider, provider, provider, provider, provider, provider, provider, () => true, provider, undefined, provider, ); return { rs, table: server.table as Map };