Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 177 additions & 23 deletions scripts/check-console-intercept-disarm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,56 @@ const VITEST_CONFIG_NAMES = [
'vitest.config.cjs',
];

// The population this gate reads, declared for `scripts/pm/dispatch-gates.mjs`
// (the ROOT_DIR_WATCH_HINTS idiom), so a card that edits a package-root
// manifest or vitest config is told about this gate before CI is. `scan()`
// below opens exactly two files per workspace package: its `package.json` and
// its package-root vitest config. Nothing else under a package moves the
// verdict, so a new test file under an existing package derives nothing.
//
// One pair per glob `workspaceGlobs()` reads, in that order. A `<dir>/*` glob
// is spelled `<dir>/*/package.json` and `<dir>/*/vitest.config.*` — the glob in
// a non-final segment makes the derivation judge it as a pattern, one path
// segment per `*`. A literal member `<dir>` would be spelled `<dir>/package.json`
// and `<dir>/vitest.config` (`watchHintsForGlob`): with no glob ahead of it a
// trailing `.*` is collapsed to `<dir>/vitest.config.`, which names nothing,
// while the bare stem reaches every one of VITEST_CONFIG_NAMES through the
// derivation's module-extension equality. None is read today: the parse stops
// at the first comment line in the `packages:` list, so the `ui` members below
// it are not in this gate's population, and are not declared here either.
//
// ⛔ A literal array, never computed from the globs (`check:watch-hint-literal`):
// the derivation reads source text, so a computed list contributes no hint.
// Hand-kept, and `main()` holds it to the workspace file: a glob in
// `pnpm-workspace.yaml` with no pair here, or a pair here for a glob that file
// no longer lists, is a finding (`watchHintDrift`). Not readable from the
// repo-root files themselves — `pnpm-workspace.yaml` and `vitest.preset.mts`
// carry no separator, so no spelling of them is a hint the derivation admits.
export const ROOT_DIR_WATCH_HINTS = [
'packages/*/package.json',
'packages/*/vitest.config.*',
'packages/apps/*/package.json',
'packages/apps/*/vitest.config.*',
'packages/drivers/*/package.json',
'packages/drivers/*/vitest.config.*',
'packages/plugins/*/package.json',
'packages/plugins/*/vitest.config.*',
'packages/qa/*/package.json',
'packages/qa/*/vitest.config.*',
'packages/triggers/*/package.json',
'packages/triggers/*/vitest.config.*',
'packages/services/*/package.json',
'packages/services/*/vitest.config.*',
'packages/adapters/*/package.json',
'packages/adapters/*/vitest.config.*',
'packages/connectors/*/package.json',
'packages/connectors/*/vitest.config.*',
'apps/*/package.json',
'apps/*/vitest.config.*',
'examples/*/package.json',
'examples/*/vitest.config.*',
];

const DISARM_RE = /\bdisableConsoleIntercept\s*:\s*true\b/;
const REARM_RE = /\bdisableConsoleIntercept\s*:\s*false\b/;
const PROJECTS_RE = /\bprojects\s*:\s*\[/;
Expand Down Expand Up @@ -207,6 +257,31 @@ const REMEDY = ` // Late console writes must not redden a green suite (#10374
* instead of silently emptying its population.
*/
export function workspacePackageDirs(root) {
const dirs = [];
for (const glob of workspaceGlobs(root)) {
if (glob.endsWith('/*')) {
const base = join(root, glob.slice(0, -2));
if (!existsSync(base)) continue;
for (const entry of readdirSync(base, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const dir = join(base, entry.name);
if (existsSync(join(dir, 'package.json'))) dirs.push(dir);
}
} else if (!glob.includes('*')) {
const dir = join(root, glob);
if (existsSync(join(dir, 'package.json'))) dirs.push(dir);
} else {
throw new Error(
`pnpm-workspace.yaml glob '${glob}' is wider than '<dir>/*' — teach ` +
`workspacePackageDirs() the new shape before trusting this gate again`,
);
}
}
return dirs;
}

/** `pnpm-workspace.yaml`'s `packages:` globs, in file order. Throws when none parse. */
export function workspaceGlobs(root) {
const raw = readFileSync(join(root, 'pnpm-workspace.yaml'), 'utf8');
const globs = [];
let inPackages = false;
Expand All @@ -227,27 +302,51 @@ export function workspacePackageDirs(root) {
if (globs.length === 0) {
throw new Error(`no package globs parsed from ${join(root, 'pnpm-workspace.yaml')}`);
}
const dirs = [];
for (const glob of globs) {
if (glob.endsWith('/*')) {
const base = join(root, glob.slice(0, -2));
if (!existsSync(base)) continue;
for (const entry of readdirSync(base, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const dir = join(base, entry.name);
if (existsSync(join(dir, 'package.json'))) dirs.push(dir);
}
} else if (!glob.includes('*')) {
const dir = join(root, glob);
if (existsSync(join(dir, 'package.json'))) dirs.push(dir);
} else {
throw new Error(
`pnpm-workspace.yaml glob '${glob}' is wider than '<dir>/*' — teach ` +
`workspacePackageDirs() the new shape before trusting this gate again`,
);
}
return globs;
}

/**
* The watch-hint pair one workspace glob owes `ROOT_DIR_WATCH_HINTS`, spelled
* the way that declaration's comment explains. A glob wider than `<dir>/*` owes
* nothing here: `workspacePackageDirs()` already refuses it, loudly.
*/
export function watchHintsForGlob(glob) {
if (glob.endsWith('/*')) return [`${glob}/package.json`, `${glob}/vitest.config.*`];
if (!glob.includes('*')) return [`${glob}/package.json`, `${glob}/vitest.config`];
return [];
}

/**
* Where `ROOT_DIR_WATCH_HINTS` and the workspace globs disagree: `missing` are
* pairs a listed glob owes and the declaration lacks (a package there edits its
* config and no dispatch derivation names this gate); `stale` are declared
* hints no listed glob owes (a lead for a population this gate no longer reads).
*/
export function watchHintDrift(globs, hints = ROOT_DIR_WATCH_HINTS) {
const owed = globs.flatMap(watchHintsForGlob);
return {
missing: owed.filter((h) => !hints.includes(h)),
stale: hints.filter((h) => !owed.includes(h)),
};
}

function watchHintDriftFindings(root) {
const { missing, stale } = watchHintDrift(workspaceGlobs(root));
const findings = [];
if (missing.length > 0) {
findings.push(
`ROOT_DIR_WATCH_HINTS lacks ${missing.join(', ')} — pnpm-workspace.yaml lists the glob, so a ` +
`card editing a package-root manifest or vitest config there is not told about this gate ` +
`until CI. Add the pair to the declaration in scripts/check-console-intercept-disarm.mjs.`,
);
}
return dirs;
if (stale.length > 0) {
findings.push(
`ROOT_DIR_WATCH_HINTS declares ${stale.join(', ')}, which no pnpm-workspace.yaml glob owes — ` +
`remove it from the declaration in scripts/check-console-intercept-disarm.mjs.`,
);
}
return findings;
}

/** @returns {{findings: string[], vitestPackages: number}} */
Expand Down Expand Up @@ -325,11 +424,12 @@ function main() {
let result;
try {
result = scan(REPO_ROOT);
result.driftFindings = watchHintDriftFindings(REPO_ROOT);
} catch (error) {
console.error(`check-console-intercept-disarm: MEASUREMENT FAILED — ${error.message}`);
process.exit(2);
}
const { findings, vitestPackages } = result;
const { findings, vitestPackages, driftFindings } = result;
if (vitestPackages === 0) {
console.error(
'check-console-intercept-disarm: MEASUREMENT FAILED — the scan found ZERO ' +
Expand All @@ -343,8 +443,14 @@ function main() {
`check-console-intercept-disarm: ${findings.length} package(s) exposed to the ` +
`vitest late-console teardown race (#10374):\n\n${findings.join('\n\n')}\n`,
);
process.exit(1);
}
if (driftFindings.length > 0) {
console.error(
`check-console-intercept-disarm: the declared watch population has drifted from ` +
`pnpm-workspace.yaml:\n\n${driftFindings.join('\n\n')}\n`,
);
}
if (findings.length > 0 || driftFindings.length > 0) process.exit(1);
console.log(
`OK: ${vitestPackages} vitest-running package(s), every one disarms console ` +
`interception at the package root.`,
Expand Down Expand Up @@ -644,6 +750,54 @@ check(() => {
}
});

// The declared watch population (ROOT_DIR_WATCH_HINTS) against the REAL
// workspace file, and the drift reading in both directions. Fixture globs are
// assembled from segments so no fixture string reads as a path literal of
// this gate's own.
const realDrift = watchHintDrift(workspaceGlobs(REPO_ROOT));
check(() => {
if (realDrift.missing.length > 0 || realDrift.stale.length > 0) {
failures += 1;
console.error(
`self-test FAIL: ROOT_DIR_WATCH_HINTS has drifted from pnpm-workspace.yaml — ` +
`missing: ${realDrift.missing.join(', ') || 'none'}; stale: ${realDrift.stale.join(', ') || 'none'}.`,
);
}
});
const extraGlob = ['fixture-root', '*'].join('/');
const addedGlob = watchHintDrift([...workspaceGlobs(REPO_ROOT), extraGlob]);
check(() => {
const want = watchHintsForGlob(extraGlob);
if (want.length !== 2 || addedGlob.missing.join('|') !== want.join('|') || addedGlob.stale.length !== 0) {
failures += 1;
console.error(`self-test FAIL: a workspace glob the declaration lacks is not read as missing: ${JSON.stringify(addedGlob)}`);
}
});
const droppedGlob = watchHintDrift(workspaceGlobs(REPO_ROOT).filter((g) => g !== workspaceGlobs(REPO_ROOT)[0]));
check(() => {
const want = watchHintsForGlob(workspaceGlobs(REPO_ROOT)[0]);
if (droppedGlob.stale.join('|') !== want.join('|') || droppedGlob.missing.length !== 0) {
failures += 1;
console.error(`self-test FAIL: a declared pair no workspace glob owes is not read as stale: ${JSON.stringify(droppedGlob)}`);
}
});
check(() => {
// A literal member is spelled with the bare config STEM, never a trailing
// `.*` (which the derivation collapses to a name no file has); a `<dir>/*`
// member keeps the `.*`, and a wider glob owes nothing.
const literal = watchHintsForGlob('fixture-root');
const starred = watchHintsForGlob(extraGlob);
const wide = watchHintsForGlob(['fixture-root', '**'].join('/'));
if (
literal[1] !== ['fixture-root', 'vitest.config'].join('/') ||
starred[1] !== [extraGlob, 'vitest.config.*'].join('/') ||
wide.length !== 0
) {
failures += 1;
console.error(`self-test FAIL: watchHintsForGlob spelling — ${JSON.stringify({ literal, starred, wide })}`);
}
});

// ── The floor: every declared battery RAN, and ran its cases (#13489) ────
//
// Evaluated after every battery has had its chance and BEFORE the verdict, so
Expand Down Expand Up @@ -692,7 +846,7 @@ check(() => {
}

if (failures > 0) process.exit(1);
console.log(`self-test OK: ${cases.length} cases + real-tree population floor.`);
console.log(`self-test OK: ${cases.length} cases + real-tree population floor + watch-hint declaration.`);

return SELF_TEST_VERDICT;
}
Expand Down
21 changes: 10 additions & 11 deletions scripts/pm/dispatch-gates.data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -278,23 +278,22 @@ export const PATH_LIST_MARKER_KEYS = Object.freeze(['inherited-population', 'sel
* reds too: a stale exclusion is an exclusion nobody is measuring any more.
*/
export const ROOT_WALK_RESIDUE_LEDGER = [
[
'scripts/check-console-intercept-disarm.mjs',
'its `scan(REPO_ROOT)` walks `workspacePackageDirs(root)` — every workspace PACKAGE ROOT\'s package.json and '
+ 'vitest.config.*, read off pnpm-workspace.yaml. That is workspace-wide but it is not every file: a new test '
+ 'file under an existing package does not move it, a new PACKAGE does. Declaring the whole tree would put it '
+ 'on every card on a population it does not read.',
],
[
'scripts/check-console-intercept-disarm.mjs --self-test',
'the same gate file, reached through its self-test invocation; the reading above is the whole of it.',
],
[
'scripts/check-skill-frame-freshness.mjs --self-test',
'lint.yml runs the SELF-TEST HALF and never the scan — its step is named that, and the step comment states why '
+ '(the pnpm script would drag the scan in with it). The repo-root default parameter belongs to the scan half '
+ 'CI does not schedule, so a whole-tree row for this family would advertise work no workflow performs.',
],
// ⚖️ `scripts/check-console-intercept-disarm.mjs` and its `--self-test` LEFT this
// population under #29353 and their two rows are gone with them — a listed
// family that stops being a member reds here. They left BY PATH: the gate now
// declares what its scan opens, a package-root `package.json` and vitest
// config under each workspace glob, as `ROOT_DIR_WATCH_HINTS` in its own
// source, and its scan reds when that declaration drifts from
// `pnpm-workspace.yaml`. The rows had stated the population right and drawn
// the wrong conclusion from it: an EDITED package-root config moves the gate
// as surely as a new package does, so a card editing one learned of the gate
// only from CI (PR #29345 paid one fix round).
// ⚖️ `check:pm-half-states` LEFT this population under #16904's D2 and its row
// is gone with it — a listed family that stops being a member reds here, and
// a stale exclusion is an exclusion nobody measures. It is now placeable BY
Expand Down
64 changes: 64 additions & 0 deletions scripts/pm/dispatch-gates.self-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7988,6 +7988,70 @@ export function selfTest({ tier = 'full' } = {}) {
);
}

// ── A package-root config edit derives the console-intercept gate (#29353) ──
//
// The card's acceptance, held against the LIVE tree. The gate's scan opens a
// package-root `package.json` and vitest config under each workspace glob and
// nothing else, and it declares exactly that population in its own source, so
// BOTH invocations CI runs (scan and `--self-test`, one lint.yml step) are
// placed by every such file, while a test file under an existing package —
// tracked, or one a card adds — places neither. The rows it left in
// ROOT_WALK_RESIDUE_LEDGER are held gone by the stale limb above.
{
const gate = 'scripts/check-console-intercept-disarm.mjs';
const invocations = [gate, `${gate} --self-test`];
const entries = invocations.map((c) => liveDiscovery.byCheck.get(c));
t('both console-intercept invocations are discovered families', entries.every(Boolean));
const tracked = [...liveTree.files];
const trackedSet = new Set(tracked);
// Package roots read off the tree, not off a copy of the workspace globs:
// a tracked vitest config whose own directory carries a tracked manifest,
// under one of the three workspace top-level roots.
const rootConfigs = tracked.filter((f) => {
const m = /^((?:packages|apps|examples)\/(?:[^/]+\/)?[^/]+)\/vitest\.config\.[cm]?[jt]s$/.exec(f);
return m && trackedSet.has(`${m[1]}/package.json`);
});
t(
`control: the live tree carries package-root vitest configs to probe with (${rootConfigs.length})`,
rootConfigs.length >= 60,
);
const unplacedBy = (entry, probe) => placeFamily(entry, [probe]).verdict !== 'matched';
const missedConfigs = rootConfigs.filter((f) => entries.some((e) => e && unplacedBy(e, f)));
t(
'every package-root vitest config places both invocations — the card PR #29345 paid a CI round for' +
(missedConfigs.length ? ` — not placed by: ${missedConfigs.slice(0, 5).join(', ')}` : ''),
entries.every(Boolean) && missedConfigs.length === 0,
);
const manifests = rootConfigs.map((f) => f.replace(/\/[^/]+$/, '/package.json'));
const missedManifests = manifests.filter((f) => entries.some((e) => e && unplacedBy(e, f)));
t(
'and so does the package-root package.json beside each — the other file the scan opens' +
(missedManifests.length ? ` — not placed by: ${missedManifests.slice(0, 5).join(', ')}` : ''),
entries.every(Boolean) && missedManifests.length === 0,
);
// A card's file surface is often paths that do not exist yet: a NEW package.
const newPackage = ['packages', 'plugins', 'plugin-not-yet-written'].join('/');
t(
'a NEW package\'s root config and manifest place both invocations too',
entries.every((e) => e && !unplacedBy(e, `${newPackage}/vitest.config.ts`) && !unplacedBy(e, `${newPackage}/package.json`)),
);
const testFiles = tracked.filter((f) => /^(?:packages|apps|examples)\/.+\.(?:test|spec)\.[cm]?[jt]sx?$/.test(f));
t(
`control: the live tree carries test files under packages to probe with (${testFiles.length})`,
testFiles.length >= 1000,
);
t(
'no test file under an existing package places either invocation, tracked or brand new',
entries.every((e) => e
&& placeFamily(e, testFiles).verdict !== 'matched'
&& unplacedBy(e, ['packages', 'services', 'service-sms', 'src', 'not-yet-written.test.ts'].join('/'))),
);
t(
'nor does a vitest config BELOW a package root — the scan deliberately reads the root one only',
entries.every((e) => e && unplacedBy(e, ['packages', 'services', 'service-sms', 'test', 'fixtures', 'vitest.config.ts'].join('/'))),
);
}

// ── The CI-MEASURED-ONLY shape (#14004) ───────────────────────────────────
//
// The two markers above are DECLARATIONS a gate carries. This one is the
Expand Down
Loading