Skip to content

Commit aab0e51

Browse files
committed
wip(pm): self-test pins for the ui/scripts suite entry, measured numbers
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AYg1gtz1tWt66GpPnTo1QF
1 parent dd6401f commit aab0e51

2 files changed

Lines changed: 88 additions & 5 deletions

File tree

‎scripts/pm/dispatch-gates.data.mjs‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,8 @@ export const ESCAPABLE_LITERAL_LEDGER_ROWS = Object.freeze([
914914
*
915915
* ## Why the ui-scripts-suite entry stands in for a CI JOB (#26318)
916916
*
917-
* The 178 test files under `ui/scripts/__tests__/` belong to no package and run
917+
* The 180 test files under `ui/scripts/__tests__/` (178 that run, 2 skipped)
918+
* belong to no package and run
918919
* only inside the 16 `Test UI` shards, as the "always-on" files the affected-test
919920
* selector adds to every run because no package owns them. They pin facts a
920921
* change under ui package source, the Console app, the objectui skill bundle or
@@ -933,7 +934,13 @@ export const ESCAPABLE_LITERAL_LEDGER_ROWS = Object.freeze([
933934
* census guard in the self-test reds on the PR that does it.
934935
*
935936
* It names the WHOLE suite, never an affected subset: a subset map is a second
936-
* list that rots, and the whole run is about a minute. What it does not see is
937+
* list that rots. Measured at 6390fe52 on a 4-vCPU container, after the seven
938+
* @objectstack packages the job builds: 178 files passing in 284 s and 286 s
939+
* (two runs; the card's "about a minute" was read elsewhere), nearly all of it
940+
* module import — so a subset would save minutes, not a CI round trip, and the
941+
* reach it would have to model is the tree itself: by a static scan of each
942+
* file and the helpers it imports, 165 of the 180 read ui package source, 141
943+
* the ui apps, 16 the objectui skill bundle and 170 ui/scripts. What it does not see is
937944
* stated rather than hidden: a ui path outside the four roots (a ui package's
938945
* tests outside `src`, its `package.json`, a root config) is not named here,
939946
* though CI's `Test UI` job still schedules the suite for it — the full battery
@@ -1057,7 +1064,7 @@ export const CHANGE_KIND_ROWS = [
10571064
name: 'pnpm --dir ui exec vitest run scripts/',
10581065
ciJob: 'Test UI',
10591066
ciRunner: 'pnpm --dir ui exec vitest run',
1060-
why: "the 178 test files under ui/scripts/__tests__ belong to no package and run only in CI's 16 Test UI shards, where the affected-test selector adds them to every run, so no package suite and no check family covers them: they pin turbo build inputs, the skill-examples floor, the SchemaRenderer channel keys and the other facts a change under ui package source, the ui apps, skills/objectui or ui/scripts moves. The whole suite, never a subset: about a minute locally. It needs the seven @objectstack packages Test UI builds first, or about twenty files red on an import of a missing dist — build them exactly as the job does: pnpm exec turbo run build --filter=@objectstack/spec... --filter=@objectstack/client... --filter=@objectstack/core... --filter=@objectstack/formula... --filter=@objectstack/lint... --filter=@objectstack/sdui-parser... --filter=@objectstack/types... . Positional filter, no bare `--`, from the repo root: ui/scripts/vitest-invocation-guard.mjs refuses every other spelling",
1067+
why: "the 178 test files under ui/scripts/__tests__ belong to no package and run only in CI's 16 Test UI shards, where the affected-test selector adds them to every run, so no package suite and no check family covers them: they pin turbo build inputs, the skill-examples floor, the SchemaRenderer channel keys and the other facts a change under ui package source, the ui apps, skills/objectui or ui/scripts moves. The whole suite, never a subset: measured at about five minutes on a 4-vCPU container (284 s and 286 s, 178 files passing, import-bound), against the CI round trip of about 25 minutes it saves. It needs the seven @objectstack packages Test UI builds first, or about twenty files red on an import of a missing dist — build them exactly as the job does: pnpm exec turbo run build --filter=@objectstack/spec... --filter=@objectstack/client... --filter=@objectstack/core... --filter=@objectstack/formula... --filter=@objectstack/lint... --filter=@objectstack/sdui-parser... --filter=@objectstack/types... . Positional filter, no bare `--`, from the repo root: ui/scripts/vitest-invocation-guard.mjs refuses every other spelling",
10611068
},
10621069
],
10631070
},

‎scripts/pm/dispatch-gates.self-test.mjs‎

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
CHANGESET_PROBE_PATH,
5858
CHANGE_KIND_GATES,
5959
bindChangeKindRows,
60+
ciJobRunsRunner,
6061
bindMandatoryTierRows,
6162
bindSuspectTierRows,
6263
COMPOUND_ANCHOR_KEYS,
@@ -279,6 +280,8 @@ import {
279280
withoutAnchor,
280281
workflowEnvValues,
281282
zeroSegmentForms,
283+
readByUiScriptsSuite,
284+
UI_SCRIPTS_SUITE_ROOTS,
282285
} from './dispatch-gates.mjs';
283286
import {
284287
COMPOUND_ANCHOR_LEDGER,
@@ -4831,6 +4834,76 @@ export function selfTest({ tier = 'full' } = {}) {
48314834
t('an undiscoverable durability guard renders STALE for this entry',
48324835
seamStale.filter((l) => l.includes(`⚠ ${DURABILITY_SEAM_GATE}: STALE`)).length === 1);
48334836

4837+
// ── The ui/scripts/__tests__ suite (#26318) ─────────────────────────────────
4838+
//
4839+
// The 178 files run only in CI's Test UI shards, so no family named them and a
4840+
// card under ui package source or skills/objectui learned of a red one CI round
4841+
// later. The entry stands in for that CI JOB (`ciJobRunsRunner`): pinned here
4842+
// are the predicate on all four roots and outside them, the rendering and the
4843+
// runnable union through the whole table, the `--ran` verdict without it, the
4844+
// STALE branch, and the binding against the live workflows.
4845+
const UI_SUITE = 'pnpm --dir ui exec vitest run scripts/';
4846+
const UI_SUITE_SPECIMENS = Object.freeze({
4847+
'ui/packages/**/src': 'ui/packages/sdui-parser/src/expression-carriage.ts',
4848+
'ui/apps/**': 'ui/apps/console/src/App.tsx',
4849+
'skills/objectui/**': 'skills/objectui/SKILL.md',
4850+
'ui/scripts/**': 'ui/scripts/check-skill-examples.mjs',
4851+
});
4852+
const UI_SUITE_CONTROLS = Object.freeze([
4853+
'packages/core/src/x.ts',
4854+
'ui/packages/core/package.json',
4855+
'skills/objectstack/SKILL.md',
4856+
'scripts/pm/dispatch-gates.mjs',
4857+
]);
4858+
const uiEntry = CHANGE_KIND_GATES.filter((k) => k.gates.some((g) => g.name === UI_SUITE));
4859+
t('exactly one entry in the table names the ui/scripts suite, bound to the Test UI job',
4860+
uiEntry.length === 1 && uiEntry[0].matches('ui/scripts/x.mjs') && uiEntry[0].gates[0].ciJob === 'Test UI' && UI_SUITE.startsWith(`${uiEntry[0].gates[0].ciRunner} `));
4861+
t('the roots are the card\'s four, in the trigger language', ['ui/packages/**/src/**', 'ui/apps/**', 'skills/objectui/**', 'ui/scripts/**'].every((r) => UI_SCRIPTS_SUITE_ROOTS.includes(r)));
4862+
for (const [root, specimen] of Object.entries(UI_SUITE_SPECIMENS)) {
4863+
t(`a path under ${root} is read by the ui/scripts suite (${specimen})`, readByUiScriptsSuite(specimen));
4864+
}
4865+
t('a dispatch surface given as a ui package\'s src DIRECTORY counts too', readByUiScriptsSuite('ui/packages/core/src') && readByUiScriptsSuite('./ui/scripts'));
4866+
for (const control of UI_SUITE_CONTROLS) {
4867+
t(`CONTROL: ${control} is outside the four roots`, !readByUiScriptsSuite(control));
4868+
}
4869+
// A resolver that resolves a family by its `pnpm` spelling and a CI-job gate by
4870+
// its own name — the shape `derive`'s resolver has.
4871+
const uiResolved = (name, gate = null) => (gate?.ciJob ? name : `pnpm ${name}`);
4872+
const throughTableUi = (p) => changeKindLines([p], uiResolved).some((l) => l.includes(`- ${UI_SUITE} —`));
4873+
for (const [root, specimen] of Object.entries(UI_SUITE_SPECIMENS)) {
4874+
t(`through the whole table, a diff under ${root} derives the suite, anchored on the delimiter`, throughTableUi(specimen));
4875+
t(`…and the runnable union carries it for ${specimen}`, commandsFor({ kindGroups: changeKindGates([specimen], uiResolved) }).includes(UI_SUITE));
4876+
}
4877+
for (const control of UI_SUITE_CONTROLS) {
4878+
t(`…and ${control} derives no suite line and no suite command`,
4879+
!throughTableUi(control) && !commandsFor({ kindGroups: changeKindGates([control], uiResolved) }).includes(UI_SUITE));
4880+
}
4881+
const uiLine = changeKindLines([UI_SUITE_SPECIMENS['skills/objectui/**']], uiResolved, uiEntry).find((l) => l.includes(`- ${UI_SUITE} —`)) ?? '';
4882+
t('the suite line states its build prerequisite, runnably', /seven @objectstack packages/.test(uiLine) && uiLine.includes('turbo run build') && uiLine.includes('--filter=@objectstack/sdui-parser...'));
4883+
t('…and says it is the whole suite, never a subset', /The whole suite, never a subset/.test(uiLine));
4884+
const uiDerived = commandsFor({ kindGroups: changeKindGates([UI_SUITE_SPECIMENS['ui/apps/**']], uiResolved) });
4885+
const uiWithout = runReconciliation({
4886+
derived: uiDerived,
4887+
record: parseRunRecord(uiDerived.filter((c) => c !== UI_SUITE).map((c) => `${c} :: exit 0`).join('\n')),
4888+
});
4889+
t('⭐ a run record carrying every derived command BUT the suite is NOT ok, and names the suite unrun', !uiWithout.ok && uiWithout.unrun.some((u) => u.command === UI_SUITE));
4890+
const uiWith = runReconciliation({ derived: uiDerived, record: parseRunRecord(uiDerived.map((c) => `${c} :: exit 0`).join('\n')) });
4891+
t('CONTROL: the same record WITH the suite is ok', uiWith.ok);
4892+
const uiStale = changeKindLines([UI_SUITE_SPECIMENS['ui/scripts/**']], () => null, uiEntry);
4893+
t('an unresolvable binding renders STALE, naming the job and the runner it lost',
4894+
uiStale.filter((l) => l.includes(`⚠ ${UI_SUITE}: STALE`) && l.includes("job named 'Test UI…'") && l.includes("'pnpm --dir ui exec vitest run'")).length === 1);
4895+
// The binding itself, against fixtures and then the LIVE workflows.
4896+
const uiJobFixture = (name, run) => [{ file: 'ci.yml', text: `on: pull_request\njobs:\n test-ui:\n name: ${name}\n runs-on: ubuntu-latest\n steps:\n - name: Run\n run: |\n ${run}\n` }];
4897+
const uiBinding = uiEntry[0]?.gates[0] ?? {};
4898+
t('the binding resolves where the job runs the runner', ciJobRunsRunner(uiBinding, uiJobFixture('Test UI (${{ matrix.shard }}/16)', 'pnpm --dir ui exec vitest run --shard=1/16 "${filters[@]}"')));
4899+
t('…and not once the job is renamed', !ciJobRunsRunner(uiBinding, uiJobFixture('UI tests (${{ matrix.shard }}/16)', 'pnpm --dir ui exec vitest run --shard=1/16')));
4900+
t('…nor once the runner is respelled', !ciJobRunsRunner(uiBinding, uiJobFixture('Test UI (${{ matrix.shard }}/16)', 'pnpm --dir ui run test --shard=1/16')));
4901+
t('⭐ the binding resolves against the LIVE workflows — the census guard for a gate no family carries', ciJobRunsRunner(uiBinding, discoverFamilies().workflowEntries));
4902+
t('a half-declared CI-job binding REFUSES at load',
4903+
(() => { try { bindChangeKindRows([{ kind: 'k', matches: 'test-file', gates: [{ name: 'x y', ciJob: 'Test UI', why: 'w' }] }]); return false; } catch (e) { return String(e.message).includes('CI-job binding that is not whole'); } })());
4904+
t('…and so does one whose name does not narrow its runner',
4905+
(() => { try { bindChangeKindRows([{ kind: 'k', matches: 'test-file', gates: [{ name: 'pnpm vitest run', ciJob: 'Test UI', ciRunner: 'pnpm --dir ui exec vitest run', why: 'w' }] }]); return false; } catch (e) { return String(e.message).includes('CI-job binding that is not whole'); } })());
4906+
48344907
// ── The metadata-form edge (#9116) ────────────────────────────────────────
48354908
//
48364909
// The bundles' OTHER producer, and the half no owning-package test can reach:
@@ -5867,7 +5940,9 @@ export function selfTest({ tier = 'full' } = {}) {
58675940
}
58685941
}
58695942
t('the live workflows discover a farm at all (the guard is not vacuous)', liveFamilies.size > 20);
5870-
const declared = CHANGE_KIND_GATES.flatMap((k) => k.gates.map((g) => g.name));
5943+
// A CI-job-subset gate (#26318) is no family by construction; its census is
5944+
// the live-binding case under the ui/scripts entry, so it is left out here.
5945+
const declared = CHANGE_KIND_GATES.flatMap((k) => k.gates.filter((g) => !g.ciJob).map((g) => g.name));
58715946
const missing = declared.filter((n) => !liveFamilies.has(n));
58725947
t(`every convention gate named in the table is a live family (missing: ${missing.join(', ') || 'none'})`, missing.length === 0);
58735948
// The two gates this card moved out of the closing prose, pinned individually
@@ -7474,7 +7549,8 @@ export function selfTest({ tier = 'full' } = {}) {
74747549
// 131 -> 133 (#22998): the header gained two lines naming the full-battery-only jobs.
74757550
// 133 -> 137 (#25867): the header gained four lines on the ui jobs' queue-diff outputs.
74767551
'scripts/pm/check-expected-skips.mjs:137 self-test-reads',
7477-
'scripts/pm/dispatch-gates.mjs:736 inherited-population',
7552+
// 736 -> 738 (#26318): the data import and its re-export each gained UI_SCRIPTS_SUITE_ROOTS.
7553+
'scripts/pm/dispatch-gates.mjs:738 inherited-population',
74787554
].join(' · '),
74797555
censusRows.join(' · '),
74807556
);

0 commit comments

Comments
 (0)