From 175b645b8ad8b12be19322b533722db015a4007d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 06:24:50 +0000 Subject: [PATCH 1/4] test(ui-merge): the Lint UI readers pin the root ci.yml's lint-ui job GitHub runs only the repository root's .github/workflows/. objectui's lint.yml, which five ui tests read, runs nowhere since the monorepo merge; the root ci.yml ported its Lint job as lint-ui (roster row 16 of the #25697 audit), and spells every gate through its ui alias. The bash 3.2 floor and entry-guard wiring pins, the test-path-roots and vi.mock override-shape pins, and lint-workflow's ratchet prose and trigger claims now read that job by path: its steps, its comments, the root `on:` block (which filters no paths) and the filter job's ui globs, which must still match a ui TypeScript source. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS --- .../__tests__/bash32-floor-wiring.test.ts | 32 +++-- .../__tests__/check-test-path-roots.test.ts | 13 +- .../check-vi-mock-override-shape.test.ts | 58 ++++----- .../__tests__/entry-guard-wiring.test.ts | 33 ++++-- ui/scripts/__tests__/lint-workflow.test.ts | 112 ++++++++++-------- 5 files changed, 139 insertions(+), 109 deletions(-) diff --git a/ui/scripts/__tests__/bash32-floor-wiring.test.ts b/ui/scripts/__tests__/bash32-floor-wiring.test.ts index d8e51c6c02d..cf749f1c255 100644 --- a/ui/scripts/__tests__/bash32-floor-wiring.test.ts +++ b/ui/scripts/__tests__/bash32-floor-wiring.test.ts @@ -6,9 +6,11 @@ import { fileURLToPath } from 'node:url'; import { parse as parseYaml } from 'yaml'; import { selfTestCases, stripAnsi } from './helpers/child-verdict'; +import { rootCi, uiRun, UI_JOBS } from './helpers/root-ci'; const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GATE = 'scripts/check-bash32-floor.mjs'; +const ALIAS = 'check:bash32-floor'; /** * objectui#7692: this repository held its own shell to nothing. @@ -36,10 +38,12 @@ const GATE = 'scripts/check-bash32-floor.mjs'; * passes: * * 1. the script exists and `package.json`'s alias points at it; - * 2. `lint.yml` really runs it, in a step that executes, on pull requests; - * 3. the workflow spelling and the alias spelling name the SAME script, so - * `pnpm check:bash32-floor` and the pre-install `node` invocation cannot - * drift into checking different things; + * 2. the root `Lint UI` job (`lint-ui` in the repository root's `ci.yml`, + * where objectstack#25697 ported `lint.yml`) really runs it, in a step + * that executes, on pull requests; + * 3. that step runs the alias, and the alias names the script, so + * `pnpm check:bash32-floor` and the pre-install CI step cannot drift into + * checking different things; * 4. it runs BEFORE `pnpm install` — placed after, an install failure takes * the gate with it and the tree goes unjudged with the job red for an * unrelated reason; @@ -50,9 +54,13 @@ const GATE = 'scripts/check-bash32-floor.mjs'; * whose loss would put the card straight back. */ describe('check-bash32-floor is wired, not merely present', () => { - const workflow = parseYaml(fs.readFileSync(path.join(ROOT, '.github/workflows/lint.yml'), 'utf8')); - const steps: Array> = workflow.jobs.lint.steps; - const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(GATE)); + // objectstack#25712: the job that runs it is the root `ci.yml`'s `lint-ui`, + // where objectstack#25697 ported objectui's `lint.yml` (roster row 16), read + // by path. It spells the gate through its alias, so `ALIAS` is what a step + // runs and the alias case below is what ties that to the file. + const workflow = parseYaml(rootCi()); + const steps: Array> = workflow.jobs[UI_JOBS.lint].steps; + const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(uiRun(ALIAS))); it('the gate script exists', () => { expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); @@ -60,16 +68,16 @@ describe('check-bash32-floor is wired, not merely present', () => { it('package.json aliases it, and the alias points at the script that exists', () => { const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); - const alias = pkg.scripts['check:bash32-floor']; + const alias = pkg.scripts[ALIAS]; expect(alias).toBeTruthy(); expect(alias).toContain(GATE); }); - it('lint.yml runs it — exactly one step, both legs', () => { + it('the root Lint UI job runs it — exactly one step, both legs', () => { expect(gateSteps).toHaveLength(1); const run = gateSteps[0].run as string; - expect(run).toContain(`node ${GATE} --self-test`); - expect(run.split('\n').some((l) => l.trim() === `node ${GATE}`)).toBe(true); + expect(run).toContain(`${uiRun(ALIAS)} --self-test`); + expect(run.split('\n').some((l) => l.trim() === uiRun(ALIAS))).toBe(true); }); it('that step is not disabled — it runs whenever the job runs its other steps', () => { @@ -82,7 +90,7 @@ describe('check-bash32-floor is wired, not merely present', () => { expect(others).toContain(condition); }); - it('lint.yml still gates pull requests, or the step above is inert', () => { + it('the root ci.yml still gates pull requests, or the step above is inert', () => { // `on` parses as the boolean `true` in YAML 1.1; `yaml` gives back `on`. const on = workflow.on ?? workflow[true as unknown as string]; expect(Object.keys(on)).toContain('pull_request'); diff --git a/ui/scripts/__tests__/check-test-path-roots.test.ts b/ui/scripts/__tests__/check-test-path-roots.test.ts index 42dfd552c11..b40f9bea252 100644 --- a/ui/scripts/__tests__/check-test-path-roots.test.ts +++ b/ui/scripts/__tests__/check-test-path-roots.test.ts @@ -38,6 +38,8 @@ import { sitesIn, } from '../check-test-path-roots.mjs'; +import { jobBlock, rootCi, uiRun, UI_JOBS, withoutComments } from './helpers/root-ci'; + /** * The repo root, derived from THIS FILE's own location — never from * `process.cwd()`. That is the rule this file's subject enforces, and a test @@ -236,13 +238,12 @@ describe('check-test-path-roots — the gate itself', () => { const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')) as { scripts: Record }; expect(pkg.scripts['check:test-path-roots']).toBe('node scripts/check-test-path-roots.mjs'); - // `Lint` is in `REQUIRED_CONTEXTS` and subscribes `merge_group`; a step + // objectstack#25712: the job is the root `ci.yml`'s `lint-ui` (`Lint UI`, a + // required context of a workflow that subscribes `merge_group`), where + // objectstack#25697 ported objectui's `lint.yml`; read by path. A step // inside it gates without adding a context to the queue's required set. - const lint = readFileSync(join(REPO_ROOT, '.github/workflows/lint.yml'), 'utf8') - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); - expect(lint).toContain('node scripts/check-test-path-roots.mjs'); + const lint = withoutComments(jobBlock(rootCi(), UI_JOBS.lint)); + expect(lint).toContain(uiRun('check:test-path-roots')); }); it('it goes through the one entry-guard predicate rather than hand-typing one', () => { diff --git a/ui/scripts/__tests__/check-vi-mock-override-shape.test.ts b/ui/scripts/__tests__/check-vi-mock-override-shape.test.ts index 3e4f8064103..27c56439a5a 100644 --- a/ui/scripts/__tests__/check-vi-mock-override-shape.test.ts +++ b/ui/scripts/__tests__/check-vi-mock-override-shape.test.ts @@ -18,6 +18,9 @@ import { summarise, } from '../check-vi-mock-override-shape.mjs'; import { COVERED_SPECIFIERS } from '../check-vi-mock-inherit.mjs'; +import { scan as preInstallScan } from '../check-pre-install-import-graph.mjs'; + +import { globMatches, jobBlock, repoPath, rootCi, uiFilterGlobs, uiRun, UI_JOBS } from './helpers/root-ci'; /** * objectui#8903 — the test for `scripts/check-vi-mock-override-shape.mjs`. @@ -454,54 +457,53 @@ describe('wiring', () => { const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as { scripts: Record; }; - const lint = fs.readFileSync(path.join(repoRoot, '.github/workflows/lint.yml'), 'utf8'); + // objectstack#25712: the job is the root `ci.yml`'s `lint-ui`, where + // objectstack#25697 ported objectui's `lint.yml` (roster row 16), read by path. + const lint = jobBlock(rootCi(), UI_JOBS.lint); it('is reachable as a pnpm script', () => { expect(pkg.scripts['check:vi-mock-override-shape']).toBe(`node scripts/${GATE}`); }); - it('runs in lint.yml, AFTER the install it needs', () => { - const gateAt = lint.indexOf(`node scripts/${GATE}`); + it('runs in the root Lint UI job, AFTER the install it needs', () => { + const gateAt = lint.indexOf(uiRun('check:vi-mock-override-shape')); const installAt = lint.indexOf('pnpm install --frozen-lockfile'); expect(gateAt, 'the gate must be run by a workflow').toBeGreaterThan(-1); expect(installAt).toBeGreaterThan(-1); expect(gateAt, 'the gate imports `typescript`; before the install there is no node_modules').toBeGreaterThan(installAt); }); - it('is NOT run by the pre-install vi-mock workflow', () => { + it('is NOT among the steps any workflow runs before an install', () => { // `check-pre-install-import-graph.mjs` would fail the moment it were: this - // gate reaches the package `typescript`, and that workflow deliberately has - // no install step. Measured on this tree by injecting the same import into - // `check-vi-mock-inherit.mjs`: that gate went from exit 0 to exit 1. - const sibling = fs.readFileSync(path.join(repoRoot, '.github/workflows/repo-gates.yml'), 'utf8'); - expect(sibling).not.toContain(GATE); + // gate reaches the package `typescript`. Measured on this tree by injecting + // the same import into `check-vi-mock-inherit.mjs`: that gate went from + // exit 0 to exit 1. This used to read objectui's install-free + // `repo-gates.yml`; since objectstack#25712 the pre-install population is + // derived from the root workflows, so this asks that derivation. + const { scripts } = preInstallScan(repoRoot) as { scripts: string[] }; + expect(scripts.length, 'the derivation read no pre-install step at all').toBeGreaterThan(0); + expect(scripts).not.toContain(`scripts/${GATE}`); }); - it('the lint.yml skip switch cannot hide a change this gate judges', () => { - // The switch skips the expensive steps when only ignored paths changed. - // Both halves of what this gate compares live in TypeScript files, so no - // ignored pattern may match one. If a pattern that does is ever added, the - // gate silently stops seeing the pull requests most likely to trip it. - const patterns = [...lint.matchAll(/:\(exclude,glob\)([^'"\s]+)/g)].map((m) => m[1]); - expect(patterns.length, 'the ignore list moved — re-read this switch').toBeGreaterThan(0); - - const toRe = (glob: string) => - new RegExp( - `^${glob - .split('**') - .map((part) => part.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')) - .join('.*')}$`, - ); + it('the filter that starts the root Lint UI job cannot hide a change this gate judges', () => { + // objectui's `lint.yml` skipped its expensive steps when only ignored paths + // changed. The root `lint-ui` runs when the `filter` job's `ui` globs + // match, so that list is the switch now (objectstack#25712). Both halves of + // what this gate compares live in TypeScript files, so every one of them + // must start the job, or the gate silently stops seeing the pull requests + // most likely to trip it. + const globs = uiFilterGlobs(rootCi()); const tsPaths = [ 'packages/app-shell/src/views/RecordDetailView.headerRefresh.test.tsx', 'packages/collaboration/src/PresenceProvider.tsx', 'scripts/check-vi-mock-override-shape.mjs', 'packages/collaboration/src/index.ts', ]; - for (const glob of patterns) { - for (const p of tsPaths) { - expect(toRe(glob).test(p), `ignore pattern ${glob} matches ${p} — this gate would go blind`).toBe(false); - } + for (const p of tsPaths) { + expect( + globs.some((glob) => globMatches(glob, repoPath(p))), + `no \`ui\` filter glob matches ${repoPath(p)} — this gate would go blind`, + ).toBe(true); } }); }); diff --git a/ui/scripts/__tests__/entry-guard-wiring.test.ts b/ui/scripts/__tests__/entry-guard-wiring.test.ts index 289138c87e9..d44aedec702 100644 --- a/ui/scripts/__tests__/entry-guard-wiring.test.ts +++ b/ui/scripts/__tests__/entry-guard-wiring.test.ts @@ -6,9 +6,11 @@ import { fileURLToPath } from 'node:url'; import { parse as parseYaml } from 'yaml'; import { selfTestCases, stripAnsi } from './helpers/child-verdict'; +import { rootCi, uiRun, UI_JOBS } from './helpers/root-ci'; const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GATE = 'scripts/check-entry-guard.mjs'; +const ALIAS = 'check:entry-guard'; /** * objectui#6092: `scripts/invoked-as.mjs` arrived from objectstack (#5984) @@ -32,11 +34,12 @@ const GATE = 'scripts/check-entry-guard.mjs'; * 1. the script exists and its `package.json` alias points at it — the alias * is what a developer runs, and an alias naming a moved file fails loudly * only if something runs it; - * 2. `lint.yml` really runs it, on pull requests, in a step that executes — - * the wiring claim itself; - * 3. the workflow's spelling and the alias's spelling name the SAME script, - * so the pre-install `node` invocation and `pnpm check:entry-guard` cannot - * drift into checking different things; + * 2. the root `Lint UI` job (`lint-ui` in the repository root's `ci.yml`, + * where objectstack#25697 ported `lint.yml`) really runs it, on pull + * requests, in a step that executes — the wiring claim itself; + * 3. that step runs the alias, and the alias names the script, so the + * pre-install CI step and `pnpm check:entry-guard` cannot drift into + * checking different things; * 4. it runs BEFORE `pnpm install`, which is not decoration: placed after, * an install failure would take the gate with it and the tree would go * unjudged with the job red for an unrelated reason; @@ -50,9 +53,13 @@ const GATE = 'scripts/check-entry-guard.mjs'; * records at length for this same workflow. */ describe('check-entry-guard is wired, not merely present', () => { - const workflow = parseYaml(fs.readFileSync(path.join(ROOT, '.github/workflows/lint.yml'), 'utf8')); - const steps: Array> = workflow.jobs.lint.steps; - const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(GATE)); + // objectstack#25712: the job that runs it is the root `ci.yml`'s `lint-ui`, + // where objectstack#25697 ported objectui's `lint.yml` (roster row 16), read + // by path. It spells the gate through its alias, so `ALIAS` is what a step + // runs and the alias case below is what ties that to the file. + const workflow = parseYaml(rootCi()); + const steps: Array> = workflow.jobs[UI_JOBS.lint].steps; + const gateSteps = steps.filter((s) => typeof s.run === 'string' && (s.run as string).includes(uiRun(ALIAS))); it('the gate script exists', () => { expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); @@ -60,16 +67,16 @@ describe('check-entry-guard is wired, not merely present', () => { it('package.json aliases it, and the alias points at the script that exists', () => { const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); - const alias = pkg.scripts['check:entry-guard']; + const alias = pkg.scripts[ALIAS]; expect(alias).toBeTruthy(); expect(alias).toContain(GATE); }); - it('lint.yml runs it — exactly one step, both legs', () => { + it('the root Lint UI job runs it — exactly one step, both legs', () => { expect(gateSteps).toHaveLength(1); const run = gateSteps[0].run as string; - expect(run).toContain(`node ${GATE} --self-test`); - expect(run.split('\n').some((l) => l.trim() === `node ${GATE}`)).toBe(true); + expect(run).toContain(`${uiRun(ALIAS)} --self-test`); + expect(run.split('\n').some((l) => l.trim() === uiRun(ALIAS))).toBe(true); }); it('that step is not disabled — it runs whenever the job runs its other steps', () => { @@ -84,7 +91,7 @@ describe('check-entry-guard is wired, not merely present', () => { expect(others).toContain(condition); }); - it('lint.yml still gates pull requests, or the step above is inert', () => { + it('the root ci.yml still gates pull requests, or the step above is inert', () => { // `on` parses as the boolean `true` in YAML 1.1; `yaml` gives back `on`. const on = workflow.on ?? workflow[true as unknown as string]; expect(Object.keys(on)).toContain('pull_request'); diff --git a/ui/scripts/__tests__/lint-workflow.test.ts b/ui/scripts/__tests__/lint-workflow.test.ts index dfa6272affc..dce1f34a1d4 100644 --- a/ui/scripts/__tests__/lint-workflow.test.ts +++ b/ui/scripts/__tests__/lint-workflow.test.ts @@ -3,6 +3,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { globMatches, jobBlock, repoPath, rootCi, triggerBlock, uiFilterGlobs, UI_JOBS, withoutComments } from './helpers/root-ci'; + /** * objectui#3261: the header comment in `.github/workflows/lint.yml` said * `eslint.config.js` sets *three* `object-ui/*` rules to `error` and named the @@ -78,17 +80,29 @@ import { fileURLToPath } from 'node:url'; * `eslint.config.js`, where each rule's ADR/issue sits in the comment beside it. * A reference cannot drift out of step with its target; a copy of its contents * is the thing that had already gone stale three times. + * + * ## The workflow is the root's now (objectstack#25712) + * + * GitHub runs only the repository root's `.github/workflows/`, and + * objectstack#25697 ported `lint.yml`'s `Lint` job to the root `ci.yml` as + * `lint-ui` (roster row 16). objectui's own `lint.yml` runs nowhere, so the + * workflow surface is that job: its text for the rule-name check, its comments + * for the count check. The trigger claims are the root's too. Its `on:` block + * filters no paths at all, so the ignore list the second claim read is gone; a + * pull request reaches `lint-ui` through the `filter` job's `ui` globs, and + * those are what may not skip a TypeScript source. */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const read = (relative: string): string => fs.readFileSync(path.join(repoRoot, relative), 'utf8'); /** Repo-relative, so the same string labels the surface in failure messages. */ -const WORKFLOW = '.github/workflows/lint.yml'; +const WORKFLOW = `ci.yml (${UI_JOBS.lint})`; const COVERAGE_SCRIPT = 'scripts/check-lint-coverage.mjs'; const DOC = 'content/docs/guide/ci-cd-pipeline.md'; -const workflow = read(WORKFLOW); +const ci = rootCi(); +const workflow = jobBlock(ci, UI_JOBS.lint); const coverageScript = read(COVERAGE_SCRIPT); const doc = read(DOC); @@ -120,16 +134,14 @@ const objectUiErrorRules = eslintConfig .map(([name]) => name); /** - * The `# …` prose block between `name:` and `on:` — the workflow comment this - * issue is about. Scoped so the assertions below read the explanation, not the - * YAML. + * The job's `# …` comment lines, the explanation its steps carry. Scoped so the + * count assertion reads the prose, not the YAML. */ -function workflowHeaderComment(): string { - const start = workflow.indexOf('\n#'); - const end = workflow.indexOf('\non:'); - expect(start, `${WORKFLOW} must still carry a header comment above \`on:\``).toBeGreaterThan(-1); - expect(end, `${WORKFLOW} must still have a top-level \`on:\` block`).toBeGreaterThan(start); - return workflow.slice(start, end); +function workflowComments(): string { + return workflow + .split('\n') + .filter((line) => /^\s*#/.test(line)) + .join('\n'); } /** @@ -191,7 +203,7 @@ const SURFACES: ProseSurface[] = [ { label: WORKFLOW, text: workflow, - prose: () => unwrap(workflowHeaderComment(), /^[ \t]*#[ \t]?/gm), + prose: () => unwrap(workflowComments(), /^[ \t]*#[ \t]?/gm), }, { label: COVERAGE_SCRIPT, @@ -211,8 +223,10 @@ const SURFACES: ProseSurface[] = [ * word `pull_request` also appears in `concurrency`/`github.ref` expressions. */ function onBlock(): string { - const start = workflow.indexOf('\non:'); - const rest = workflow.slice(start + 1); + const triggers = withoutComments(triggerBlock(ci)); + const start = triggers.indexOf('\non:'); + expect(start, 'the root ci.yml must still have a top-level `on:` block').toBeGreaterThan(-1); + const rest = triggers.slice(start + 1); const next = rest.search(/\n[a-z_]+:/); return next === -1 ? rest : rest.slice(0, next); } @@ -296,54 +310,52 @@ describe('the premise both comments rest on', () => { }); }); -describe('lint.yml — the trigger claims its header comment still makes', () => { +describe('the root Lint UI job — the trigger claims the ratchets rest on', () => { it('still runs on pull requests, so those rules are not inert again (#2923)', () => { const on = onBlock(); expect( on, - `lint.yml no longer triggers on \`pull_request\`. That is exactly the state ` + - `#2923 fixed: every \`object-ui/*\` rule \`eslint.config.js\` sets to \`error\` ` + - `goes silently inert, because the only thing that runs them on a PR is this ` + - `workflow. The header comment describes that as history — keep it history.`, + `the root ci.yml no longer triggers on \`pull_request\`. That is exactly the ` + + `state #2923 fixed: every \`object-ui/*\` rule \`eslint.config.js\` sets to ` + + `\`error\` goes silently inert, because the only thing that runs them on a PR ` + + `is the ${UI_JOBS.lint} job.`, ).toMatch(/^\s{2}pull_request:/m); + + const condition = workflow.match(/^ {4}if: (.*)$/m)?.[1] ?? ''; + expect( + condition, + `${WORKFLOW}'s \`if:\` reads the event name, so some event no longer lints: ${condition}`, + ).not.toMatch(/event_name/); }); it('does not skip the TypeScript sources those rules lint', () => { - // A narrow tripwire, not a glob engine (the repo has no glob matcher at the - // root, and vendoring one for this would cost more than it protects). The - // realistic way to un-gate the ratchets without touching the triggers above - // is adding a TypeScript pattern to the ignore list; today's entries are all - // markdown, `content/**`, `docs/**` and `.changeset/**`. - // - // That list now lives in TWO places and both have to be read, which is the - // objectui#3523 change: `paths-ignore` stayed on the `push` trigger, but for - // pull requests it moved INTO the job, as `:(exclude,glob)…` pathspecs in - // the `Decide whether this change needs a full run` step. Reading only the - // `on:` block would have gone on passing while a `**/*.ts` exclusion was - // added to the job — the gate reporting green having linted nothing. - // `merge-queue-reporting.test.ts` pins the two lists to each other and pins - // why the trigger may not filter pull requests at all; this one stays on its - // own question, which is what may be in the list. - const ignored = [ - ...[...onBlock().matchAll(/^\s*-\s*'([^']+)'/gm)].map((m) => m[1]), - ...[...workflow.matchAll(/':\(exclude,glob\)([^']+)'/g)].map((m) => m[1]), - ]; - - expect( - ignored.length, - 'lint.yml must still declare an ignore list — on the `push` trigger, in the job, or both', - ).toBeGreaterThan(0); + // The realistic way to un-gate the ratchets without touching the triggers + // above is a filter that stops matching TypeScript. objectui's `lint.yml` + // kept an ignore list in two places (`paths-ignore` on `push`, and + // `:(exclude,glob)` pathspecs in the job for pull requests, objectui#3523). + // The root ci.yml keeps neither: its `on:` block filters no paths, and the + // `filter` job's `ui` globs decide whether `lint-ui` runs. So both are read + // here: no paths filter comes back on the trigger, no exclusion appears in + // the job, and a ui `.ts` or `.tsx` change still matches a `ui` glob. + expect(onBlock(), 'the root ci.yml\'s `on:` block filters paths again').not.toMatch( + /^\s+paths(-ignore)?:/m, + ); + const excluded = [...withoutComments(workflow).matchAll(/':\(exclude,glob\)([^']+)'/g)].map((m) => m[1]); + expect(excluded, `${WORKFLOW} excludes paths from its run`).toEqual([]); - const swallowsSource = ignored.filter((pattern) => /\.tsx?$/.test(pattern)); + const globs = uiFilterGlobs(ci); + const missed = ['packages/core/src/index.ts', 'packages/components/src/index.ts', 'apps/console/src/App.tsx'] + .map(repoPath) + .filter((file) => !globs.some((glob) => globMatches(glob, file))); expect( - swallowsSource, - `lint.yml skips TypeScript changes:\n` + - swallowsSource.map((p) => ` - ${p}`).join('\n') + + missed, + `the root \`filter\` job's \`ui\` globs no longer match these TypeScript sources:\n` + + missed.map((p) => ` - ${p}`).join('\n') + `\n\nEvery \`object-ui/*\` rule \`eslint.config.js\` sets to \`error\` only gates ` + - `a PR because this workflow lints the .ts/.tsx it touches. Excluding them is the ` + - `pre-#2923 inert state with extra steps — and since objectui#3523 it is the quieter ` + - `version of it, because the \`Lint\` check still reports, green.`, + `a PR because ${UI_JOBS.lint} lints the .ts/.tsx it touches. A filter that skips ` + + `them is the pre-#2923 inert state with extra steps, and the quieter version of ` + + `it, because \`Lint UI\` still reports, skipped, which counts as success.`, ).toEqual([]); }); }); From 2fe7431353a0264b650bc4e222d372a9f3e6540d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 06:24:50 +0000 Subject: [PATCH 2/4] test(ui-merge): the pins on objectui's governance workflows retire with their roster rows Thirteen ui tests, and check-governed-queue-guard's own --self-test, pinned how a workflow under ui/.github/workflows/ runs a gate: changeset-guard, changeset-presence, performance-budget, governed-surface-guard, merge-queue-head-patrol, line-citation-gate, shadcn-check, check-links, required-check-set-patrol and dependabot-auto-merge. None of them runs under the monorepo, and no root workflow runs these gates, so those pins assert nothing GitHub does. Each retired case is named where it stood, with the row of the #25697 workflow roster that carries the workflow (rows 3, 4, 6, 9, 11, 15, 20, 22, 25, 26), and the port that brings the pins back against a root workflow, by path. Every case that reads a script, a fixture, a config or package.json stays. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01A6SEitNngAMugGbuJHWsDS --- .../__tests__/check-changeset-claims.test.ts | 171 +++----------- .../check-changeset-no-major.test.ts | 53 ++--- .../check-changeset-overwrite.test.ts | 74 ++---- .../check-changeset-presence.test.ts | 127 ++-------- .../check-eager-closure-budget.test.ts | 106 +++------ .../check-governed-queue-guard.test.ts | 83 ++----- .../__tests__/check-links-workflow.test.ts | 160 ++----------- .../__tests__/check-merge-queue-head.test.ts | 113 ++------- ...heck-new-cross-file-line-citations.test.ts | 36 +-- .../check-required-check-set.test.ts | 78 ++----- .../__tests__/render-budget-comment.test.ts | 219 ++---------------- .../render-changeset-claims-comment.test.ts | 18 +- .../__tests__/shadcn-check-report.test.ts | 92 ++------ ui/scripts/check-governed-queue-guard.mjs | 46 ++-- ui/scripts/markdown-test-inputs.mjs | 9 - 15 files changed, 273 insertions(+), 1112 deletions(-) diff --git a/ui/scripts/__tests__/check-changeset-claims.test.ts b/ui/scripts/__tests__/check-changeset-claims.test.ts index 7db3554ba5d..b5759a01742 100644 --- a/ui/scripts/__tests__/check-changeset-claims.test.ts +++ b/ui/scripts/__tests__/check-changeset-claims.test.ts @@ -35,11 +35,14 @@ import { * What this file pins, in the order the gate can fail: * * 1. **It can start on the pull request that needs it.** The falsifying change - * is an ordinary source change, so the gate lives in the workflow with NO - * path filter. That absence is the load-bearing property and is pinned here: - * adding a `paths:` filter to that workflow would blind this gate silently, - * which is the exact failure mode `changeset-guard.yml` can afford and this - * one cannot. + * is an ordinary source change, so the gate lived in the workflow with NO + * path filter, and that absence was pinned here. Retired with + * objectstack#25712, with the delivery pins of section 1b and the comment + * step's count pins in the corpus section: they read objectui's + * `changeset-presence.yml`, which runs nowhere under the monorepo (GitHub + * runs the root `.github/workflows/` alone). It is row 4 of the + * objectstack#25697 workflow roster (→ sub-issue 4), and a port brings them + * back against its root workflow, by path. * 2. **Both directions of the verdict**, against throwaway repositories rather * than this one's history, so they stay decidable when the history moves. A * report that fires, and a FIRING CONTROL — the same fixture with the named @@ -61,24 +64,6 @@ import { */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const GATE = 'scripts/check-changeset-claims.mjs'; -const WORKFLOW = '.github/workflows/changeset-presence.yml'; - -/** - * A workflow's YAML with whole-line comments removed. - * - * Required, not cosmetic: this workflow's header discusses its own trigger set - * at length, so a scan that counted the prose would report filter entries the - * file does not have. Same helper, same reason, as in the sibling changeset - * gate tests. - */ -function withoutComments(yaml: string): string { - return yaml - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); -} - -const workflowYaml = withoutComments(fs.readFileSync(path.join(repoRoot, WORKFLOW), 'utf8')); // ── fixture repositories ───────────────────────────────────────────────────── @@ -200,88 +185,27 @@ function lastCommitRange(fixture: Fixture): string[] { } // ── 1. the gate can start on the pull request that needs it ────────────────── - -describe('changeset-presence.yml — where this gate has to live', () => { - it('exists, and the workflow runs it', () => { - expect(fs.existsSync(path.join(repoRoot, GATE)), `${GATE} must exist for the workflow to run it`).toBe(true); - expect(workflowYaml).toMatch(new RegExp(`run:\\s*node\\s+${GATE.replace(/[.]/g, '\\.')}`)); - }); - - it('gives the job the history its diff needs', () => { - // The gate compares this change against its MERGE BASE. checkout's default - // is a depth-1 clone where `git merge-base` has nothing to find, and an - // unresolvable base is exit 1 in the script rather than a skip — so getting - // this wrong is a red build, not a silent pass. Pinned so it stays that way. - expect(workflowYaml).toMatch(/fetch-depth:\s*0/); - }); - - it('carries NO paths filter — the property this gate depends on', () => { - // `changeset-guard.yml` can afford a `paths: ['.changeset/**']` filter - // because a change that modifies a changeset touches `.changeset/**` BY - // DEFINITION. This gate's subject is the opposite: an ordinary source change - // that falsifies somebody else's pending claim, which is under no obligation - // to touch `.changeset/**` at all. A path filter added here would blind the - // gate with no red anywhere, so its ABSENCE is pinned. - expect(workflowYaml).not.toMatch(/^\s*paths(-ignore)?:/m); - }); -}); - +// +// Retired with objectstack#25712: `exists, and the workflow runs it`, `gives the +// job the history its diff needs` and `carries NO paths filter — the property +// this gate depends on`. +// // ── 1b. the finding is DELIVERED, not archived (objectui#9140) ─────────────── - -describe('delivery — the ruling that the finding reaches the pull request', () => { - // objectui#9140 measured the "REQUEST TO READ" at zero answers out of four - // live instances, because it was addressed to a job log nobody opens on a - // green check. The director's ruling (maintainer 「同意」) moves WHERE the - // finding is read and ⛔ nothing else: exit 0, not a required context, - // nothing blocks. - it('hands this run\'s finding set to the renderer instead of re-measuring it', () => { - // One measurement, two consumers. A report that re-derives its own subject - // can disagree with the log it claims to report, and the disagreement is - // invisible to both sides. - expect(workflowYaml).toMatch(/node\s+scripts\/check-changeset-claims\.mjs\s+--json\s+claims\.json/); - expect(workflowYaml).toMatch( - /node\s+scripts\/render-changeset-claims-comment\.mjs\s+--from\s+claims\.json/, - ); - }); - - it('posts through the channel the Console Performance Budget report uses', () => { - expect(workflowYaml).toMatch(/uses:\s*actions\/github-script@/); - const budget = withoutComments( - fs.readFileSync(path.join(repoRoot, '.github/workflows/performance-budget.yml'), 'utf8'), - ); - expect(budget).toMatch(/uses:\s*actions\/github-script@/); - }); - - it('holds `pull-requests: write` on the JOB, leaving the declaration gate with a read-only token', () => { - // A job-level block REPLACES the workflow-level one, so `contents: read` - // has to be named again or checkout loses it. The workflow-level block must - // stay read-only: the declaration gate above needs nothing but a checkout, - // and a grant it does not use is a grant nobody is auditing. - expect(workflowYaml).toMatch(/permissions:\n\s+contents: read\n\s+pull-requests: write/); - expect(workflowYaml).toMatch(/^permissions:\n\s+contents: read\n/m); - expect(workflowYaml).not.toMatch(/^permissions:\n\s+contents: read\n\s+pull-requests/m); - }); - - it('⛔ never subscribes `pull_request_target`, which would run this on a fork\'s branch with a write token', () => { - expect(workflowYaml).not.toMatch(/pull_request_target/); - }); - - it('comments only on a pull request', () => { - // A `merge_group` build has no pull request to comment on, and the gate - // still has to REPORT there — so the delivery steps are conditioned and the - // measuring step is not. - const conditions = workflowYaml.match(/if: \$\{\{ github\.event_name == 'pull_request' \}\}/g) ?? []; - expect(conditions.length).toBe(2); - }); - - it('cannot paint this job red by failing to post', () => { - // A fork's pull request gets a read-only token and the API call fails for a - // reason that says nothing about changesets. Report-only means the gate - // declines to fail on its FINDINGS; it must not start failing on its - // PLUMBING either. - expect(workflowYaml).toMatch(/continue-on-error: true/); - }); -}); +// +// Retired with objectstack#25712: the six delivery cases (the run hands its +// `--json` finding set to `render-changeset-claims-comment.mjs`, posts through +// `actions/github-script` as `performance-budget.yml` does, holds +// `pull-requests: write` on the job alone, never subscribes +// `pull_request_target`, comments only on a pull request, and cannot paint the +// job red by failing to post). The ruling they pinned stands: the finding +// reaches the pull request, exit 0, not a required context, nothing blocks. +// +// Every one of them read objectui's `changeset-presence.yml` (one also read its +// `performance-budget.yml`, roster row 22). Both run nowhere under the +// monorepo: GitHub runs the root `.github/workflows/` alone, and no root +// workflow runs this gate. `changeset-presence.yml` is row 4 of the +// objectstack#25697 workflow roster (→ sub-issue 4); a port brings these pins +// back against its root workflow, by path. // ── 2. both directions of the verdict ──────────────────────────────────────── @@ -953,35 +877,16 @@ describe('the corpus is the prose this change publishes about itself', () => { expect(gate).toContain('check-new-cross-file-line-citations.mjs'); }); - it('delivers a born-false-only finding instead of leaving it in the job log', () => { - // objectui#9140 measured the job-log channel at zero answers out of four. - // The comment step decides whether to post from the finding COUNT, so a run - // whose only finding is born-false has to be counted there too. - expect(workflowYaml).toContain('(measured.bornFalse ?? []).length'); - }); - - it('⭐ delivers a self-contradiction-only finding too (objectui#9841)', () => { - // ⚠️ ASSERTED AS ITS OWN TERM, ⛔ never as the whole sum. A pin that matched - // the sum verbatim would redden on a harmless rewording and — worse — would - // be repaired by pasting whatever the file now says, which is how a dropped - // term gets blessed. This one goes red for exactly one reason: this reading - // stopped being counted, and its findings stopped creating the comment. - expect(workflowYaml).toContain('(measured.selfContradiction ?? []).length'); - }); - - it('counts EVERY reading the gate carries — one term per reading, none left out', () => { - // The standing obligation, stated where the next reading's author will read - // it: a reading that is measured, rendered and then not counted here is - // delivered to a job log nobody opens. That is objectui#9842's shape, and it - // is the reason this surface was extended rather than left for later. - for (const term of [ - 'measured.findings.length', - '(measured.bornFalse ?? []).length', - '(measured.selfContradiction ?? []).length', - ]) { - expect(workflowYaml).toContain(term); - } - }); + // Retired with objectstack#25712: `delivers a born-false-only finding instead + // of leaving it in the job log`, `⭐ delivers a self-contradiction-only finding + // too (objectui#9841)` and `counts EVERY reading the gate carries — one term + // per reading, none left out`. Each read the comment step's finding count in + // objectui's `changeset-presence.yml` (`measured.findings.length`, + // `(measured.bornFalse ?? []).length`, + // `(measured.selfContradiction ?? []).length`), which runs nowhere under the + // monorepo. The obligation is the port's: the root workflow that delivers + // this gate's findings counts every reading it carries, and these pins come + // back against it, by path (roster row 4, → sub-issue 4). }); describe('the boundary control the first ablation of this change exposed', () => { diff --git a/ui/scripts/__tests__/check-changeset-no-major.test.ts b/ui/scripts/__tests__/check-changeset-no-major.test.ts index d34eae6e74e..7ce92d29a89 100644 --- a/ui/scripts/__tests__/check-changeset-no-major.test.ts +++ b/ui/scripts/__tests__/check-changeset-no-major.test.ts @@ -48,14 +48,15 @@ const mask: (source: string) => string = maskComments; * group's own manifests (objectui#12334, under the maintainer's ruling on * objectui#12030). The fixtures below pin both directions of that computation. * - * These tests are the second lock. `.github/workflows/changeset-guard.yml` - * catches it on the PR that adds the changeset; the repo-state test below - * catches it in `pnpm test` even if that workflow is ever removed or its - * trigger stops matching. + * These tests are the second lock. objectui's `changeset-guard.yml` caught it + * on the PR that added the changeset; the repo-state test below catches it in + * `pnpm test` even if that workflow is removed or its trigger stops matching. + * Under the monorepo that workflow runs nowhere (GitHub runs the root + * `.github/workflows/` alone), so the repo-state test, in `Test UI`, is the + * lock that runs until sub-issue 4 ports the guard (objectstack#25712). */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const changesetDir = path.join(repoRoot, '.changeset'); -const workflowDir = path.join(repoRoot, '.github/workflows'); const GATE = path.join(repoRoot, 'scripts/check-changeset-no-major.mjs'); interface Entry { @@ -410,37 +411,11 @@ describe('the repository itself', () => { }); }); -describe('changeset-guard.yml', () => { - const guard = fs.readFileSync(path.join(workflowDir, 'changeset-guard.yml'), 'utf8'); - - it('runs the guard script', () => { - expect(guard).toContain('node scripts/check-changeset-no-major.mjs'); - }); - - it('triggers on `.changeset/**` — the paths every other workflow ignores', () => { - // The inverse pin, and the reason `ci.yml` still cannot host this check: on - // a changeset-only PR its `Decide whether this change needs a full run` step - // finds nothing outside its exclusion list (markdown and `.changeset/**` - // among it) and skips every step below itself, so no gate in that workflow - // ever reads the changeset. This one has to run outside that decision. - // - // Not the reason this comment used to give — "a changeset-only PR matches - // its `paths-ignore` twice over, so no job in it would ever run". Since - // objectui#3523 step 2, `paths-ignore` is gone from the `pull_request` - // trigger and survives only on `push`: such a PR does start `ci.yml` and - // does produce its contexts (measured — PR #3856, one markdown file, 16 - // checks; PR #4339, one line added to AGENTS.md, 17). The conclusion - // outlived the premise; objectui#3857 pinned the correction. - // - // That surviving `push` copy is what the two assertions below read, and it - // is why they stay green — `ci.yml` genuinely still declares `paths-ignore` - // with markdown in it, on `push`. The in-job exclusion list is held - // identical to that filter by `scripts/__tests__/merge-queue-reporting.test.ts`, - // so the pair cannot drift apart silently. - expect(guard).toMatch(/paths:\s*\n\s*- '\.changeset\/\*\*'/); - - const ci = fs.readFileSync(path.join(workflowDir, 'ci.yml'), 'utf8'); - expect(ci).toContain("paths-ignore:"); - expect(ci).toContain("- '**/*.md'"); - }); -}); +// `changeset-guard.yml` (`runs the guard script`, and `triggers on +// .changeset/** — the paths every other workflow ignores`) retired with +// objectstack#25712. Both cases read objectui's `changeset-guard.yml` and its +// `ci.yml`, which run nowhere under the monorepo: GitHub runs the root +// `.github/workflows/` alone, and no root workflow runs this gate. The guard is +// row 3 of the objectstack#25697 workflow roster (→ sub-issue 4); a port brings +// these pins back against its root workflow, by path. The repo-state cases +// above are the lock that runs meanwhile. diff --git a/ui/scripts/__tests__/check-changeset-overwrite.test.ts b/ui/scripts/__tests__/check-changeset-overwrite.test.ts index d909cf86c08..1b00eb2be51 100644 --- a/ui/scripts/__tests__/check-changeset-overwrite.test.ts +++ b/ui/scripts/__tests__/check-changeset-overwrite.test.ts @@ -20,12 +20,13 @@ import { declaredEntries } from '../check-changeset-overwrite.mjs'; * * What this file pins, in the order the gate can fail: * - * 1. **The workflow can start on the pull request that needs it.** A path - * filter is normally a false-negative surface; here it cannot be, because a - * change that modifies a `.changeset/*.md` touches `.changeset/**` by - * definition. The filter must still carry the gate's OWN closure — its YAML - * and the script it runs (objectui#6321) — or the pull request that changes - * the gate is not the one that runs it. + * 1. **The workflow can start on the pull request that needs it.** Retired + * with objectstack#25712. These pins read objectui's `changeset-guard.yml` + * (the gate's run step, `fetch-depth: 0`, its own closure in both `paths:` + * filters, the `.changeset/**` entry), and that workflow runs nowhere under + * the monorepo: GitHub runs the root `.github/workflows/` alone. It is row 3 + * of the objectstack#25697 workflow roster (→ sub-issue 4), and a port + * brings these pins back against its root workflow, by path. * 2. **The verdicts**, against throwaway repositories rather than this one's * history, so they stay decidable when the history moves. Both directions: * a modification of a pre-existing changeset is REPORTED, and a change that @@ -45,7 +46,6 @@ import { declaredEntries } from '../check-changeset-overwrite.mjs'; */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const GATE = 'scripts/check-changeset-overwrite.mjs'; -const WORKFLOW = '.github/workflows/changeset-guard.yml'; /** * The peer release declaration every fixture repository starts with — the file @@ -67,23 +67,6 @@ const WORKFLOW = '.github/workflows/changeset-guard.yml'; */ const PEER_CHANGESET = '.changeset/fixture-olive-donkeys-smile.md'; -/** - * A workflow's YAML with whole-line comments removed. - * - * Required, not cosmetic: this workflow's header discusses `paths` and its own - * self-coverage at length, so a scan that counted the prose would report filter - * entries the file does not have. Same helper, same reason, as in - * `check-changeset-presence.test.ts` and `merge-queue-reporting.test.ts`. - */ -function withoutComments(yaml: string): string { - return yaml - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); -} - -const workflowYaml = withoutComments(fs.readFileSync(path.join(repoRoot, WORKFLOW), 'utf8')); - // ── fixture repositories ───────────────────────────────────────────────────── const fixtures: string[] = []; @@ -177,40 +160,15 @@ function lastCommitRange(fixture: Fixture): string[] { } // ── 1. the workflow is reachable, and covers itself ────────────────────────── - -describe('changeset-guard.yml — the gate can start on the pull request that needs it', () => { - it('exists, and runs the gate script', () => { - expect(fs.existsSync(path.join(repoRoot, GATE)), `${GATE} must exist for the workflow to run it`).toBe(true); - expect(workflowYaml).toMatch(new RegExp(`run:\\s*node\\s+${GATE.replace(/[.]/g, '\\.')}`)); - }); - - it('gives the job the history its diff needs', () => { - // The gate compares this change against its MERGE BASE with the target - // branch. checkout's default is a depth-1 clone where `git merge-base` has - // nothing to find, and an unresolvable base is a hard failure in the script - // — so getting this wrong is a red build rather than a silent pass. Pinned - // so it stays that way. - expect(workflowYaml).toMatch(/fetch-depth:\s*0/); - }); - - it("carries the gate's own closure in the paths filter (objectui#6321)", () => { - // Without this, the pull request that edits the gate is not the pull request - // that runs it, and the first real execution lands on somebody else's - // unrelated `.changeset/**` change. `paths:` appears twice — `pull_request` - // and `push` — and both must carry it. - const occurrences = workflowYaml.split(GATE).length - 1; - expect(occurrences, `${GATE} must appear in BOTH paths: filters and the run: step`).toBeGreaterThanOrEqual(3); - expect(workflowYaml.split(WORKFLOW).length - 1, 'the workflow must list its own YAML').toBeGreaterThanOrEqual(2); - }); - - it('still triggers on .changeset/** — the filter this gate depends on', () => { - // A path filter is normally a false-negative surface. It is not here, and - // only because of this entry: a change that modifies or deletes a - // `.changeset/*.md` touches `.changeset/**` BY DEFINITION, so the filter - // cannot skip the case the gate exists for. - expect(workflowYaml).toMatch(/- '\.changeset\/\*\*'/); - }); -}); +// +// Retired with objectstack#25712: `exists, and runs the gate script`, `gives the +// job the history its diff needs`, `carries the gate's own closure in the paths +// filter (objectui#6321)` and `still triggers on .changeset/**`. Each read +// objectui's `changeset-guard.yml`, which runs nowhere under the monorepo: +// GitHub runs the root `.github/workflows/` alone, and no root workflow runs +// this gate. That workflow is row 3 of the objectstack#25697 workflow roster +// (→ sub-issue 4); a port brings these pins back against its root workflow, by +// path. The verdicts below never read a workflow and stand as they were. // ── 2. the verdicts ────────────────────────────────────────────────────────── diff --git a/ui/scripts/__tests__/check-changeset-presence.test.ts b/ui/scripts/__tests__/check-changeset-presence.test.ts index 2010053dc67..a91dd064a5f 100644 --- a/ui/scripts/__tests__/check-changeset-presence.test.ts +++ b/ui/scripts/__tests__/check-changeset-presence.test.ts @@ -34,6 +34,12 @@ import { * start is the shape objectui#3523 spent a P0 on, and this gate's own subject * matter makes it acute: `changeset-guard.yml` is invisible to a PR that * forgot its changeset precisely because it filters on `.changeset/**`. + * Retired with objectstack#25712: these pins read objectui's + * `changeset-presence.yml` and `changeset-guard.yml`, which run nowhere + * under the monorepo (GitHub runs the root `.github/workflows/` alone). The + * two are rows 4 and 3 of the objectstack#25697 workflow roster + * (→ sub-issue 4), and a port brings them back against its root workflow, + * by path. * 2. **The guarded surface is DERIVED**, not a hand-written glob. The issue * proposed one glob under `packages/`; `@object-ui/console` lives at * `apps/console` and would have been missed — the most-edited published @@ -63,25 +69,6 @@ import { */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const GATE = 'scripts/check-changeset-presence.mjs'; -const WORKFLOW = '.github/workflows/changeset-presence.yml'; -const workflowDir = path.join(repoRoot, '.github/workflows'); - -/** - * A workflow's YAML with whole-line comments removed. - * - * Required, not cosmetic: this workflow's header discusses `paths`, - * `paths-ignore` and `push` at length — a scan that counted the prose would - * report filters and triggers the file does not have. Same helper, same reason, - * as in `docs-links-workflow.test.ts` and `merge-queue-reporting.test.ts`. - */ -function withoutComments(yaml: string): string { - return yaml - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); -} - -const workflowYaml = withoutComments(fs.readFileSync(path.join(repoRoot, WORKFLOW), 'utf8')); // ── fixture repositories ───────────────────────────────────────────────────── @@ -191,94 +178,20 @@ function lastCommitRange(fixture: Fixture): string[] { } // ── 1. the workflow is reachable ───────────────────────────────────────────── - -describe('changeset-presence.yml — the gate can start on the PR that needs it', () => { - it('exists, and runs the gate script', () => { - expect(fs.existsSync(path.join(repoRoot, WORKFLOW)), 'a check nothing runs is not a gate').toBe(true); - expect(fs.existsSync(path.join(repoRoot, GATE)), `${GATE} must exist for the workflow to run it`).toBe(true); - expect(workflowYaml).toMatch(new RegExp(`run:\\s*node\\s+${GATE.replace(/[.]/g, '\\.')}`)); - }); - - it('carries NO path filter of any kind', () => { - // The reason it is its own workflow AND unfiltered. A filter on the trigger - // skips the whole workflow (GitHub has no per-job path filter), so the - // context is never CREATED on a PR that does not match — and a required - // context that is never created leaves the PR pending rather than failing - // it, which in the queue means the ruleset's 60-minute timeout - // (objectui#3523, #3509). It would also be a second copy of the script's - // guarded surface, free to drift from the `.changeset/config.json` the - // script derives it from. - expect(workflowYaml).not.toMatch(/paths-ignore:/); - expect(workflowYaml).not.toMatch(/^\s+paths:/m); - }); - - it('reports on pull requests and on merge-queue builds', () => { - expect(workflowYaml).toMatch(/^\s*pull_request:/m); - expect( - workflowYaml, - 'A gate with no path filter reports on every PR and can therefore be REQUIRED. A required ' + - 'context that does not report on a queue build does not fail the queue, it stalls it until ' + - "the ruleset's 60-minute status-check timeout (objectui#3523).", - ).toMatch(/^\s*merge_group:/m); - }); - - it('does not run on push to main, where nothing can be declared any more', () => { - // Not tidiness: the change has already landed, so a red push paints `main` - // red at whoever committed next, and no declaration can still be written - // into that change. The PR and the queue build are the two moments it can. - expect(workflowYaml).not.toMatch(/^\s*push:/m); - }); - - it('checks out enough history for a merge base to exist', () => { - // `fetch-depth: 0`. The default depth-1 clone gives `git merge-base` nothing - // to find, and the script treats an unresolvable base as a hard failure — so - // omitting this is a red build, not a silent pass. Pinned so it stays that - // way rather than being "fixed" by making the script skip. - expect(workflowYaml).toMatch(/fetch-depth:\s*0/); - }); - - it('is the only workflow that runs this gate, and needs no install to do it', () => { - const runners = fs - .readdirSync(workflowDir) - .filter((file) => file.endsWith('.yml')) - .filter((file) => withoutComments(fs.readFileSync(path.join(workflowDir, file), 'utf8')).includes(GATE)); - expect(runners).toEqual(['changeset-presence.yml']); - expect( - workflowYaml, - 'This gate reads `.changeset/config.json` and `git diff` only. A `pnpm install` here would ' + - 'turn a few-second check into a full install on every pull request.', - ).not.toMatch(/pnpm install/); - }); - - it('leaves changeset-guard.yml alone — the two gates face opposite directions', () => { - // `changeset-guard.yml`'s `paths: ['.changeset/**']` is deliberate: on a PR - // that adds ONLY a changeset, every gate inside `ci.yml` and `lint.yml` - // skips, so nothing in either of them ever reads the changeset — and that - // guard exists to see exactly that PR. A PR that FORGOT its changeset does - // not touch `.changeset/**`, so widening those paths would have broken the - // case it was built for while still not catching this one. Two workflows, - // one direction each. - // - // Not the reason this comment used to give — "a PR adding ONLY a changeset - // starts no other workflow". That is the sentence PR #4371 rewrote in the - // docblock of `check-changeset-presence.mjs`, the script this file - // accompanies, and the pair disagreed across the two until objectui#4381. - // objectui#3523 step 2 deleted `paths-ignore` from `ci.yml`'s and - // `lint.yml`'s `pull_request` trigger; it survives only on `push`. Such a PR - // therefore DOES start both and DOES produce their contexts — measured: PR - // #3856 (one markdown file) 16 checks, PR #4339 (one line added to - // AGENTS.md) 17. What still skips is the expensive work: the `Decide whether - // this change needs a full run` step excludes markdown and `.changeset/**` - // and skips every step below itself, and its exclusion list is that `push` - // filter unchanged, held identical to it by - // `scripts/__tests__/merge-queue-reporting.test.ts`. The conclusion outlived - // its premise; objectui#3857 pinned the correction. - const guard = withoutComments(fs.readFileSync(path.join(workflowDir, 'changeset-guard.yml'), 'utf8')); - expect(guard).toMatch(/^\s+paths:/m); - expect(guard).toMatch(/\.changeset\//); - expect(guard).toMatch(/check-changeset-no-major\.mjs/); - }); -}); +// +// Retired with objectstack#25712: `exists, and runs the gate script`, `carries +// NO path filter of any kind`, `reports on pull requests and on merge-queue +// builds`, `does not run on push to main, where nothing can be declared any +// more`, `checks out enough history for a merge base to exist`, `is the only +// workflow that runs this gate, and needs no install to do it` and `leaves +// changeset-guard.yml alone — the two gates face opposite directions`. Each read +// objectui's `changeset-presence.yml` or `changeset-guard.yml` from +// `ui/.github/workflows/`, which runs nowhere under the monorepo: GitHub runs +// the root `.github/workflows/` alone, and no root workflow runs this gate. +// They are rows 4 and 3 of the objectstack#25697 workflow roster +// (→ sub-issue 4), where the root `pr-automation.yml` `Check Changeset` job is +// the counterpart; a port brings these pins back against its root workflow, by +// path. // ── 2. the guarded surface is derived from the release configuration ───────── diff --git a/ui/scripts/__tests__/check-eager-closure-budget.test.ts b/ui/scripts/__tests__/check-eager-closure-budget.test.ts index e57eccb7434..b98294d84cf 100644 --- a/ui/scripts/__tests__/check-eager-closure-budget.test.ts +++ b/ui/scripts/__tests__/check-eager-closure-budget.test.ts @@ -43,7 +43,6 @@ import { import { OPTIONAL_CONTEXTS, REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const workflowPath = path.join(repoRoot, '.github/workflows/performance-budget.yml'); const viteConfigPath = path.join(repoRoot, 'apps/console/vite.config.ts'); const checkerPath = path.join(repoRoot, 'scripts/check-eager-closure-budget.mjs'); @@ -2057,7 +2056,7 @@ describe('main', () => { * not readable from inside a checkout. `evaluateCeilingFreshness`'s own docblock * carries what the tree can re-derive instead, cited to * `scripts/dependabot-merge-gate.mjs` and to the workflow's `on:` block; the pin - * at the bottom of this file keeps all three files off the claim. + * at the bottom of this file keeps the gate's prose files off the claim. * * Not hypothetical: run 32804357171 started 6m50s after `0409b766d` lowered the * aggregate ceiling from 4,086,000 to 3,345,000 and published @@ -2458,36 +2457,20 @@ describe('ceiling freshness (objectui#6245)', () => { /** * The checker can only be correct if the workflow keeps feeding it and the * build keeps emitting the report. Neither half is reachable from a unit test. + * + * The workflow half (`runs the closure checker in the budget step`, `keeps the + * entry-chunk budget alongside it`, `measures the closure even when the entry + * chunk is over budget` and `maps the gauge-failure exit code to \`error\`, not + * to a size verdict`) retired with objectstack#25712. It read objectui's + * `performance-budget.yml`, which runs nowhere under the monorepo: GitHub runs + * the root `.github/workflows/` alone, and no root workflow runs this checker. + * That workflow is row 22 of the objectstack#25697 workflow roster, "kept, + * follow-up port" into `Build & E2E UI`; the port brings these pins back + * against the root job, by path. The build half stands. */ -describe('performance-budget.yml + vite.config.ts contract', () => { - const workflow = fs.readFileSync(workflowPath, 'utf8'); +describe('vite.config.ts contract', () => { const viteConfig = fs.readFileSync(viteConfigPath, 'utf8'); - it('runs the closure checker in the budget step', () => { - expect(workflow).toContain('node scripts/check-eager-closure-budget.mjs'); - }); - - it('keeps the entry-chunk budget alongside it', () => { - // Replacing a blind gauge is not licence to drop the check already there. - expect(workflow).toContain('MAX_ENTRY_GZIP_KB=350'); - }); - - it('measures the closure even when the entry chunk is over budget', () => { - // The entry check used to `exit 1` on breach. If it still did, a fat entry - // chunk would hide the number that actually governs a page load. - const step = workflow.slice(workflow.indexOf('MAX_ENTRY_GZIP_KB=350')); - const entryVerdict = step.indexOf('ENTRY BUDGET EXCEEDED'); - const closureRun = step.indexOf('node scripts/check-eager-closure-budget.mjs'); - expect(entryVerdict).toBeGreaterThan(-1); - expect(closureRun).toBeGreaterThan(entryVerdict); - }); - - it('maps the gauge-failure exit code to `error`, not to a size verdict', () => { - expect(workflow).toContain('if [ "$CLOSURE_CODE" -eq 2 ]; then'); - const branch = workflow.slice(workflow.indexOf('if [ "$CLOSURE_CODE" -eq 2 ]; then')); - expect(branch.slice(0, 400)).toContain('budget_status=error'); - }); - it('emits the report from a plugin that is not skipped on CI', () => { // `compression` and `visualizer` sit behind `...(!isCI ? [` — the budget // runs ON CI, so the report emitter must not join them there. @@ -2764,7 +2747,11 @@ describe('the prose attached to the baselines (objectui#7046)', () => { * `.github/workflows/performance-budget.yml` joined this population in * objectui#7850 — the same rule, the same reader, no second matcher. Its prose * is `#` comments rather than `/**` blocks, so it reaches the reader through - * `yamlProse` below, which translates the marker and nothing else. + * `yamlProse` below, which translates the marker and nothing else. It left the + * population with objectstack#25712, with its must-hit control: that workflow + * runs nowhere under the monorepo (roster row 22, "kept, follow-up port"). The + * translation and its self-test stay for the root job the port lands in, which + * rejoins the population then. * * Its one obstacle was settled by REWORDING, not by an exemption. The workflow * counted chunks once as the OUTCOME of a hazard — a `"sideEffects": false` @@ -2788,9 +2775,9 @@ describe('the prose attached to the baselines (objectui#7046)', () => { */ describe("chunk counts in this gate's prose (objectui#7528)", () => { /** - * The population. Two script files whose prose is JS comments, and the - * workflow whose prose is `#` comments (objectui#7850) — the same rule and the - * same reader, with only the comment marker translated for the third. + * The population: two script files whose prose is JS comments. The workflow + * whose prose is `#` comments (objectui#7850) left it with objectstack#25712, + * and its root successor joins with the marker translated by `yamlProse`. */ const PROSE_FILES: Record string }> = { 'check-eager-closure-budget.mjs': { path: checkerPath, toProse: (source) => source }, @@ -2798,13 +2785,12 @@ describe("chunk counts in this gate's prose (objectui#7528)", () => { path: fileURLToPath(import.meta.url), toProse: (source) => source, }, - 'performance-budget.yml': { path: workflowPath, toProse: yamlProse }, }; /** A chunk population written as a numeral: "N chunks", or "N of M chunks". */ const CHUNK_COUNT = /\b\d[\d,_]*\s+(?:of\s+\d[\d,_]*\s+)?chunks?\b/gi; - /** A commit named the way all three files name one: a backticked short hash. */ + /** A commit named the way these files name one: a backticked short hash. */ const NAMES_A_COMMIT = /`[0-9a-f]{7,40}`/; /** @@ -2984,18 +2970,9 @@ describe("chunk counts in this gate's prose (objectui#7528)", () => { expect(anchored).toContain('58 of 507 chunks'); }); - /** - * The same must-hit control for the workflow entry (objectui#7850). Its prose - * reaches the reader through a translation, and a translation that stopped - * working would show up as a population that passes because it is empty. This - * fails instead. - */ - it('sees the anchored measurement the workflow must not be refused for', () => { - const anchored = chunkCounts(yamlProse(fs.readFileSync(workflowPath, 'utf8'))) - .filter((count) => count.anchored) - .map((count) => count.text); - expect(anchored).toContain('58 of 507 chunks'); - }); + // `sees the anchored measurement the workflow must not be refused for`, the + // same must-hit control for the workflow entry (objectui#7850), retired with + // that entry (objectstack#25712, roster row 22). it.each(Object.entries(PROSE_FILES))( '%s writes no chunk count that no commit anchors', @@ -3139,15 +3116,19 @@ describe('no prose here classifies `Bundle Analysis` against branch protection ( */ const CLASSIFIES = new RegExp(String.raw`\b${REQ}\s+contexts?\b|\bis\s+not\s+${REQ}\b`, 'gi'); - /** The three files objectui#9155 is about, by the name a failure prints. */ + /** + * The files objectui#9155 is about, by the name a failure prints. The third, + * objectui's `performance-budget.yml`, left with objectstack#25712: it runs + * nowhere under the monorepo (roster row 22, "kept, follow-up port"), and the + * root job the port lands in joins here. + */ const POPULATION: Record = { 'check-eager-closure-budget.mjs': checkerPath, 'check-eager-closure-budget.test.ts': fileURLToPath(import.meta.url), - 'performance-budget.yml': workflowPath, }; /** - * The must-HIT control, with a known direction. Without it the three zeroes + * The must-HIT control, with a known direction. Without it the zeroes * below are equally consistent with a matcher that stopped matching anything * — which is how a pin on absence goes quietly green forever. */ @@ -3165,7 +3146,7 @@ describe('no prose here classifies `Bundle Analysis` against branch protection ( ).toBeNull(); }); - it('reads three files that are really there, not three empty strings', () => { + it('reads files that are really there, not empty strings', () => { for (const [name, file] of Object.entries(POPULATION)) { const source = fs.readFileSync(file, 'utf8'); expect(source.length, `${name} read as empty`).toBeGreaterThan(1000); @@ -3204,22 +3185,9 @@ describe('no prose here classifies `Bundle Analysis` against branch protection ( expect(OPTIONAL_CONTEXTS['Bundle Analysis']).toContain('performance-budget.yml filters on paths'); }); - /** - * The other cited source, re-derived rather than restated: the prose may say - * this job has no `merge_group` leg only for as long as that is true of the - * `on:` block. Guarded by locating a block that is found and substantial - * first, so a renamed section cannot pass by scanning nothing. - */ - it('leaves the workflow without the `merge_group` leg the prose cites the absence of', () => { - const source = fs.readFileSync(workflowPath, 'utf8'); - const start = source.indexOf('\non:\n'); - const end = source.indexOf('\npermissions:\n'); - expect(start, 'the `on:` block is not where this pin looks for it').toBeGreaterThanOrEqual(0); - expect(end).toBeGreaterThan(start); - const onBlock = source.slice(start, end); - expect(onBlock.length).toBeGreaterThan(200); - expect(onBlock).toContain(' push:'); - expect(onBlock).toContain(' pull_request:'); - expect(onBlock).not.toMatch(/^\s{2}merge_group:/m); - }); + // `leaves the workflow without the \`merge_group\` leg the prose cites the + // absence of` retired with objectstack#25712. It re-derived the other cited + // source from objectui's `performance-budget.yml` `on:` block, and that + // workflow runs nowhere under the monorepo (roster row 22, "kept, follow-up + // port"); the port brings it back against the root job, by path. }); diff --git a/ui/scripts/__tests__/check-governed-queue-guard.test.ts b/ui/scripts/__tests__/check-governed-queue-guard.test.ts index a97f137c9c0..3a4c65f68e6 100644 --- a/ui/scripts/__tests__/check-governed-queue-guard.test.ts +++ b/ui/scripts/__tests__/check-governed-queue-guard.test.ts @@ -3,12 +3,10 @@ import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parse as parseYaml } from 'yaml'; import { REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs'; import { CHECK_CONTEXT_NAME, - CHECK_JOB_ID, CHECK_WORKFLOW, GOVERNED_SURFACES, governedPathsIn, @@ -36,15 +34,24 @@ const WORKFLOW = `.github/workflows/${CHECK_WORKFLOW}`; * set. That is repository settings, which no test here can read and no agent may * change. What IS asserted is that this repository has written the answer down * where its own tooling reads it, and that every piece of the chain exists. + * + * ⚠️ The workflow half of the chain retired with objectstack#25712: `the gate + * script and its workflow both exist` (its workflow leg), `the job publishes + * exactly the check name the script declares`, `subscribes BOTH legs`, + * `re-fires on ready_for_review`, `carries no path filter on either leg`, + * `checks out full history`, `grants pull-requests: read and no write scope + * anywhere` and `runs the self-test BEFORE the live judgment`. Each read + * objectui's `governed-surface-guard.yml`, which runs nowhere under the + * monorepo: GitHub runs the root `.github/workflows/` alone. The root + * `governed-surface-guard.yml` covers the queue guard with its own register, + * which governs `ui/.github/workflows/**` (roster row 11, → sub-issue 4, which + * decides the rest of the `ui/**` surface). `CHECK_JOB_ID` and + * `CHECK_WORKFLOW` still name objectui's job; a port brings these pins back + * against the root workflow that runs this gate, by path. */ describe('check-governed-queue-guard is wired, not merely present', () => { - const workflowText = fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8'); - const workflow = parseYaml(workflowText); - const job = workflow.jobs[CHECK_JOB_ID]; - - it('the gate script and its workflow both exist', () => { + it('the gate script exists', () => { expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); - expect(fs.existsSync(path.join(ROOT, WORKFLOW))).toBe(true); }); it('package.json aliases it, and the alias points at the script that exists', () => { @@ -55,14 +62,6 @@ describe('check-governed-queue-guard is wired, not merely present', () => { } }); - it('the job publishes exactly the check name the script declares', () => { - // The #6865-shape defect one repo over: renaming a job detaches a required - // context with nothing to say so. The name lives in two files because a name - // in one file is a name nothing can pin; this is the assertion that keeps - // the two equal. - expect(job.name).toBe(CHECK_CONTEXT_NAME); - }); - it('this repository has written the context down as blocking', () => { // `REQUIRED_CONTEXTS` is objectui's own answer to "which checks are // blocking" (`scripts/dependabot-merge-gate.mjs`), and it is what @@ -71,58 +70,6 @@ describe('check-governed-queue-guard is wired, not merely present', () => { expect(REQUIRED_CONTEXTS).toContain(CHECK_CONTEXT_NAME); }); - it('subscribes BOTH legs — the queue build refuses, the pull request warns', () => { - // Both, not either. Without `merge_group` there is no refusal at all; - // without `pull_request` a seat gets no warning before the queue, which is - // the moment #6183 could still have been stopped. - expect(workflow.on).toHaveProperty('merge_group'); - expect(workflow.on).toHaveProperty('pull_request'); - }); - - it('re-fires on ready_for_review — the first move of the incident', () => { - // Naming `types:` REPLACES GitHub's default set rather than extending it, - // so the three defaults have to be restated alongside the addition. This - // asserts the addition AND that restating did not drop one. - expect(workflow.on.pull_request.types).toEqual([ - 'opened', - 'synchronize', - 'reopened', - 'ready_for_review', - ]); - }); - - it('carries no path filter on either leg — a skipped job counts as SUCCESS', () => { - // The one direction this gate may not be wrong in. A path filter that - // mis-scopes hands branch protection a green verdict from a job that never - // ran, on the one check whose entire purpose is to refuse. - for (const leg of ['pull_request', 'merge_group'] as const) { - const on = workflow.on[leg] ?? {}; - expect(on).not.toHaveProperty('paths'); - expect(on).not.toHaveProperty('paths-ignore'); - } - }); - - it('checks out full history — a truncated diff answers with silence', () => { - const checkout = job.steps.find((s: Record) => String(s.uses ?? '').startsWith('actions/checkout')); - expect(checkout, 'the job must check out the repository').toBeDefined(); - expect(checkout.with['fetch-depth']).toBe(0); - }); - - it('grants pull-requests: read and no write scope anywhere', () => { - expect(workflow.permissions).toEqual({ contents: 'read', 'pull-requests': 'read' }); - }); - - it('runs the self-test BEFORE the live judgment, as its precondition', () => { - const runs: string[] = job.steps - .filter((s: Record) => typeof s.run === 'string') - .map((s: Record) => s.run); - const selfTestAt = runs.findIndex((r) => r.includes(`${GATE} --self-test`)); - const liveAt = runs.findIndex((r) => r.includes(GATE) && !r.includes('--self-test')); - expect(selfTestAt, 'the workflow must run the self-test').toBeGreaterThan(-1); - expect(liveAt, 'the workflow must run the live judgment').toBeGreaterThan(-1); - expect(selfTestAt).toBeLessThan(liveAt); - }); - it('its self-test passes — the half that makes a green run mean something', () => { const out = execFileSync('node', [GATE, '--self-test'], { cwd: ROOT, encoding: 'utf8' }); // objectui#7897 — the COUNT, not the shape. `\d+ cases pass` is satisfied diff --git a/ui/scripts/__tests__/check-links-workflow.test.ts b/ui/scripts/__tests__/check-links-workflow.test.ts index 27d7777c991..52be428bace 100644 --- a/ui/scripts/__tests__/check-links-workflow.test.ts +++ b/ui/scripts/__tests__/check-links-workflow.test.ts @@ -45,44 +45,14 @@ import { fileURLToPath } from 'node:url'; * right), and `push` / `pull_request` must NOT — an external link checker walks * the network, and gating PRs on third-party uptime makes other people's work * fail for reasons they cannot fix. + * + * ⚠️ Half 1, the workflow surface of half 3 and the trigger set retired with + * objectstack#25712; the note above the first describe names the cases. Half 2 + * and this header's own count pin stand. */ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const workflowPath = path.join(repoRoot, '.github/workflows/check-links.yml'); const lycheeConfigPath = path.join(repoRoot, 'lychee.toml'); -const siteConfigPath = path.join(repoRoot, 'apps/site/source.config.ts'); - -/** - * The workflow YAML with whole-line comments removed. - * - * Required, not cosmetic: the header explains at length why `push` and - * `pull_request` stay disabled and names both of them, and the commented-out - * trigger block is itself part of the record. A scan that counted either would - * report triggers this workflow does not have. - */ -function withoutComments(yaml: string): string { - return yaml - .split('\n') - .filter((line) => !/^\s*#/.test(line)) - .join('\n'); -} - -/** - * The workflow's comment prose, with the leading `#` markers stripped. - * - * Stripped, not kept: a sentence that wraps across two comment lines has a `#` - * sitting in the middle of it, and a scan that reads the raw lines cannot see a - * numeral and the noun it qualifies as adjacent when the line break falls - * between them. Removing the marker is what makes the count pin below read the - * header the way a person does. - */ -function commentProse(yaml: string): string { - return yaml - .split('\n') - .filter((line) => /^\s*#/.test(line)) - .map((line) => line.replace(/^\s*#\s?/, '')) - .join('\n'); -} /** * This file's own leading block comment — the second surface the count pin reads. @@ -136,103 +106,21 @@ function documentCounts(text: string): string[] { return [...text.matchAll(new RegExp(POPULATION_COUNT.source, 'gi'))].map((m) => m[0].replace(/\s+/g, ' ').trim()); } -/** The quoted input patterns from the workflow's `args:` block. */ -function scannedPatterns(yaml: string): string[] { - return [...withoutComments(yaml).matchAll(/^\s*'([^']+)'\s*$/gm)].map((m) => m[1]); -} - -/** Every `.md` / `.mdx` file below `dir`, as repo-relative paths. */ -function markdownFilesUnder(dir: string): string[] { - const absolute = path.join(repoRoot, dir); - if (!fs.existsSync(absolute)) return []; - const found: string[] = []; - const walk = (current: string) => { - for (const entry of fs.readdirSync(current, { withFileTypes: true })) { - const full = path.join(current, entry.name); - if (entry.isDirectory()) walk(full); - else if (/\.mdx?$/.test(entry.name)) found.push(path.relative(repoRoot, full)); - } - }; - walk(absolute); - return found; -} - /** - * Files matched by one input pattern. - * - * Only the two shapes the workflow uses are supported — `/ ** / *.` - * and a literal path. Anything else is reported rather than silently counted as - * zero, because "matches nothing" is the very defect this file exists to catch. + * Retired with objectstack#25712: `scans every markdown file the site is built + * from`, `still sweeps the repo-root internal docs and README`, `states its + * inputs in a shape this test can actually evaluate`, `runs on a schedule as + * well as on demand`, `does not gate pull requests`, and the workflow surface + * of the count pin below. Each read objectui's `check-links.yml` (its `args:` + * block against the docs tree `apps/site/source.config.ts` declares, its + * triggers, its header prose), which runs nowhere under the monorepo: GitHub + * runs the root `.github/workflows/` alone. The root `check-links.yml` sweeps + * the root's own trees, not `ui/`. objectui's is row 6 of the objectstack#25697 + * workflow roster (→ sub-issue 4, which widens the root sweep to `ui/` or keeps + * objectui's); either way the port brings these pins back against the root + * workflow, by path. */ -function filesMatching(pattern: string): string[] { - const glob = /^(.+)\/\*\*\/\*\.([a-z]+)$/.exec(pattern); - if (glob) { - const [, base, extension] = glob; - return markdownFilesUnder(base).filter((file) => file.endsWith(`.${extension}`)); - } - if (/[*?[\]]/.test(pattern)) { - throw new Error(`unsupported glob shape in check-links.yml args: ${pattern}`); - } - return fs.existsSync(path.join(repoRoot, pattern)) ? [pattern] : []; -} - -/** The docs directory the site itself declares, as a repo-relative path. */ -function declaredDocsDir(): string { - const source = fs.readFileSync(siteConfigPath, 'utf8'); - const match = /\bdir:\s*'([^']+)'/.exec(source); - expect(match, `${siteConfigPath} no longer declares a docs \`dir\``).not.toBeNull(); - const resolved = path.resolve(path.dirname(siteConfigPath), match![1]); - return path.relative(repoRoot, resolved); -} - -const workflowYaml = () => fs.readFileSync(workflowPath, 'utf8'); - -describe('check-links.yml — the external link sweep looks at the published docs', () => { - it('scans every markdown file the site is built from', () => { - // The #3449 assertion. Derived from the site config on purpose: see the - // header. `content/docs` is never spelled out here. - const docsDir = declaredDocsDir(); - const published = markdownFilesUnder(docsDir); - expect(published.length, `no markdown found under the declared docs dir '${docsDir}'`).toBeGreaterThan(100); - - const scanned = new Set(scannedPatterns(workflowYaml()).flatMap(filesMatching)); - const missed = published.filter((file) => !scanned.has(file)); - expect(missed.slice(0, 5), `${missed.length} published doc(s) are not covered by check-links.yml args`).toEqual([]); - }); - - it('still sweeps the repo-root internal docs and README', () => { - // Ruling on #3449: `docs/**` (ADRs, audits) and README keep their coverage. - // Widening to the published tree was never meant to narrow anything. - const scanned = new Set(scannedPatterns(workflowYaml()).flatMap(filesMatching)); - const internal = markdownFilesUnder('docs'); - expect(internal.length, "the repo-root internal docs tree is gone — re-read this workflow's scope").toBeGreaterThan( - 0, - ); - - const missed = internal.filter((file) => !scanned.has(file)); - expect(missed.slice(0, 5), `${missed.length} internal doc(s) are not covered by check-links.yml args`).toEqual([]); - expect(scanned.has('README.md')).toBe(true); - }); - - it('states its inputs in a shape this test can actually evaluate', () => { - // Coverage above is asserted per FILE, not per pattern, and that is - // deliberate: `docs/**/*.mdx` currently matches nothing (the internal tree - // is all `.md`) and should stay anyway, so that an internal `.mdx` landing - // tomorrow is swept rather than becoming a fresh blind spot. What must not - // happen is an args block this test cannot read — it would assert coverage - // over an empty pattern list and pass while checking nothing. - const patterns = scannedPatterns(workflowYaml()); - expect(patterns.length, 'no input patterns found in the args block').toBeGreaterThan(2); - for (const pattern of patterns) expect(() => filesMatching(pattern)).not.toThrow(); - }); - - it('runs on a schedule as well as on demand', () => { - const yaml = withoutComments(workflowYaml()); - expect(yaml, 'the periodic sweep #3213 ruling B intended').toMatch(/^\s*schedule:/m); - expect(yaml).toMatch(/^\s*-\s*cron:\s*'[^']+'/m); - expect(yaml).toMatch(/^\s*workflow_dispatch:/m); - }); - +describe('check-links — the header count pin', () => { /** * objectui#7825 — the third copy of objectui#7448's pin, in the home the card * named. This header stated two document counts as literals: the repo-root @@ -259,10 +147,7 @@ describe('check-links.yml — the external link sweep looks at the published doc * before their emptiness means anything. */ it('its header states the population and never counts it — no count can rot here', () => { - const surfaces = [ - ['check-links.yml', commentProse(workflowYaml())], - ["this test file's own header", ownHeaderComment()], - ] as const; + const surfaces = [["this test file's own header", ownHeaderComment()]] as const; for (const [label, prose] of surfaces) { expect( @@ -322,15 +207,6 @@ describe('check-links.yml — the external link sweep looks at the published doc expect(documentCounts(line), `the pin must NOT fire on: ${line}`).toEqual([]); } }); - - it('does not gate pull requests', () => { - // #3213 ruling B. Lychee goes over the network; a third-party 502 must not - // be able to redden somebody's unrelated PR. The commented-out block in the - // workflow stays commented — `withoutComments` is what makes this real. - const yaml = withoutComments(workflowYaml()); - expect(yaml, 'external link checking must not become a PR gate — see #3213').not.toMatch(/^\s*pull_request:/m); - expect(yaml).not.toMatch(/^\s*push:/m); - }); }); describe('lychee.toml — site-absolute routes are skipped, not resolved', () => { diff --git a/ui/scripts/__tests__/check-merge-queue-head.test.ts b/ui/scripts/__tests__/check-merge-queue-head.test.ts index 6cb386e6159..2efecbb95b0 100644 --- a/ui/scripts/__tests__/check-merge-queue-head.test.ts +++ b/ui/scripts/__tests__/check-merge-queue-head.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parse as parseYaml } from 'yaml'; import { CONFIRMATION_DELAY_MS, @@ -24,7 +23,6 @@ import { selectHeadEntry, selfTest, } from '../check-merge-queue-head.mjs'; -import { producedCheckNames, readWorkflows, triggerBlock } from './workflow-checks'; /** * objectui#7010 — a merge-queue head entry for which GitHub never dispatches @@ -58,8 +56,6 @@ import { producedCheckNames, readWorkflows, triggerBlock } from './workflow-chec */ const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GATE = 'scripts/check-merge-queue-head.mjs'; -const WORKFLOW_FILE = 'merge-queue-head-patrol.yml'; -const WORKFLOW = `.github/workflows/${WORKFLOW_FILE}`; const gateSource = fs.readFileSync(path.join(ROOT, GATE), 'utf8'); const NOW = Date.parse('2026-09-05T23:40:00Z'); @@ -322,14 +318,25 @@ describe('the report describes calls that were actually made', () => { }); }); +/** + * The workflow half of this describe retired with objectstack#25712: `the + * script and its workflow both exist` (its workflow leg), `the workflow + * actually runs the script`, `runs on a clock`, `subscribes no pull_request + * leg, so it owes no entry in the merge gate`, `grants exactly the three scopes + * it uses`, `serialises itself and never cancels a run mid-reading`, `captures + * the exit code with no pipe in between`, `lands the reading BEFORE it raises + * the alarm`, `never opens an issue`, the no-install leg of `needs no install` + * and the `QUEUE_BASE_BRANCH` leg of `reads the branch it is installed to + * watch`. Each read objectui's `merge-queue-head-patrol.yml`, which runs + * nowhere under the monorepo: GitHub runs the root `.github/workflows/` alone, + * and no root workflow runs this patrol. It is row 20 of the objectstack#25697 + * workflow roster (→ sub-issue 4), where the root `merge-queue-triage.yml` is + * the counterpart; a port brings these pins back against its root workflow, by + * path. What stays reads the script and `package.json`. + */ describe('the patrol is wired, not merely present', () => { - const workflowText = fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8'); - const workflow = parseYaml(workflowText); - const job = workflow.jobs.patrol; - - it('the script and its workflow both exist', () => { + it('the script exists', () => { expect(fs.existsSync(path.join(ROOT, GATE))).toBe(true); - expect(fs.existsSync(path.join(ROOT, WORKFLOW))).toBe(true); }); it('package.json aliases the offline check and the live read', () => { @@ -338,92 +345,14 @@ describe('the patrol is wired, not merely present', () => { expect(pkg.scripts['merge-queue-head']).toBe(`node ${GATE}`); }); - it('the workflow actually runs the script', () => { - const runs = job.steps.map((s: { run?: string }) => s.run ?? '').join('\n'); - expect(runs).toContain(`node ${GATE}`); - }); - - it('runs on a clock, because the failure is an absence nothing can trigger on', () => { - // A wedged entry emits no `merge_group`; `main` never moves, so no `push` - // fires; the pull-request events all happened before it was enqueued. There - // is no event to hang this on — that is the whole reason for the cron. - const crons = (workflow.on.schedule ?? []).map((s: { cron: string }) => s.cron); - expect(crons).toHaveLength(1); - expect(crons[0]).toMatch(/^[\d,]+ \* \* \* \*$/); - const minutes = crons[0].split(' ')[0].split(',').map(Number); - expect(minutes.length, 'four firings an hour — see the cadence arithmetic in the workflow header').toBe(4); - expect(minutes, 'offset off :00, where every other repository schedules').not.toContain(0); - expect(workflow.on).toHaveProperty('workflow_dispatch'); - }); - - it('subscribes no pull_request leg, so it owes no entry in the merge gate', () => { - // Every job of a `pull_request` workflow produces a check run, and - // `dependabot-merge-gate.mjs` requires every produced name to be classified - // in REQUIRED_CONTEXTS / OPTIONAL_CONTEXTS / NOT_A_GATE — a partition - // `dependabot-merge-gate.test.ts` asserts exactly. This patrol has no leg, - // so it produces nothing and owes nothing. If a leg is ever added, this goes - // red and names the other half of the work. - expect(workflow.on).not.toHaveProperty('pull_request'); - expect(workflow.on).not.toHaveProperty('merge_group'); - const self = readWorkflows().find((w) => w.file === WORKFLOW_FILE); - expect(self, 'the patrol workflow must be readable by the shared parser').toBeTruthy(); - expect(triggerBlock(self!).some((line) => /^ {2}pull_request:/.test(line))).toBe(false); - for (const [name, file] of producedCheckNames()) { - expect(file, `${name} must not come from the patrol — it can never gate a pull request`).not.toBe(WORKFLOW_FILE); - } - }); - - it('grants exactly the three scopes it uses, and no other write', () => { - expect(workflow.permissions).toEqual({ contents: 'read', actions: 'read', issues: 'write' }); - }); - - it('serialises itself and never cancels a run mid-reading', () => { - expect(workflow.concurrency.group).toBe('merge-queue-head-patrol'); - expect(workflow.concurrency['cancel-in-progress']).toBe(false); - }); - - it('captures the exit code with no pipe in between', () => { - // `node … | tail` reports the PIPE's status, and `tail` essentially never - // fails — so a wedge (3), a clean reading (0) and an unreadable one (2) - // would all read as 0. That silent collapse is the class this patrol exists - // to catch, and it would be sitting inside the patrol. - const step = job.steps.find((s: { id?: string }) => s.id === 'patrol'); - expect(step, 'the reading step must keep its `id: patrol`').toBeTruthy(); - expect(step.run).toMatch(/code=\$\?/); - expect(step.run).not.toMatch(/node scripts\/check-merge-queue-head\.mjs[^\n]*\|/); - }); - - it('lands the reading BEFORE it raises the alarm', () => { - const ids = job.steps.map((s: { name?: string }) => s.name ?? ''); - const summaryAt = ids.findIndex((n: string) => /run summary/i.test(n)); - const failAt = ids.findIndex((n: string) => /^Fail the run/i.test(n)); - expect(summaryAt).toBeGreaterThan(-1); - expect(failAt).toBeGreaterThan(summaryAt); - expect(job.steps[summaryAt].if).toBe('always()'); - }); - - it('never opens an issue — one anchor, refreshed, or nothing at all', () => { - // A patrol that filed a card per firing would file four an hour for an hour, - // on one incident. objectui#7010's own history is the precedent: a live - // recurrence was posted there as a comment "per the one-anchor rule rather - // than as a new card". - expect(workflowText).not.toMatch(/issues\.create/); - expect(workflowText).not.toMatch(/issues\.createComment/); - expect(workflowText).toMatch(/issues\.update/); - }); - - it('needs no install, so it stays inside the pre-install import population', () => { - const runs = job.steps.map((s: { run?: string; uses?: string }) => `${s.uses ?? ''} ${s.run ?? ''}`).join('\n'); - expect(runs).not.toMatch(/pnpm install/); - // The gate imports one repo-relative module and nothing else; anything from - // `node_modules` would make the install-free step throw at import time. + it('imports one repo-relative module and nothing else, so it can run before an install', () => { + // Anything from `node_modules` would make an install-free step throw at + // import time. const imports = [...gateSource.matchAll(/^import .* from '([^']+)';$/gm)].map((m) => m[1]); expect(imports).toEqual(['./invoked-as.mjs']); }); - it('reads the branch it is installed to watch, with a default that is real', () => { - const step = job.steps.find((s: { id?: string }) => s.id === 'patrol'); - expect(step.env.QUEUE_BASE_BRANCH).toContain('default_branch'); + it('defaults to a branch that is real', () => { expect(DEFAULT_BASE_BRANCH).toBe('main'); }); }); diff --git a/ui/scripts/__tests__/check-new-cross-file-line-citations.test.ts b/ui/scripts/__tests__/check-new-cross-file-line-citations.test.ts index b4b01d91853..ceb24e59f62 100644 --- a/ui/scripts/__tests__/check-new-cross-file-line-citations.test.ts +++ b/ui/scripts/__tests__/check-new-cross-file-line-citations.test.ts @@ -38,7 +38,6 @@ import { judge } from '../cross-file-line-citation-census.mjs'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const gatePath = path.join(repoRoot, 'scripts/check-new-cross-file-line-citations.mjs'); -const workflowPath = path.join(repoRoot, '.github/workflows/line-citation-gate.yml'); const docPath = path.join(repoRoot, 'content/docs/guide/ci-cd-pipeline.md'); /** Judges a hit against the real tree, the way the gate's own run does. */ @@ -420,27 +419,18 @@ describe('the instrument does not count itself', () => { }); }); +/** + * The workflow half of this describe retired with objectstack#25712: `runs the + * gate in its own workflow`, `checks out deep enough to have a merge base`, + * `filters no pull request at the trigger` and `the workflow job name matches + * the classified context name`. Each read objectui's `line-citation-gate.yml`, + * which runs nowhere under the monorepo: GitHub runs the root + * `.github/workflows/` alone, and no root workflow runs this gate. It is row 15 + * of the objectstack#25697 workflow roster (→ sub-issue 4); a port brings these + * pins back against its root workflow, by path. The page and the classifier + * pins below read no workflow and stand. + */ describe('wiring — a gate nothing runs is not a gate', () => { - const workflow = fs.readFileSync(workflowPath, 'utf8'); - - it('runs the gate in its own workflow', () => { - expect(workflow).toContain('node scripts/check-new-cross-file-line-citations.mjs'); - }); - - it('checks out deep enough to have a merge base', () => { - expect(workflow).toContain('fetch-depth: 0'); - }); - - it('filters no pull request at the trigger', () => { - // The population is comments, message strings, documentation and changesets. - // A path filter on any of those is the hole the gate exists to close, and - // `lint.yml`'s in-job short-circuit excludes exactly them, which is why this - // is its own workflow. - const trigger = workflow.slice(workflow.indexOf('\non:'), workflow.indexOf('\nconcurrency:')); - expect(trigger).toContain('pull_request'); - expect(trigger).not.toMatch(/^\s*paths(-ignore)?:/m); - }); - it('is written up on the page objectui#3653 pins, and declared report-only there', () => { const doc = fs.readFileSync(docPath, 'utf8'); expect(doc).toContain('node scripts/check-new-cross-file-line-citations.mjs'); @@ -473,10 +463,6 @@ describe('wiring — a gate nothing runs is not a gate', () => { const gate = fs.readFileSync(path.join(repoRoot, 'scripts/dependabot-merge-gate.mjs'), 'utf8'); expect(gate).toContain("'Line Citation Gate':"); }); - - it('the workflow job name matches the classified context name', () => { - expect(workflow).toMatch(/^\s*name: Line Citation Gate$/m); - }); }); /** diff --git a/ui/scripts/__tests__/check-required-check-set.test.ts b/ui/scripts/__tests__/check-required-check-set.test.ts index a52b21b8c33..28bb010f081 100644 --- a/ui/scripts/__tests__/check-required-check-set.test.ts +++ b/ui/scripts/__tests__/check-required-check-set.test.ts @@ -3,7 +3,6 @@ import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { parse as parseYaml } from 'yaml'; import { EXIT_BREACHED, @@ -19,7 +18,6 @@ import { REQUIRED_CONTEXTS } from '../dependabot-merge-gate.mjs'; const ROOT = path.resolve(fileURLToPath(import.meta.url), '../../..'); const GATE = 'scripts/check-required-check-set.mjs'; -const WORKFLOW = '.github/workflows/required-check-set-patrol.yml'; const FIXTURES = 'scripts/__tests__/fixtures/required-check-set'; /** * The verbatim live answer of 2026-10-12, taken the day objectui#12240 was @@ -73,6 +71,8 @@ const ABLATED_FIXTURE = `${FIXTURES}/type-check-removed.json`; * 4. the WIRING — the workflow exists, is scheduled, and runs this script. * A patrol nobody runs is indistinguishable from a patrol that passes, * and this is the half of the patrol's own liveness that a diff can see. + * Its workflow half retired with objectstack#25712 (the wiring describe + * names the cases); the `package.json` half stands. * * ⚠️ What this file deliberately does NOT assert: that the live ruleset is * intact. That reading needs the network and belongs to the scheduled run. A @@ -315,56 +315,16 @@ describe('check-required-check-set — the wiring', () => { expect(pkg.scripts['check:required-check-set']).toContain(GATE); }); - const workflow = (): Record => parseYaml(fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8')); - - /** - * YAML 1.1 reads a bare `on:` key as the BOOLEAN true, so `doc.on` is - * `undefined` and an assertion reaching for it would pass over an empty - * object — a wiring pin that asserts nothing. Both spellings are read, and - * the triggers assertion below fails loudly if neither resolves. - */ - const triggers = (): Record => { - const doc = workflow(); - const on = doc[String(true)] ?? doc.on; - if (!on || typeof on !== 'object') { - throw new Error(`could not read the \`on:\` block of ${WORKFLOW} — keys were ${Object.keys(doc).join(', ')}`); - } - return on as Record; - }; - - it('the patrol workflow exists and is SCHEDULED — the half of its liveness a diff can see', () => { - const on = triggers(); - const schedule = on.schedule as Array<{ cron?: string }> | undefined; - expect(Array.isArray(schedule)).toBe(true); - expect(schedule).not.toHaveLength(0); - expect(schedule?.[0]?.cron).toMatch(/^\S+ \S+ \S+ \S+ \S+$/); - expect(on).toHaveProperty('workflow_dispatch'); - }); - - it('the patrol has NO pull_request leg — it must never gate a pull request', () => { - const on = triggers(); - expect(on).not.toHaveProperty('pull_request'); - expect(on).not.toHaveProperty('pull_request_target'); - expect(on).not.toHaveProperty('merge_group'); - }); - - it('the workflow actually invokes this script', () => { - expect(fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8')).toContain(GATE); - }); - - it('the patrol asks for no write permission at all — it READS the ruleset', () => { - const perms = workflow().permissions; - expect(perms).toEqual({ contents: 'read' }); - }); - - it('a run that could not read must fail the job, so a broken transport is never a silent green', () => { - const yml = fs.readFileSync(path.join(ROOT, WORKFLOW), 'utf8'); - expect(yml).toContain(String(EXIT_CANNOT_RUN)); - expect(yml).toContain(String(EXIT_BREACHED)); - // Captured with no pipe in between — `node … | tail` reports the pipe's - // status and every verdict would collapse to 0. - expect(yml).toMatch(/exit_code=\$code/); - }); + // Retired with objectstack#25712: `the patrol workflow exists and is + // SCHEDULED`, `the patrol has NO pull_request leg`, `the workflow actually + // invokes this script`, `the patrol asks for no write permission at all` and + // `a run that could not read must fail the job`. Each read objectui's + // `required-check-set-patrol.yml`, which runs nowhere under the monorepo: + // GitHub runs the root `.github/workflows/` alone, and the root + // `required-set-patrol.yml` patrols the monorepo's own ruleset. objectui's + // patrol is row 25 of the objectstack#25697 workflow roster (→ sub-issue 4); a + // port brings these pins back against the root workflow that runs this + // script, by path. }); /** @@ -381,7 +341,11 @@ describe('check-required-check-set — the wiring', () => { * block asserts the pointer rather than the answer: nothing it reads could go * stale the next time a maintainer edits the ruleset. * - * ⚠️ The population is named in words: the three files objectui#9502 repaired. + * ⚠️ The population is named in words: the files objectui#9502 repaired that + * still say anything. The third, objectui's `dependabot-auto-merge.yml`, left it + * with objectstack#25712: that workflow runs nowhere under the monorepo (row 9 + * of the objectstack#25697 workflow roster, → sub-issue 4), so its prose + * guides no one. * `AGENTS.md` carried a fourth instance and is deliberately NOT in that set. * objectui#9502 left it standing — it is governed surface, and its * parenthetical also covered who may bypass the ruleset, which the endpoint @@ -395,12 +359,8 @@ describe('check-required-check-set — the wiring', () => { * decoration. */ describe('check-required-check-set — the prose points here instead of answering (#9502)', () => { - /** The three files objectui#9502 repaired. */ - const REPAIRED = [ - 'content/docs/guide/ci-cd-pipeline.md', - '.github/workflows/dependabot-auto-merge.yml', - 'scripts/dependabot-merge-gate.mjs', - ]; + /** The files objectui#9502 repaired, less the workflow the note above retires. */ + const REPAIRED = ['content/docs/guide/ci-cd-pipeline.md', 'scripts/dependabot-merge-gate.mjs']; /** * The carrier objectui#9502 left standing and objectui#9520 split: the one diff --git a/ui/scripts/__tests__/render-budget-comment.test.ts b/ui/scripts/__tests__/render-budget-comment.test.ts index 03127a317ee..07c1df38c04 100644 --- a/ui/scripts/__tests__/render-budget-comment.test.ts +++ b/ui/scripts/__tests__/render-budget-comment.test.ts @@ -1,5 +1,4 @@ import { describe, expect, it } from 'vitest'; -import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -9,9 +8,6 @@ import { fileURLToPath } from 'node:url'; import { renderBudgetComment, renderFromEnv } from '../render-budget-comment.mjs'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const workflowPath = path.join(repoRoot, '.github/workflows/performance-budget.yml'); -const rendererPath = path.join(repoRoot, 'scripts/render-budget-comment.mjs'); -const checkerPath = path.join(repoRoot, 'scripts/check-eager-closure-budget.mjs'); const NO_SUCH_REPORT = path.join(repoRoot, 'scripts/__tests__/__no-size-report__.md'); /** @@ -439,202 +435,19 @@ describe('renderFromEnv', () => { }); }); -/** - * The renderer can only be correct if the workflow keeps feeding it. These pin - * the two workflow-level halves of the fix, which no unit test can exercise: - * the step conditions, and the env contract between the two files. - */ -describe('performance-budget.yml contract', () => { - const workflow = fs.readFileSync(workflowPath, 'utf8'); - const renderer = fs.readFileSync(rendererPath, 'utf8'); - // Only the step conditions — prose in `#` comments discusses `always()` and - // must not be mistaken for a use of it. - const conditions = [...workflow.matchAll(/^\s*if:\s*(.+)$/gm)].map((m) => m[1]); - - it('never gates a step on always() — it fires on cancelled runs, which measure nothing', () => { - expect(conditions.filter((c) => c.includes('always()'))).toEqual([]); - }); - - it('gates the size report, the renderer and the comment on !cancelled()', () => { - // Five: those three, plus the turbo cache report (objectui#12276), which is - // bookkeeping after the verdict and reports on a red run for the same reason, - // plus the pinned @objectstack build's save (objectui#12957), bookkeeping after - // the verdict that keeps the build whatever the verdict was. - expect(conditions.filter((c) => c.includes('!cancelled()'))).toHaveLength(5); - }); - - it('generates the size report only from a complete package build', () => { - expect(workflow).toContain( - "if: ${{ !cancelled() && steps.build_packages.outcome == 'success' }}", - ); - expect(workflow).toContain('id: build_packages'); - }); - - it('never interpolates a step output into the comment JS source', () => { - // `const status = '${{ steps.budget.outputs.budget_status }}'` is how an - // absent output became the empty string that rendered as FAIL. - expect(workflow).not.toMatch(/=\s*'\$\{\{\s*steps\.budget\.outputs/); - }); - - it('passes every env var the renderer reads, and reads every one it passes', () => { - // The render step's own block: another step's `env:` (the turbo cache - // report's RESTORED_CACHE_KEY, objectui#12276) is not the renderer's contract. - const renderStep = workflow.slice(workflow.indexOf('- name: Render performance budget comment')); - const block = renderStep.slice(0, renderStep.indexOf('- name:', 1)); - expect(block, 'the render step is gone').toContain('scripts/render-budget-comment.mjs'); - const declared = [...block.matchAll(/^\s{10}([A-Z_]+): \$\{\{ steps\./gm)].map((m) => m[1]); - - expect(declared.length).toBeGreaterThan(0); - for (const name of declared) { - expect(renderer, `renderer must read env.${name}`).toContain(`env.${name}`); - } - }); - - /** - * objectui#6230: `closure_chunk_status` (objectui#5490) and - * `closure_headroom_status` (objectui#5924) were both published to - * `$GITHUB_OUTPUT` and never read, each following the precedent of the last. - * The card's explicit ask was that a THIRD round of this not happen — so the - * obligation is pinned mechanically rather than left to review: a half added - * to the checker fails here until it reaches the comment. - */ - it('passes every closure verdict the checker publishes into the comment step', () => { - const checker = fs.readFileSync(checkerPath, 'utf8'); - const call = checker.slice(checker.lastIndexOf('writeGithubOutput({')); - const published = [...call.slice(0, call.indexOf('});')).matchAll(/(closure_\w+):/g)].map( - (m) => m[1], - ); - - // Guard the guard: a regex that matched nothing would pass silently. - expect(published).toContain('closure_status'); - expect(published).toContain('closure_chunk_status'); - expect(published).toContain('closure_headroom_status'); - expect(published).toContain('closure_freshness_status'); - expect(published).toContain('closure_membership_status'); - - for (const key of published) { - expect(workflow, `workflow must pass steps.budget.outputs.${key} to the comment step`) - .toContain(`steps.budget.outputs.${key}`); - } - }); - - /** - * objectui#6245. The freshness half's inputs do NOT travel as `steps.*` env — - * they are the checker's, not the comment's — so the env-contract test above - * cannot see them, and without these three the wiring could rot silently while - * every other test in this file stayed green. The half would then report - * `error` on every PR, or, if `GITHUB_EVENT_NAME` stopped being the - * discriminator, quietly stop asking. - */ - describe('ceiling freshness inputs', () => { - const checker = fs.readFileSync(checkerPath, 'utf8'); - - it('fetches deep enough to see the base commit the merge ref was built on', () => { - expect(workflow).toContain('fetch-depth: 2'); - }); - - it('resolves both base-branch readings on pull_request runs only', () => { - expect(workflow).toContain('- name: Resolve the base-branch ceiling constants'); - expect(workflow).toContain("if: ${{ github.event_name == 'pull_request' }}"); - }); - - it('exports exactly the variables the checker reads', () => { - for (const name of [ - 'EAGER_CLOSURE_PR_BASE_SOURCE', - 'EAGER_CLOSURE_BASE_SOURCE', - 'EAGER_CLOSURE_PR_BASE_SHA', - 'EAGER_CLOSURE_BASE_SHA', - 'EAGER_CLOSURE_BASE_REF', - ]) { - expect(workflow, `workflow must export ${name}`).toContain(`echo "${name}=`); - expect(checker, `checker must read env.${name}`).toContain(`env.${name}`); - } - }); - - /** - * objectui#6245, second and third pass. The freshness half shipped a - * workflow half that `Bundle Analysis` could never run on: the PR editing it - * touched no `packages/**` path, so the gate never appeared on its own PR. - * Adding the YAML to the filters fixed that — and was still not enough. A PR - * touching only `check-eager-closure-budget.mjs`, the file that computes the - * verdict and the one this card is about, ALSO did not trigger the gate. - * - * The convention measured on `origin/main` is the gate's RUNTIME CLOSURE, - * not merely its own YAML: `half-state-patrol.yml` lists both the script it - * runs and `scripts/invoked-as.mjs`, that script's dependency. - * - * A wiring bug in any of these is fail-CLOSED — freshness `error`, exit 2 — - * so an untriggered gate does not fail quietly; it turns a REQUIRED context - * red on the next `packages/**` PR, belonging to another seat, reading to - * them as a bundle problem in their own diff. - */ - const TRIGGER_CLOSURE = [ - '.github/workflows/performance-budget.yml', - 'scripts/check-eager-closure-budget.mjs', - // The COMPOSITION half (objectui#7479) — WHICH locale catalogues a page - // load pulls, which the byte half above cannot answer. - 'scripts/check-eager-locale-catalogues.mjs', - 'scripts/render-budget-comment.mjs', - // `isEntrypoint` decides whether `main()` runs at all — a regression here - // makes the checker exit 0 having measured nothing. - 'scripts/invoked-as.mjs', - // The turbo cache report and prune the job runs after its verdict (objectui#12276). - 'scripts/report-turbo-cache.mjs', - // The pinned @objectstack build the job restores or builds before install (objectui#12957). - 'scripts/objectstack-pin.mjs', - ]; - - it('triggers on its whole runtime closure, so no part of this gate ships unexercised', () => { - // `#` lines are part of the block — an entry that needs explaining carries - // its reason inline — and `[ \t]*` rather than `\s*` so the run cannot walk - // past the end of the list into the next key. - const filters = [...workflow.matchAll(/^[ \t]*paths:\n((?:[ \t]*(?:- |#).*\n)+)/gm)].map( - (m) => m[1], - ); - // Guard the guard: `push` and `pull_request` both carry one, and they must - // not drift apart — a gate that runs on a PR but not on the merge (or the - // reverse) is worse than one that runs on neither. - expect(filters).toHaveLength(2); - for (const filter of filters) { - for (const entry of TRIGGER_CLOSURE) { - expect(filter, `every paths: filter must list ${entry}`).toContain(entry); - } - } - }); - - /** - * The closure is defined by what the JOB EXECUTES, which is why the - * `__tests__` files are deliberately absent from it: this job runs two - * `node scripts/*.mjs` commands and never vitest, so a test-only edit cannot - * move this gate's verdict, and those tests already run on every PR in the - * root vitest `unit` project. This asserts the definition rather than the - * list, so the two stay in step. - */ - it('lists every script the job runs, and nothing it merely tests', () => { - const invoked = [...workflow.matchAll(/^\s*(?:run: )?node (scripts\/[\w./-]+\.mjs)/gm)].map( - (m) => m[1], - ); - expect(invoked.length).toBeGreaterThan(0); - for (const script of new Set(invoked)) { - expect(TRIGGER_CLOSURE, `${script} is executed by this job, so it belongs in the closure`) - .toContain(script); - } - expect(TRIGGER_CLOSURE.some((e) => e.includes('__tests__'))).toBe(false); - }); - - it('lets the bundle be measured even when the resolve step fails', () => { - // The checker turns absent inputs into a freshness ERROR, so failing the - // job here instead would only cost the run its numbers (objectui#3152). - const step = workflow.slice(workflow.indexOf('- name: Resolve the base-branch ceiling')); - const budgetStep = step.indexOf('- name: Check console performance budget'); - expect(step.slice(0, budgetStep)).toContain('continue-on-error: true'); - expect(budgetStep).toBeGreaterThan(-1); - }); - }); - - it('writes budget_status on every path the budget step can exit through', () => { - // pass, fail, and the two "nothing to measure" errors. - const statuses = [...workflow.matchAll(/budget_status=(\w+)/g)].map((m) => m[1]); - expect(new Set(statuses)).toEqual(new Set(['pass', 'fail', 'error'])); - }); -}); +// `performance-budget.yml contract` retired with objectstack#25712. Its +// thirteen cases pinned the workflow halves no unit test can exercise: no step +// gated on `always()`; the size report, the renderer and the comment gated on +// `!cancelled()`; the size report only from a complete package build; no step +// output interpolated into the comment's JS source; the env contract with the +// renderer, both directions; every closure verdict the checker publishes passed +// to the comment step; the ceiling-freshness inputs (`fetch-depth: 2`, both +// base-branch readings on `pull_request` only, exactly the variables the checker +// reads); the trigger carrying the gate's whole runtime closure; the job listing +// every script it runs and nothing it merely tests; the bundle measured even +// when the resolve step fails; and `budget_status` written on every exit path. +// Each read objectui's `performance-budget.yml`, which runs nowhere under the +// monorepo: GitHub runs the root `.github/workflows/` alone, and no root +// workflow runs this renderer. That workflow is row 22 of the objectstack#25697 +// workflow roster, "kept, follow-up port" into `Build & E2E UI`; the port +// brings these pins back against the root job, by path. diff --git a/ui/scripts/__tests__/render-changeset-claims-comment.test.ts b/ui/scripts/__tests__/render-changeset-claims-comment.test.ts index 6560cb4713c..77e5b4fd11f 100644 --- a/ui/scripts/__tests__/render-changeset-claims-comment.test.ts +++ b/ui/scripts/__tests__/render-changeset-claims-comment.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { MARKER, @@ -31,7 +30,11 @@ import { * the whole body is pinned free of tag-shaped fragments. * 2. **One copy of the marker.** The workflow reads it off the body it is * about to post. A second spelling in the YAML is drift waiting to happen, - * so its absence is pinned. + * so its absence was pinned. Retired with objectstack#25712: the pin read + * objectui's `changeset-presence.yml`, which runs nowhere under the + * monorepo (GitHub runs the root `.github/workflows/` alone). It is row 4 + * of the objectstack#25697 workflow roster (→ sub-issue 4), and a port + * brings the pin back against its root workflow, by path. * 3. **An empty finding set never reads as a request.** That inversion is * objectui#3152's bug in this gate's costume, and it would mute the channel * the ruling exists to un-mute. @@ -40,9 +43,6 @@ import { * 「REQUEST TO READ」self-description becomes true by delivery, and it stays * true only while the comment keeps saying what the gate does not do. */ -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); -const WORKFLOW = '.github/workflows/changeset-presence.yml'; - /** The shape GitHub deletes on save — the reason the marker is plain text. */ const TAG_SHAPED = /<[^<>\s][^<>]*>/; @@ -119,10 +119,10 @@ describe('the marker the in-place update depends on', () => { expect(body).not.toContain('