Skip to content

Commit a256cd4

Browse files
claude[bot]claude
andauthored
test(scripts): floor workspace-enumerator's folded self-test, adopted by its seven callers (#15419)
`selfTest()` carried 22 static `t(name, ok)` sites and a failure-only sink, so "every case held" and "the cases never ran" returned the same empty list -- and the seven gates that fold it in decided on that list alone. None of them could floor the module's cases, because a green run contributes zero registrations to any of their ledgers. Pin the registered NAMES in the module. The body's four named section banners are four batteries, opened by `battery()` on the banner's own line; the existing `t()` sink routes through `registerCase()` with no case rewritten and no assertion changed. Floors are MEASURED on a run -- 18 / 3 / 1 / 3 -- and the roster's own size is pinned at 4. The check is EXPORTED rather than placed: this module is deliberately not a gate, so it has no `--self-test` dispatch and no verdict site of its own. Each of the seven folding callers adopts `workspaceEnumeratorFloorFailures()` exactly where it already adopts the module's failures -- five spread it beside the existing spread, two loop it through `expect(false, failure)`. Adopting AFTER the call is what makes an early return above the cases read as a breach; evaluated before `selfTest()`'s own `return` it would be skipped by the same early return it exists to catch. Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk Co-authored-by: Claude <noreply@anthropic.com>
1 parent be2594e commit a256cd4

8 files changed

Lines changed: 156 additions & 0 deletions

scripts/check-dev-prereqs.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ import { fileURLToPath } from 'url';
240240
import {
241241
WorkspaceEnumerationError,
242242
selfTest as workspaceEnumeratorSelfTest,
243+
workspaceEnumeratorFloorFailures,
243244
workspaceMemberDirs,
244245
} from './workspace-enumerator.mjs';
245246

@@ -898,6 +899,7 @@ function selfTest() {
898899
// its own (#11510 — being a gate is exactly what it must not be); every gate
899900
// that consolidated onto it folds in its checks.
900901
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
902+
failures.push(...workspaceEnumeratorFloorFailures());
901903

902904
// ── The floor: every declared battery RAN, and ran its cases (#13489) ───
903905
//

scripts/check-published-files.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ import { join, posix, resolve } from 'node:path';
106106
import {
107107
readWorkspaceGlobs,
108108
selfTest as workspaceEnumeratorSelfTest,
109+
workspaceEnumeratorFloorFailures,
109110
workspacePackageDirs,
110111
} from './workspace-enumerator.mjs';
111112

@@ -654,6 +655,7 @@ function selfTest() {
654655
// self-test ran and returned nothing.
655656
expect(enumeratorFailures.length === 0, `the shared workspace enumerator reported ${enumeratorFailures.length} failure(s)`);
656657
failures.push(...enumeratorFailures);
658+
failures.push(...workspaceEnumeratorFloorFailures());
657659

658660
// -- The floor: every declared battery RAN, and ran its cases (#13489) -----
659661
//

scripts/check-published-readme-exports.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ import { createProgramChecked } from './ts-parse.mjs';
296296
import {
297297
WORKSPACE_FILE,
298298
selfTest as workspaceEnumeratorSelfTest,
299+
workspaceEnumeratorFloorFailures,
299300
workspacePackageDirs,
300301
} from './workspace-enumerator.mjs';
301302

@@ -3545,6 +3546,7 @@ function selfTest() {
35453546
// The shared workspace enumerator is a plain module with no CI invocation of
35463547
// its own (#11510); every gate that consolidated onto it folds in its checks.
35473548
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
3549+
failures.push(...workspaceEnumeratorFloorFailures());
35483550

35493551
// ── The floor: every declared battery RAN, and ran its cases (#13489) ───
35503552
//

scripts/check-test-source-alias.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ import {
318318
isExclusionGlob,
319319
readWorkspaceGlobs,
320320
selfTest as workspaceEnumeratorSelfTest,
321+
workspaceEnumeratorFloorFailures,
321322
} from './workspace-enumerator.mjs';
322323
import { tmpdir } from 'node:os';
323324
import process from 'node:process';
@@ -2959,6 +2960,7 @@ function selfTest() {
29592960
// (#11510 — being a gate is exactly what it must not be); every script that
29602961
// consolidated onto it folds in its checks.
29612962
for (const failure of workspaceEnumeratorSelfTest({ root: REPO_ROOT })) expect(false, failure);
2963+
for (const failure of workspaceEnumeratorFloorFailures()) expect(false, failure);
29622964
} finally {
29632965
rmSync(root, { recursive: true, force: true });
29642966
}

scripts/check-type-check-coverage.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ import { join, posix, resolve } from 'node:path';
484484
import { getHeapStatistics } from 'node:v8';
485485
import {
486486
selfTest as workspaceEnumeratorSelfTest,
487+
workspaceEnumeratorFloorFailures,
487488
workspacePackageDirs,
488489
} from './workspace-enumerator.mjs';
489490
// `typecheck`-script -> tsconfig program set. Shared with
@@ -5929,6 +5930,7 @@ function selfTest() {
59295930
// above: its cases are defined and run in `workspace-enumerator.mjs`, and a
59305931
// floor written here would pin a count that module is free to change.
59315932
failures.push(...workspaceEnumeratorSelfTest({ root: ROOT }));
5933+
failures.push(...workspaceEnumeratorFloorFailures());
59325934

59335935
if (failures.length) {
59345936
console.error(`✗ check:type-check-coverage --self-test — ${failures.length} failure(s)\n`);

scripts/check-type-source-resolution.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ import {
152152
isExclusionGlob,
153153
readWorkspaceGlobs,
154154
selfTest as workspaceEnumeratorSelfTest,
155+
workspaceEnumeratorFloorFailures,
155156
} from './workspace-enumerator.mjs';
156157
// The `typecheck`-script -> tsconfig program set, shared with
157158
// `check-type-check-coverage.mjs` (#11490). Imported rather than re-derived:
@@ -2364,6 +2365,7 @@ function selfTest() {
23642365
// (#11510 — being a gate is exactly what it must not be); every script that
23652366
// consolidated onto it folds in its checks.
23662367
for (const failure of workspaceEnumeratorSelfTest({ root: REPO_ROOT })) expect(false, failure);
2368+
for (const failure of workspaceEnumeratorFloorFailures()) expect(false, failure);
23672369
} finally {
23682370
rmSync(root, { recursive: true, force: true });
23692371
}

scripts/pnpm-filter-targets.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import {
9292
WorkspaceEnumerationError,
9393
parseWorkspaceGlobs,
9494
selfTest as workspaceEnumeratorSelfTest,
95+
workspaceEnumeratorFloorFailures,
9596
workspacePackageDirs,
9697
} from './workspace-enumerator.mjs';
9798

@@ -675,6 +676,7 @@ export async function selfTest() {
675676
// The shared workspace enumerator is a plain module with no CI invocation of
676677
// its own (#11510); every script that consolidated onto it folds in its checks.
677678
failures.push(...workspaceEnumeratorSelfTest({ root: root ?? HERE }));
679+
failures.push(...workspaceEnumeratorFloorFailures());
678680

679681
if (failures.length === 0) {
680682
console.log(

scripts/workspace-enumerator.mjs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,143 @@ export function workspacePackages(root) {
314314
return out;
315315
}
316316

317+
// ── The self-test's own battery roster and floor (#13489) ──────────────────
318+
//
319+
// `failures.length === 0` used to be this self-test's ONLY success condition,
320+
// so "every case held" and "the cases never ran" printed the same line — and
321+
// here that line is printed by SEVEN other files, none of which could tell the
322+
// difference either. Closed the way PR #13487 validated on check-doc-authoring:
323+
// what is pinned is the registered NAMES, not a number.
324+
//
325+
// This body carries FOUR named section banners, so each banner is one battery,
326+
// opened by a `battery()` call on the banner's own line — the sectioned shape
327+
// PR #15327 landed, rather than the single hoisted battery PR #15217 landed for
328+
// bodies carrying fewer than two. Sectioning is what lets the parse cases stop
329+
// running while the expansion cases keep going and still have something name
330+
// the half that went quiet.
331+
//
332+
// The counts are a FLOOR, not an equality — adding cases is ordinary work and
333+
// must not red. A battery BELOW its floor means cases stopped running; the
334+
// remedy is to find what stopped registering.
335+
//
336+
// ── Why the LEDGER is module-level and the CHECK sits in the CALLERS ───────
337+
//
338+
// This module is deliberately NOT a gate (see the header), so unlike every
339+
// other file in this recipe it has no `--self-test` dispatch and no verdict
340+
// site of its own: `selfTest()` REGISTERS and returns its failures, and the
341+
// seven folding callers DECIDE. So the check is exported instead of placed —
342+
// `workspaceEnumeratorFloorFailures()` below — and each caller adopts it
343+
// exactly where it already adopts the failures, immediately after the call.
344+
// The ledger it reads therefore has to outlive `selfTest()`'s frame — hence
345+
// module scope rather than the local map the single-body recipe closes over.
346+
//
347+
// ⛔ The floor is NOT evaluated at the end of `selfTest()` before its `return`:
348+
// an early return anywhere above that line would skip the check and the cases
349+
// TOGETHER, which is the one defect this card exists to catch. Adopted AFTER
350+
// the call, in a caller, the same early return lands as a count below the floor
351+
// and reds — in all seven callers at once, which is the accepted cost of a
352+
// module that seven gates fold in and none of them owns.
353+
//
354+
// ⛔ A pinned TOTAL is not the repair: this body's 25 registrations split 18 /
355+
// 3 / 1 / 3, so the parse battery could drop from 18 cases to 3 and keep a
356+
// total "right" the moment a sibling grows.
357+
const SELF_TEST_BATTERIES = Object.freeze({
358+
'the parse, one case per divergence the consolidation settled': 18,
359+
'the expansion': 3,
360+
'the property this module exists to keep: NO path population': 1,
361+
'the live half, when a caller supplies the repo root': 3,
362+
});
363+
364+
// DELETING an entry silences that battery's floor exactly as effectively as
365+
// zeroing it, so the roster's own size is pinned too.
366+
const SELF_TEST_BATTERY_FLOOR = 4;
367+
368+
// The key an assertion is filed under when no battery is open. It is not a
369+
// declared battery, so it reds by the same set difference rather than silently
370+
// inflating whichever battery happened to run last.
371+
const UNATTRIBUTED_BATTERY = '(no battery open)';
372+
373+
// The battery ledger, read by `workspaceEnumeratorFloorFailures()` below from
374+
// the OTHER function. `battery()` opens a battery; every assertion registered
375+
// after that line is attributed to the one most recently opened, so a section
376+
// that stops running stops registering and names ITSELF at the floor rather
377+
// than going quiet.
378+
//
379+
// ⚠️ Named for the roster's role, deliberately NOT with a self-test spelling:
380+
// `check:pm-dispatch-gates` anchors on a top-level declaration whose NAME spells
381+
// self-test and every such name owes a row in its COMPOUND_ANCHOR_LEDGER. This
382+
// machinery holds no fixtures to mask and reads no path literal, so the accurate
383+
// name is the one that says `battery`.
384+
const batterySeen = new Map();
385+
let openBattery = null;
386+
387+
/** Open a battery. Every assertion registered after this line is attributed to it. */
388+
function battery(name) {
389+
openBattery = name;
390+
}
391+
392+
/** Called by `selfTest()`'s own assertion sink, once per assertion. */
393+
function registerCase() {
394+
const name = openBattery ?? UNATTRIBUTED_BATTERY;
395+
batterySeen.set(name, (batterySeen.get(name) ?? 0) + 1);
396+
}
397+
398+
/**
399+
* The floor: every declared battery RAN, and ran its cases (#13489).
400+
*
401+
* Guards the registrations made by **`selfTest()`** — the body whose assertion
402+
* sink `t()` routes through `registerCase()`. Every folding caller calls this
403+
* immediately after it adopts `selfTest()`'s failures and BEFORE its own
404+
* verdict line, so that line can only be printed by a run in which the set of
405+
* batteries that registered assertions EQUALS the set declared, each at or
406+
* above its own count. A set difference says WHICH battery stopped; a count
407+
* says only that something did.
408+
*
409+
* ⚠️ EXPORTED, and prefixed with this module's name, because the callers are
410+
* the verdict sites: seven gates import it, and each of them already declares
411+
* a `batteryFloorFailures` of its OWN for its OWN roster. The unprefixed name
412+
* this recipe uses everywhere else would collide in all seven import lists —
413+
* the same reason they all import `selfTest as workspaceEnumeratorSelfTest`.
414+
*
415+
* @returns {string[]} floor breaches; empty means the floor held
416+
*/
417+
export function workspaceEnumeratorFloorFailures() {
418+
const declared = Object.keys(SELF_TEST_BATTERIES);
419+
const problems = [];
420+
if (declared.length < SELF_TEST_BATTERY_FLOOR) {
421+
problems.push(
422+
`workspace-enumerator: SELF_TEST_BATTERIES declares ${declared.length} batteries, below the pinned `
423+
+ `${SELF_TEST_BATTERY_FLOOR} — a battery deleted from the roster takes its own floor with it.`,
424+
);
425+
}
426+
for (const [name, count] of batterySeen) {
427+
if (declared.includes(name)) continue;
428+
problems.push(
429+
`workspace-enumerator: self-test battery "${name}" registered ${count} case(s) but is not declared in `
430+
+ 'SELF_TEST_BATTERIES — an assertion attributed to no declared battery is one nothing floors.',
431+
);
432+
}
433+
for (const name of declared) {
434+
const count = batterySeen.get(name) ?? 0;
435+
if (count >= SELF_TEST_BATTERIES[name]) continue;
436+
problems.push(
437+
count === 0
438+
? `workspace-enumerator: self-test battery "${name}" DID NOT RUN — 0 cases registered, `
439+
+ `${SELF_TEST_BATTERIES[name]} pinned. The verdict below would have claimed those cases hold.`
440+
: `workspace-enumerator: self-test battery "${name}" registered ${count} case(s), below its pinned floor of `
441+
+ `${SELF_TEST_BATTERIES[name]} — cases that used to run no longer do.`,
442+
);
443+
}
444+
if (problems.length) {
445+
problems.push(
446+
'workspace-enumerator: a battery at or below its floor means cases STOPPED RUNNING — the battery is the '
447+
+ 'bug, not the number. Find what stopped registering (an early return, a deleted block, a guard that '
448+
+ 'now skips) and restore it.',
449+
);
450+
}
451+
return problems;
452+
}
453+
317454
/**
318455
* The shared assertions, returned rather than printed so each importing gate
319456
* can fold them into its own `--self-test` report.
@@ -328,6 +465,7 @@ export function workspacePackages(root) {
328465
export function selfTest({ root = null } = {}) {
329466
const failures = [];
330467
const t = (name, ok) => {
468+
registerCase();
331469
if (!ok) failures.push(`workspace-enumerator: ${name}`);
332470
};
333471
// Every fixture path is ASSEMBLED, never spelled. A path-shaped literal
@@ -352,6 +490,7 @@ export function selfTest({ root = null } = {}) {
352490
const flat = JSON.stringify([PKGS]);
353491

354492
// ── the parse, one case per divergence the consolidation settled ──────────
493+
battery('the parse, one case per divergence the consolidation settled');
355494
t('a plain list parses', answer(`packages:\n - ${PKGS}\n - ${APPS}\n`) === both);
356495
t('quotes are stripped', answer(`packages:\n - '${PKGS}'\n - "${APPS}"\n`) === both);
357496
t('CRLF parses the same', answer(`packages:\r\n - ${PKGS}\r\n - ${APPS}\r\n`) === both);
@@ -384,6 +523,7 @@ export function selfTest({ root = null } = {}) {
384523
t('the flow-sequence form is REFUSED rather than read as empty', answer(`packages: [${PKGS}, ${APPS}]\n`) === 'REFUSED');
385524

386525
// ── the expansion ─────────────────────────────────────────────────────────
526+
battery('the expansion');
387527
const NOWHERE = P('', 'nonexistent');
388528
const expandRefused = (glob) => {
389529
try {
@@ -401,6 +541,7 @@ export function selfTest({ root = null } = {}) {
401541
);
402542

403543
// ── the property this module exists to keep: NO path population ───────────
544+
battery('the property this module exists to keep: NO path population');
404545
//
405546
// Pinned mechanically, not by review, and read off THIS FILE's own bytes so
406547
// a stale copy cannot satisfy it. A path-shaped literal added here — in the
@@ -448,6 +589,7 @@ export function selfTest({ root = null } = {}) {
448589
}
449590

450591
// ── the live half, when a caller supplies the repo root ───────────────────
592+
battery('the live half, when a caller supplies the repo root');
451593
if (root !== null) {
452594
let live = null;
453595
try {

0 commit comments

Comments
 (0)