Skip to content

Commit 4bdd92c

Browse files
committed
test(dogfood): the anonymous sweep reports red routes by class and mount family, with route detail behind a local-only switch
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H
1 parent acd55cd commit 4bdd92c

1 file changed

Lines changed: 89 additions & 13 deletions

File tree

‎packages/qa/dogfood/test/anonymous-door-sweep.dogfood.test.ts‎

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,31 @@
7878
// an outside family only by an amendment of ADR-0138, and the classification
7979
// is edited after that amendment, never instead of it.
8080
//
81-
// ## Disclosure
81+
// ## Disclosure: what a red run prints, and the one local switch
8282
//
83-
// Classes, positions and functions only (#22146). The assertions are
84-
// class-level; a failure names the route pattern and the status it answered,
85-
// which is what fixing it needs, and never a body.
83+
// Classes, positions and functions only (#22146; ruling 6134095444: "no
84+
// route-level readings go on GitHub"). This suite runs on `main` with the
85+
// dogfood battery, so whatever a red case prints lands in a public CI log.
86+
// So a red sweep case prints, BY DEFAULT, one line per class and mount family:
87+
// the class (a door class, an outside family or the everything-else class),
88+
// the mount family the route table names (the pattern's leading segments; the
89+
// table records patterns, not registrars), the source, and the COUNT of red
90+
// routes in it. ⛔ It never pairs a route with the answer it got, and it never
91+
// prints a body. The judgement is unchanged: the same routes are judged, and a
92+
// case is red exactly when one of them misses its class.
93+
//
94+
// Route-level detail (each red route's pattern and why it missed its class) is
95+
// for a developer's own machine only, behind `OS_TEST_ANON_SWEEP_ROUTE_DETAIL`,
96+
// which is off by default:
97+
//
98+
// OS_TEST_ANON_SWEEP_ROUTE_DETAIL=1 pnpm --filter @objectstack/dogfood \
99+
// exec vitest run test/anonymous-door-sweep.dogfood.test.ts
100+
//
101+
// ⛔ Never set it in CI or in a workflow. Two belts keep it local: through
102+
// `turbo run test` (the path CI takes) the variable is never forwarded,
103+
// because turbo's strict env mode hands a task only what its `env` declares
104+
// and `@objectstack/dogfood#test` declares `OS_TEST_TIERS` and `OS_TEST_SHARD`
105+
// alone; and the switch is ignored whenever `CI` is set.
86106
//
87107
// Not eligible for the shared showcase project: it boots a derived
88108
// configuration with its own plugins.
@@ -193,7 +213,11 @@ async function send(stack: VerifyStack, method: string, path: string, init: Requ
193213
const withBody = method !== 'GET' && method !== 'HEAD';
194214
let timer: ReturnType<typeof setTimeout> | undefined;
195215
const timeout = new Promise<never>((_, reject) => {
196-
timer = setTimeout(() => reject(new Error(`${method} ${path} did not answer within 15s`)), 15_000);
216+
// Path-free unless the local switch is on: a hang is an answer too (header, Disclosure).
217+
timer = setTimeout(
218+
() => reject(new Error(`a request did not answer within 15s${ROUTE_DETAIL ? `: ${method} ${path}` : ''}`)),
219+
15_000,
220+
);
197221
});
198222
try {
199223
const res = await Promise.race([
@@ -223,6 +247,31 @@ async function send(stack: VerifyStack, method: string, path: string, init: Requ
223247

224248
const keyOf = (r: { method: string; pattern: string }) => `${r.method} ${r.pattern}`;
225249

250+
/**
251+
* The local-only switch for route-level failure detail (header, Disclosure).
252+
* Off by default, and ignored whenever `CI` is set.
253+
*/
254+
const ROUTE_DETAIL = process.env.OS_TEST_ANON_SWEEP_ROUTE_DETAIL === '1' && !process.env.CI;
255+
256+
/**
257+
* The mount family a route belongs to: the pattern's leading segments past the
258+
* API base, which is as much as the route table can say about who registered
259+
* it. A declared endpoint is in no route table, so its family is its kind.
260+
*/
261+
function mountFamilyOf(route: SweptRoute): string {
262+
if (route.source === 'declared') {
263+
return `declared ${route.endpoint?.type ?? 'unknown-type'} endpoints (authRequired: ${String(route.endpoint?.authRequired)})`;
264+
}
265+
const segs = route.pattern.split('/').filter((x) => x !== '');
266+
const base = segs[0] === 'api' ? (segs[1] === 'v1' ? 2 : 1) : 0;
267+
const head = segs[base];
268+
const kept = head === undefined || head.startsWith(':') || head === '*' ? segs.slice(0, base) : segs.slice(0, base + 1);
269+
return `/${kept.join('/')}`;
270+
}
271+
272+
const classLabelOf = (cls: SweepClass): string =>
273+
cls.kind === 'door' ? `door class ${cls.door.id}` : cls.kind === 'outside' ? `outside family ${cls.family.id}` : cls.kind;
274+
226275
interface Swept {
227276
readonly route: SweptRoute;
228277
readonly probePath: string;
@@ -533,36 +582,63 @@ describe('ADR-0138 D2 anonymous sweep: one unauthenticated request per registere
533582
const order = (p: Swept) => (p.cls.kind === 'outside' && p.cls.family.id === 'dev-metadata-hmr' ? 1 : 0);
534583
for (const p of [...population].sort((a, b) => order(a) - order(b))) {
535584
if (p.cls.kind === 'middleware') continue;
536-
p.answer = await send(stack, p.route.method === 'ALL' ? 'GET' : p.route.method, p.probePath);
585+
try {
586+
p.answer = await send(stack, p.route.method === 'ALL' ? 'GET' : p.route.method, p.probePath);
587+
} catch {
588+
// No answer is recorded as status 0: it misses every class and is
589+
// counted, never thrown with its route (header, Disclosure).
590+
p.answer = { status: 0, contentType: '' };
591+
}
537592
}
538593
}, 300_000);
539594

540-
const failures = (pick: (p: Swept) => string | undefined): string[] =>
541-
population.flatMap((p) => {
595+
/**
596+
* The red routes of one case, as that case prints them. By default one line
597+
* per class and mount family with the COUNT of red routes in it; with the
598+
* local switch, one line per route. Non-empty exactly when a route is red,
599+
* so the switch changes what is printed and never what is judged.
600+
*/
601+
const failures = (pick: (p: Swept) => string | undefined): string[] => {
602+
const red = population.flatMap((p) => {
542603
const why = pick(p);
543-
return why ? [`${keyOf(p.route)} (${p.route.source}) — ${why}`] : [];
604+
return why ? [{ p, why }] : [];
544605
});
606+
if (ROUTE_DETAIL) return red.map(({ p, why }) => `${keyOf(p.route)} (${p.route.source}) — ${why}`);
607+
const counts = new Map<string, number>();
608+
for (const { p } of red) {
609+
const group = `${classLabelOf(p.cls)} · ${mountFamilyOf(p.route)} (${p.route.source})`;
610+
counts.set(group, (counts.get(group) ?? 0) + 1);
611+
}
612+
return [...counts].sort(([a], [b]) => a.localeCompare(b)).map(([group, n]) => `${group}: ${n} route(s) red`);
613+
};
614+
615+
const DETAIL_HINT = ROUTE_DETAIL
616+
? ''
617+
: 'Route-level detail is local only: set OS_TEST_ANON_SWEEP_ROUTE_DETAIL=1 and run this file directly (see its header).';
545618

546619
it('every route but middleware got exactly one answer', () => {
547-
const unanswered = population.filter((p) => p.cls.kind !== 'middleware' && p.answer === undefined);
620+
const unanswered = population.filter((p) => p.cls.kind !== 'middleware' && !(p.answer && p.answer.status > 0));
548621
expect(unanswered.map((p) => keyOf(p.route))).toEqual([]);
549622
});
550623

551624
it('the everything-else class answers 401 throughout, with nothing served', () => {
552625
const swept = population.filter((p) => p.cls.kind === 'everything-else');
553626
expect(swept.length, 'the everything-else class is the bulk of the table').toBeGreaterThan(100);
554627
const red = failures((p) => (p.cls.kind === 'everything-else' ? EVERYTHING_ELSE(p.answer!) : undefined));
555-
expect(red, `\nRoutes in no door class and outside no family, which ADR-0138 D2 says answer 401:\n${red.join('\n')}\n`).toEqual([]);
628+
expect(
629+
red,
630+
`\nADR-0138 D2: the everything-else class (no door class, no outside family) answers 401.\n${red.join('\n')}\n${DETAIL_HINT}\n`,
631+
).toEqual([]);
556632
});
557633

558634
it('every door-class route answers its class, asked about no declared subject', () => {
559635
const red = failures((p) => (p.cls.kind === 'door' && p.cls.door.expect ? p.cls.door.expect(p.answer!) : undefined));
560-
expect(red, `\n${red.join('\n')}\n`).toEqual([]);
636+
expect(red, `\nADR-0138 D2: a door-class route asked about no declared subject answers its class.\n${red.join('\n')}\n${DETAIL_HINT}\n`).toEqual([]);
561637
});
562638

563639
it('the outside families answer as D2 states, where it states an answer', () => {
564640
const red = failures((p) => (p.cls.kind === 'outside' && p.cls.family.expect ? p.cls.family.expect(p.answer!) : undefined));
565-
expect(red, `\n${red.join('\n')}\n`).toEqual([]);
641+
expect(red, `\nADR-0138 D2: an outside family answers as D2 states, where it states an answer.\n${red.join('\n')}\n${DETAIL_HINT}\n`).toEqual([]);
566642
});
567643

568644
it('every row of the classification claims a route on this boot: none is stale', () => {

0 commit comments

Comments
 (0)