Skip to content

Commit 833e4a8

Browse files
ci(test-ui): add the Test UI aggregate job and register it as a required context (#26569)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9C2Dy8KN4fv23aea1JR1i9T)_ Fixes #26460 Clause-②: no Metadata-protocol: none **Tier H** (`.github/workflows/ci.yml`, `AGENTS.md`; `.claude/**` rides along). Draft until an approval from GOVERNED_APPROVERS (os-zhuang, hotlong). The ruleset half — adding `Test UI` to the `main` ruleset's required status checks — is the maintainer's, done in the same sitting as the merge (#9325's two halves; precedent `Lint UI` / `Build & E2E UI` via #25857 and `Type Check UI` via #25878). ## 维护者速读 - 改了什么:在 ci.yml 加一个汇总任务 `Test UI`(job id `test-ui-gate`),覆盖 16 个 `Test UI (k/16)` 分片,任一分片不是 success 就红;分片因 ui 过滤被跳过时为绿。注册表、汇总名册和两处写着必过名单的说明文字同步从 8 个变 9 个。 - 为什么改:裁决 6435318516(字母 A,分阶段)。Test UI 分片都不是必过,队列照样合入 Test UI 红的 PR(#26419 → #26421)。ui 车道的第一阶段读数已完成:最近 50 次跑了 Test UI 的队列构建 flake 0/50(6436579765)。 - 风险与回滚:只新增一个约 10 秒的任务,不改分片、超时、过滤。合入后在 ruleset 里加上 `Test UI` 才生效;回滚就是从 ruleset 删掉这一项再 revert 本 PR。若 Test UI 出现 flake,会把队列条目弹出。 - 先看:`.github/workflows/ci.yml` 新增的 `test-ui-gate` 任务(判定的 case 分支);`scripts/check-required-contexts.mjs` 新增的注册行;`AGENTS.md` 第 525–528 行的名单。 - 两分钟验证:`node scripts/check-required-contexts.mjs && node scripts/check-aggregator-roster.mjs`,应看到 `ci.yml:test-ui-gate → 'Test UI'` 和 `5 aggregator(s)`;本 PR 的 checks 里应有一个名为 `Test UI` 的绿色检查。 - 需要你做的:批准本 PR,合入时在 Settings → Rulesets → `main` 的必过检查里加上 `Test UI`。 ## What changes - `.github/workflows/ci.yml`: a new job `test-ui-gate`, `name: Test UI`, `needs: [test-ui, filter]`, `if: always()`, placed right after `test-ui`. It copies `test-gate`'s shape: the `name:` literal and `if: always()` are both load-bearing, and it declares its roster (`OS_AGGREGATOR_MEMBERS: test-ui`, `OS_AGGREGATOR_NON_MEMBERS: filter`). Its one step reads the matrix rollup `needs['test-ui'].result`: - `success` → green; - `skipped` → green only when `filter` concluded `success` and said the diff reaches no ui path (`ui_tree` or `ui` is `'false'`, the shards' own `if:`), the #4928 guard. That is how a PR or queue build that touches no ui path reads, the same way `Lint UI` and `Build & E2E UI` read `skipped` today; - anything else (`failure`, `cancelled`, an unexplained `skipped`, a lifecycle value such as `abandoned`) → red, with an `::error` naming the rollup. No other job changes: no shard, timeout or filter edit, and no Test Core line (#25870 edits that section in parallel; the hunks do not overlap). - `scripts/check-required-contexts.mjs`: one `REQUIRED_CONTEXTS` row (`ci.yml`, `test-ui-gate`, `Test UI`, `authorized` naming ruling 6435318516 and the reading 6436579765). Both `statesTheSet` surfaces' `mustName` gain `Test UI`. Self-test: battery (3) renames the bare `Test UI` name and expects red; battery (9) collides the `Test UI (k/16)` shard name with the bare gate name and expects red, plus the coexistence control. - `scripts/check-aggregator-roster.mjs`: `REQUIRED_AGGREGATORS` gains `ci.yml/test-ui-gate/Test UI`, the floor that keeps the new aggregate from losing its roster declaration. Self-test batteries (2) and (3) gain the dropped-member and phantom-`needs:` controls for it. - `AGENTS.md` (merge-queue rule) and `.claude/skills/pm-dispatch/references/platform-readings.md`: the required-set list grows from eight to nine. `check:required-contexts` reds until both name the registry exactly (`statesTheSet`); the count words are hand-followed. ## Why Ruling A (staged) on #26460, comment 6435318516. The ui step it orders first is done: the flake reading on the newest 50 queue builds that ran Test UI is 0/50, and every Test UI red in the last 24 hours has a named cause that is fixed on `main` (comment 6436579765). ## Acceptance notes - **Rollup, not per-shard credentials.** `test-gate` counts shard attestations because one rollup word cannot carry several shards' verdicts. The Test UI shards publish no attestation, and adding one is a change to `test-ui` (pinned by the ui lane's `ui/scripts/__tests__`), so this job reads the rollup and fails closed on every value except `success` and a filter-explained `skipped`. The rollup's known blind spot, a matrix where some legs are skipped and the rest succeed, cannot occur here: all 16 legs share one job-level `if:` and a static matrix, and the ui lane pins the width at 16 (`select-affected-tests-wiring.test.ts`). - **`cancelled` is red.** That is also what a shard killed by its own `timeout-minutes` reads, which must not pass. On a superseded push it paints the old sha red, which no required check reads. - objectui's per-shard reader `ui/scripts/check-test-shard-results.mjs` (the ported `Test` aggregate, row 7 of #25697's roster, "dropped until cutover") hardcodes the `Test (shard N/M)` spelling, so it cannot read `Test UI (k/16)` without a ui-lane change. Not used here; noted for the ui lane. ## Verification All on head `853ddedb4`. <details><summary>The two touched gates and their self-tests</summary> ``` node scripts/check-required-contexts.mjs → exit 0 · 9 pinned, ci.yml:test-ui-gate → 'Test UI' node scripts/check-required-contexts.mjs --self-test → exit 0 · 179 assertions node scripts/check-aggregator-roster.mjs → exit 0 · 5 aggregator(s); all 5 required-context aggregate(s) declared node scripts/check-aggregator-roster.mjs --self-test → exit 0 · 63 assertions ``` </details> <details><summary>dispatch-gates derived family</summary> `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran` → exit 0: **80 derived, 80 run, 0 NOT-MEASURED** (a derived zero). Five families first read exit 3 (PREREQUISITE NOT MET: no built `packages/*` dist) and were re-run green after `turbo run build --filter='./packages/**'`: `check:doc-formula-expressions`, `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`. `check:pm-dispatch-gates`: 1935 fast-tier cases pass. Also run, outside the derivation because the ui lane's pins read `ci.yml` as text: `pnpm --dir ui exec vitest run scripts/__tests__` → exit 0, Test Files 178 passed, 2 skipped (180); Tests 5097 passed. ``` node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-platform-checklist-watchdog.mjs :: exit 0 node scripts/check-platform-checklist-watchdog.mjs --self-test :: exit 0 node scripts/check-release-section-coverage.mjs :: exit 0 node scripts/check-release-section-coverage.mjs --strict :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-step-collectors.mjs :: exit 0 node scripts/check-step-collectors.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/ci/scheduled-full-run.mjs --self-test :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0 node scripts/pm/board-snapshot.mjs --self-test :: exit 0 node scripts/pm/check-governed-queue-guard.mjs --self-test :: exit 0 node scripts/pm/check-harness-current.mjs --self-test :: exit 0 node scripts/pm/ci-failure.mjs --self-test :: exit 0 node scripts/render-release-coverage-anchor.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:aggregator-roster :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:declared-population-live :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:docs-audit-scope :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:node-version :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:platform-checklist :: exit 0 pnpm check:pm-close-cards :: exit 0 pnpm check:pm-expected-skips :: exit 0 pnpm check:pm-fleet-write-dispatch :: exit 0 pnpm check:pm-fleet-write-execute :: exit 0 pnpm check:pm-fleet-write-validate :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-governed-prose :: exit 0 pnpm check:pm-half-states :: exit 0 pnpm check:pm-issue-transfer :: exit 0 pnpm check:pm-post-stamped :: exit 0 pnpm check:pm-record-recognisers :: exit 0 pnpm check:pm-skill-id-lint :: exit 0 pnpm check:pm-skill-ratchet :: exit 0 pnpm check:pnpm-acquisition :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:pnpm-flag-spellings :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:required-contexts :: exit 0 pnpm check:scripts-symbol-anchors :: exit 0 pnpm check:shard-attestation :: exit 0 pnpm check:skill-frame-sync :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:stall-guard-budget :: exit 0 pnpm check:stall-guard-headroom :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:workflow-status-functions :: exit 0 pnpm check:workflow-step-name-quoting :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 ``` </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. - [ ] 3. No formatter ran; `pnpm lint` is CI's repo-wide run, not run locally. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. Nothing publishes (CI workflow, repo scripts and instruction prose): `skip-changeset`; both declaration lines are above. - [x] 6. `packages/spec` not touched. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the maintainer's Chinese summary). - [ ] The *Before Submitting* list in CONTRIBUTING.md is done. (Local runs are the derived gate subset above; the full farm is CI's.) --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmNhqdySz5JeM3ARKGW)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 500bc9d commit 833e4a8

5 files changed

Lines changed: 119 additions & 14 deletions

File tree

‎.claude/skills/pm-dispatch/references/platform-readings.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,9 +387,9 @@
387387
- 本车道强制多是过宽的回忆:`dispatch-gates.mjs --tier PATH` 逐路径现推,路径线是下限非放行。
388388
- 该脚本只答自己那棵树:姊妹仓路径回 absent from this tree,姊妹仓的档位与条款②只能手推。
389389
- `os-verify-lock.sh` 只住 objectstack:objectui 无它,跑本仓副本读的是容器级锁,非该仓深度。
390-
- required checks 的名单是每仓事实,objectstack 八个:
390+
- required checks 的名单是每仓事实,objectstack 九个:
391391
`TypeScript Type Check` · `Lint & Repo Gates` · `Test Core` · `Build Core` · `Lint UI` ·
392-
`Governed Surface Queue Guard` · `Type Check UI` · `Build & E2E UI`。
392+
`Governed Surface Queue Guard` · `Type Check UI` · `Test UI` · `Build & E2E UI`。
393393
- ⭐ 本表的 `mustName` 声明 `statesTheSet` ⇒ 必须逐字等于注册表,加注册行不跟改本表即红。
394394
- ⇒ 门绿即名单逐字齐全的读数;⛔ 但数量词未钉:实测 七→六 照绿,只能手跟改。
395395
- ⛔ 聚合命令同样不作判定:`check:type-check-debt` 可在包级 typecheck 绿时红。

‎.github/workflows/ci.yml‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3722,6 +3722,74 @@ jobs:
37223722
pnpm --dir ui run test --shard=${{ matrix.shard }}/16 --reporter=default --reporter=github-actions
37233723
fi
37243724
3725+
test-ui-gate:
3726+
# Stable required-check name for the sharded Test UI matrix (#26460, ruling
3727+
# A, comment 6435318516), in test-gate's shape and for test-gate's reason
3728+
# (#3622): a matrix publishes only `Test UI (k/16)`, so the bare `Test UI`
3729+
# context the ruleset requires can come from nothing but this job, and the
3730+
# shard count stays free to move without a settings edit. Until this job,
3731+
# none of the 16 shards was required and the queue landed PRs whose Test UI
3732+
# was red (#26419 → #26421).
3733+
#
3734+
# `name:` and `if: always()` are both load-bearing, as on test-gate. The
3735+
# name IS the required context (`check:required-contexts` pins the
3736+
# literal), and a gate that skips because a dependency failed publishes no
3737+
# context at all, which leaves the PR permanently pending (#3622). ⛔ Never
3738+
# `if: !cancelled()` here.
3739+
#
3740+
# The verdict reads the matrix rollup `needs.test-ui.result`, and only
3741+
# `success` passes, with one exception: `skipped`, when `filter` concluded
3742+
# `success` AND said the diff reaches no ui path (the shards' own `if:`).
3743+
# That is #4928's skipped-only-when-filter-succeeded guard, and it makes a
3744+
# queue build or PR that touches no ui path read green here the way Lint
3745+
# UI and Build & E2E UI read `skipped` there. Everything else is red:
3746+
# `failure`; `cancelled`, which is also what a shard killed by its own
3747+
# `timeout-minutes` reads (on a superseded push this paints the OLD sha
3748+
# red, which no required check reads); a `skipped` that `filter` does not
3749+
# explain; and any lifecycle value such as the `abandoned` test-gate met
3750+
# (run 31120902911). Unlike test-gate it counts no shard credentials: the
3751+
# Test UI shards publish none, and adding them is a change to `test-ui`,
3752+
# which is the ui lane's job.
3753+
name: Test UI
3754+
needs: [test-ui, filter]
3755+
# ── ROSTER DECLARATION (#10490) ───────────────────────────────────────
3756+
# `check:aggregator-roster` holds this equal to `needs:` in both
3757+
# directions. `filter` is the #4928 input, not a lane.
3758+
env:
3759+
OS_AGGREGATOR_MEMBERS: test-ui
3760+
OS_AGGREGATOR_NON_MEMBERS: filter
3761+
if: always()
3762+
runs-on: ubuntu-latest
3763+
timeout-minutes: 10
3764+
permissions:
3765+
contents: read
3766+
steps:
3767+
- name: Verify Test UI shard results
3768+
env:
3769+
OS_TEST_UI_RESULT: ${{ needs['test-ui'].result }}
3770+
OS_FILTER_RESULT: ${{ needs.filter.result }}
3771+
OS_UI_TREE: ${{ needs.filter.outputs.ui_tree }}
3772+
OS_UI: ${{ needs.filter.outputs.ui }}
3773+
run: |
3774+
echo "Test UI (k/16) rollup: '$OS_TEST_UI_RESULT'; filter: '$OS_FILTER_RESULT' (ui_tree='$OS_UI_TREE', ui='$OS_UI')."
3775+
case "$OS_TEST_UI_RESULT" in
3776+
success)
3777+
echo "✓ Test UI: every shard concluded success."
3778+
;;
3779+
skipped)
3780+
if [ "$OS_FILTER_RESULT" = 'success' ] && { [ "$OS_UI_TREE" = 'false' ] || [ "$OS_UI" = 'false' ]; }; then
3781+
echo "✓ Test UI: the shards were skipped because filter found no ui path in this diff."
3782+
else
3783+
echo "::error title=Test UI::the Test UI shards were skipped, but filter (result '$OS_FILTER_RESULT', ui_tree='$OS_UI_TREE', ui='$OS_UI') does not explain the skip. A skip passes only when filter succeeded and said the diff reaches no ui path (#4928)."
3784+
exit 1
3785+
fi
3786+
;;
3787+
*)
3788+
echo "::error title=Test UI::the Test UI shards concluded '$OS_TEST_UI_RESULT', not success. Read the red 'Test UI (k/16)' check-runs on this sha for the failing test; a shard killed by its timeout reads 'cancelled'."
3789+
exit 1
3790+
;;
3791+
esac
3792+
37253793
# ── Lint UI and Build & E2E UI: the ceilings (#25869 row F13) ──────────
37263794
# objectui's rule (its lint.yml `Lint` ceiling): the smallest round number
37273795
# that is both >= 3x the slowest successful run and >= that run + 15 min, so

‎AGENTS.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -520,11 +520,11 @@ Even inside your own worktree, operate defensively:
520520
yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it:
521521
every poll then misreads "not on `main` yet" as "queued". Always read *two* things:
522522
the queue branch **and** `origin/main`. And **the queue enforces only the required
523-
set** — eight contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`,
524-
`Test Core`, `Build Core`, `Governed Surface Queue Guard`, `Lint UI`, `Type Check UI` and
525-
`Build & E2E UI`. A check outside those eight is advisory and rides through, and an advisory red
523+
set** — nine contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`,
524+
`Test Core`, `Build Core`, `Governed Surface Queue Guard`, `Lint UI`, `Type Check UI`, `Test UI`
525+
and `Build & E2E UI`. A check outside those nine is advisory and rides through, and an advisory red
526526
that lands rides `main`'s merge ref into every later PR until stanched. A required context is
527-
matched by check-run name, so a rename detaches its gate silently — treat those eight names as contract.
527+
matched by check-run name, so a rename detaches its gate silently — treat those nine names as contract.
528528
**Two speeds** (ruling record 6120820653): PR and queue runs carry the fast set, 10 minutes of
529529
wall per queue entry; `main`'s scheduled (4-hourly) and push runs carry the full battery — dogfood, e2e,
530530
`Temporal Conformance (live PG + MySQL)` — read by the publish lane before

‎scripts/check-aggregator-roster.mjs‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
*
1111
* ## The gap this closes
1212
*
13-
* Four of this repo's required contexts are aggregator jobs standing in for a
13+
* Five of this repo's required contexts are aggregator jobs standing in for a
1414
* set of real jobs, each deciding its verdict from what it lists in `needs:`:
1515
* `test-gate` / `Test Core` and `dogfood-gate` / `Dogfood Regression Gate` in
1616
* ci.yml, and `typecheck` / `TypeScript Type Check` and, since #22991,
17-
* `lint` / `Lint & Repo Gates` in lint.yml.
17+
* `lint` / `Lint & Repo Gates` in lint.yml, and, since #26460, ci.yml's
18+
* `test-ui-gate` / `Test UI`.
1819
*
1920
* Nothing held those rosters to anything. The failure mode is one forgotten
2021
* line:
@@ -68,7 +69,7 @@
6869
*
6970
* Discovery is by declaration, so a NEW aggregator that adds the two keys is
7071
* covered without editing this file. `REQUIRED_AGGREGATORS` is the floor
71-
* underneath that: the four jobs that carry a branch-protection-required
72+
* underneath that: the five jobs that carry a branch-protection-required
7273
* context must each be found AND declared, so deleting the declaration to make
7374
* a red go away is itself red.
7475
*
@@ -110,6 +111,8 @@ export const REQUIRED_AGGREGATORS = [
110111
{ workflow: 'lint.yml', job: 'typecheck', context: 'TypeScript Type Check' },
111112
// #22991: the single 193-step job became an aggregate over eight lanes.
112113
{ workflow: 'lint.yml', job: 'lint', context: 'Lint & Repo Gates' },
114+
// #26460: the `Test UI` aggregate over the 16 `test-ui` shards.
115+
{ workflow: 'ci.yml', job: 'test-ui-gate', context: 'Test UI' },
113116
];
114117

115118
/** A legal GitHub Actions job id. A comma or a stray quote fails here. */
@@ -507,6 +510,7 @@ async function selfTest() {
507510
from: ' needs: [dogfood, dogfood-verify, filter]\n',
508511
to: ' needs: [dogfood, filter]\n',
509512
},
513+
{ label: 'test-ui-gate loses its matrix from needs:', file: 'ci.yml', member: 'test-ui', from: ' needs: [test-ui, filter]\n', to: ' needs: [filter]\n' },
510514
];
511515
for (const scenario of dropped) {
512516
const verdict = fixture(scenario.label, scenario.file, (s) => s.replace(scenario.from, scenario.to));
@@ -536,6 +540,7 @@ async function selfTest() {
536540
to: ' needs: [dogfood, dogfood-verify, filter, dogfood-ghost]\n',
537541
ghost: 'dogfood-ghost',
538542
},
543+
{ label: 'test-ui-gate needs a job that was deleted', file: 'ci.yml', from: ' needs: [test-ui, filter]\n', to: ' needs: [test-ui, filter, test-ui-ghost]\n', ghost: 'test-ui-ghost' },
539544
];
540545
for (const scenario of phantom) {
541546
const verdict = fixture(scenario.label, scenario.file, (s) => s.replace(scenario.from, scenario.to));

‎scripts/check-required-contexts.mjs‎

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,15 @@ export const REQUIRED_CONTEXTS = [
471471
'after #25856 (fc2aaaec52) closed bill item 49 with the job green on the main push run 40253800516',
472472
carries: 'the type-check + vitest-setup type-check family of the `ui/` workspace',
473473
},
474+
{
475+
workflow: 'ci.yml',
476+
job: 'test-ui-gate',
477+
context: 'Test UI',
478+
authorized:
479+
'#26460 ruling letter A (staged), maintainer 「批 #408 同意」 2026-10-27T06:24Z, recorded in comment 6435318516 — ' +
480+
'after the ui flake reading 6436579765 (0/50); the ruleset entry is the maintainer\'s, in the same sitting as the merge (#9325)',
481+
carries: 'the sharded `ui/` vitest matrix, `Test UI (k/16)`, read through the aggregate job',
482+
},
474483
];
475484

476485
/**
@@ -585,10 +594,10 @@ export const INSTRUCTION_SURFACES = [
585594
// with every gate green, which is what put the exclusivity rule below in
586595
// the judge. Then 7→5 by #22998 (two-speed CI: the dogfood and temporal
587596
// contexts moved to the full battery). Then 5→7 by #25853 (the `ui/`
588-
// import's `Lint UI` and `Build & E2E UI`) and 7→8 with `Type Check UI`
589-
// once bill item 49 closed (#25856). The full set is pinned here so the
590-
// corrected sentence cannot rot back — a rename in ANY of the eight
591-
// reddens this gate instead.
597+
// import's `Lint UI` and `Build & E2E UI`), 7→8 with `Type Check UI`
598+
// once bill item 49 closed (#25856), and 8→9 with the `Test UI` aggregate
599+
// (#26460). The full set is pinned here so the corrected sentence cannot
600+
// rot back — a rename in ANY of the nine reddens this gate instead.
592601
file: 'AGENTS.md',
593602
statesTheSet: true,
594603
mustName: [
@@ -600,6 +609,7 @@ export const INSTRUCTION_SURFACES = [
600609
'Lint UI',
601610
'Build & E2E UI',
602611
'Type Check UI',
612+
'Test UI',
603613
],
604614
},
605615
{
@@ -631,6 +641,7 @@ export const INSTRUCTION_SURFACES = [
631641
'Lint UI',
632642
'Build & E2E UI',
633643
'Type Check UI',
644+
'Test UI',
634645
],
635646
},
636647
{
@@ -652,7 +663,7 @@ export const INSTRUCTION_SURFACES = [
652663
* of the six surfaces have one; `AGENTS.md` does not, because a repo-root FILE
653664
* has no separator to be found by — so an AGENTS.md card derived this gate not
654665
* at all, while that surface is the one carrying `mustName` for every
655-
* required context (widened 2→6 by #9677, 6→7 by #17798, 7→5 by #22998, 5→7 by #25853, 7→8). Editing the merge-queue
666+
* required context (widened 2→6 by #9677, 6→7 by #17798, 7→5 by #22998, 5→7 by #25853, 7→8, 8→9 by #26460). Editing the merge-queue
656667
* paragraph there is precisely how this gate goes red, and it was reachable
657668
* only by judgment.
658669
*
@@ -1936,6 +1947,13 @@ async function selfTest() {
19361947
renamedTestCore.problems.some((p) => p.includes("job 'test-gate'") && p.includes("'Test Core'")),
19371948
'growing a suffix on the bare `Test Core` name ⇒ red — the exact literal is the contract, not decoration',
19381949
);
1950+
// The same property on the second matrix aggregate (#26460): `Test UI` is the
1951+
// bare name of `test-ui-gate`, and its shards are `Test UI (k/16)`.
1952+
const renamedTestUi = fixture('rename Test UI', 'ci.yml', (s) => s.replace(' name: Test UI\n', ' name: Test UI (aggregate)\n'));
1953+
assert(
1954+
renamedTestUi.problems.some((p) => p.includes("job 'test-ui-gate'") && p.includes("'Test UI'")),
1955+
'growing a suffix on the bare `Test UI` name ⇒ red, naming the aggregate job and the required context',
1956+
);
19391957

19401958
// ── (2) the job disappearing entirely ─────────────────────────────────────
19411959
// Re-pointed from `console-pin` to `temporal-conformance` when #9533 dropped
@@ -2087,6 +2105,20 @@ async function selfTest() {
20872105
baseline.problems.length === 0 && sources['ci.yml'].includes(TEST_CORE_SHARD_NAME),
20882106
'the real suffixed shard name coexists with the bare gate name (assertion 9 compares whole names, not prefixes)',
20892107
);
2108+
// The second live specimen (#26460): the `test-ui` shards and their `Test UI`
2109+
// aggregate, the same shape with the matrix width read the same way.
2110+
const TEST_UI_SHARD_NAME = `name: Test UI (\${{ matrix.shard }}/${shardTotal(sources['ci.yml'], 'test-ui')})`;
2111+
const collidedUi = fixture('collide the Test UI shard name with its gate name', 'ci.yml', (s) =>
2112+
s.replace(TEST_UI_SHARD_NAME, 'name: Test UI'),
2113+
);
2114+
assert(
2115+
collidedUi.problems.some((p) => p.includes("job 'test-ui'") && p.includes('published by ci.yml:test-ui-gate')),
2116+
'a Test UI shard wearing the bare `Test UI` context name ⇒ red',
2117+
);
2118+
assert(
2119+
sources['ci.yml'].includes(TEST_UI_SHARD_NAME),
2120+
'the real suffixed Test UI shard name coexists with the bare `Test UI` gate name',
2121+
);
20902122

20912123
// ── (8) a registry that lists one context twice ──────────────────────────
20922124
battery('(8) a registry that lists one context twice');

0 commit comments

Comments
 (0)