ci(test): e2e and live filename tiers move to a nightly run on main - #16481
Conversation
One switch, OS_TEST_TIERS (queue | nightly, unset = queue), read once in scripts/nightly-tiers.mjs and applied in packages/cli's tier walk — the one config that owns tier files (60 *.e2e.test.ts; the tree owns no *.live.test.*). turbo.json hashes the variable into every test task so a nightly can never replay a queue-mode cache entry. ci.yml's shard step spells queue; test-nightly-tiers.yml runs the two tiers on main, two shards through the existing partitioner, and files or refreshes one deduplicated card on red. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
…ion declaration
dispatch-gates refuses a family reachable only from a scheduled workflow, so
the nightly carries the patrol posture: a pull_request trigger paths-filtered
to its own file and the switch reader. The reader declares no-path-population
(its self-test drives a temp fixture) and resolves vitest's CLI entry by
walking up from require.resolve('vitest') instead of spelling a subpath the
derivation read as a population. Concurrency keyed per ref so a PR run never
queues behind main's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 22 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9ea72e5e05c817d2360e42bef104d8f5a87c393a && git checkout 9ea72e5e05c817d2360e42bef104d8f5a87c393a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 21c5dcbb3d073716c7d15354a6f9b312cf2205ff 07464ab2279e4f5e18ccb3bb682359d14a706f67 && git checkout -B drift-repro 21c5dcbb3d073716c7d15354a6f9b312cf2205ff && git merge --no-ff 07464ab2279e4f5e18ccb3bb682359d14a706f67
node scripts/docs-audit/affected-docs.mjs --json 21c5dcbb3d073716c7d15354a6f9b312cf2205ff |
…ghtly-e2e-live-tiers
…oduction-posture boot needs The test sets NODE_ENV=production and boots a real kernel; LocalCryptoProvider then needs a stable key, which this test never supplied. It was green only when an earlier dev-mode boot in the same job had persisted one to the runner's home directory (the ordering coupling serve-process.ts documents) — and with the e2e tier no longer in the queue run, that sibling is gone. Set OS_SECRET_KEY to a fresh random value in beforeAll, restored in afterAll beside NODE_ENV; nothing is written to disk and nothing is depended on from a sibling. Ruled by the seat for exactly this one file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Fixes #16455
Fixes #16491
What
The
e2eandlivetest tiers leave the per-PR and merge-queue runs and run nightly onmain. Selection is by the existing filename tiers only (*.e2e.test.*,*.live.test.*); no test file is renamed, deleted or edited.OS_TEST_TIERS(queue|nightly; unset reads asqueue; anything else is refused loudly), read once inscripts/nightly-tiers.mjsand applied inpackages/cli/vitest-tiers.ts's walk — the one place both of that package's vitest projects derive their population from, sounit/integrationstay a partition of whatever the setting selects. The unit project becomes an explicitincludelist (population minus integration) instead ofexclude: INTEGRATION_FILES, because an exclude-shaped project falls back to vitest's defaultincludeand would collect the tier files the queue must not run.turbo.jsonhashesOS_TEST_TIERSinto everytesttask (env, notpassThroughEnv): turbo 2.10 runs in strict env mode and strips undeclared variables, so without this the switch never reaches vitest — and a per-package<pkg>#testoverride does not inherit the base task'senv(measured with--dry-run=json: identical hash across modes until each override named it), so all 27 overrides name it. A nightly can therefore never replay a queue-mode cache entry..github/workflows/ci.yml: only the "Run this shard's tests" step gainsOS_TEST_TIERS: queuein itsenv:. Nothing else in the file moves..github/workflows/test-nightly-tiers.yml(new,coverage-nightly.yml's shape:schedule+workflow_dispatch, node 22, pnpm, restore-only caches): enumerates the tier-owning packages (nightly-tiers.mjs --packages,turbo lsdocument shape), refuses loudly if any of them does not honour the switch (--check, judged by what vitest collects under each setting), shards them withscripts/partition-test-shards.mjsacross 2 shards, runs the same leg loop as Test Core underOS_TEST_TIERS=nightlywith vitest's JSON reporter beside the default one, and a singlereportjob files or refreshes one card on red — titlenightly-tiers: red on main, labelsbug·domain:devx·priority:p1on create only, body with the run link and the failing files (from the JSON reports, never a log regex), de-duplicated by scanning open issues for the title prefix through the repo-scoped REST API with the workflow's token (bounded pages; a truncated scan refuses to file). On green it does nothing; on any ref butmainit writes nothing and publishes the would-be card to the run summary.Deviation from the card: one test edit, ruled by the seat
The card's no-test-edit clause is lifted for exactly one file,
packages/cli/src/commands/secret/orphans.driver-contract.test.ts, by the seat's ruling on the rider #16491 (its claim, first sentence, verbatim): "Claim: PM loop round 9 — rider on flight X (#16455, PR #16481), ruled by the seat at X's delivery 04:3xZ: the card's ⛔ no-test-edit clause is lifted for exactly this one file." — #16491 (comment)The edit (commit
07464ab227, +7/−1):beforeAllsaves and setsOS_SECRET_KEYtorandomBytes(32).toString('hex')besideNODE_ENV = 'production'— the variableserve-process.tsalready applies to its spawns — andafterAllrestores it besideNODE_ENV; onenode:cryptoimport. Nothing is written under$HOME, nothing is depended on from a sibling, no other test file is touched. Measured after the edit: the file alone withOS_SECRET_KEYunset and with it set bothTest Files 1 passed (1) / Tests 5 passed (5), and$HOME/.objectstack/dev-crypto-keystill absent afterwards.Measurement first (ruling 1)
At
6eba38f5a3: 72vitest.config.tsfiles, every one a standalonedefineConfig, none extending a shared config; no rootvitest.config.*, novitest.workspace. Tier-named files: 60*.e2e.test.ts, all inpackages/cli; 0*.live.test.*anywhere (the brief's "23*live*files" are substring hits —live-mysql,live-dialect-matrix,lint-liveness,queue-delivery— not tier-named, and not selected). So the count of configs that would have to read the switch to exclude the two tiers is 1 (≤ 10 ⇒ continue), and that config already partitions its suite throughvitest-tiers.ts, which is where the switch is applied.Collection, read from
vitest list --filesOnlyinpackages/cli(272 test files on disk):queue→ 212 (180 unit + 32 integration);nightly→ 60 (1 unit + 59 integration) == the tier files on disk; 212 + 60 = 272, no file in both projects under either setting; unset ==queue;OS_TEST_TIERS=night→ exit 1 naming the two legal values.Sharding (ruling 4)
Two shards, derived rather than picked: cli is the only tier-owning package (~75% of its 1231 s CI suite is e2e ⇒ ≈ 15 min),
FILE_SHARDED_PACKAGESalready cuts it into two vitest slices, and the partitioner refuses one shard (assertSlicesSpread: "bin 1 holds more than one slice of @objectstack/cli … Refusing to shard"), so 2 is the smallest legal count; each shard carries one slice (@objectstack/cli 1/2/2/2, 229 s predicted each from the dataset), well under 60 min.Ablation (ruling 5)
Scratch commit
8ff289aba0on top of770dd18205addedpackages/cli/test/zz-nightly-tiers-ablation.e2e.test.ts(its marker line confirmed on disk bygrep -c= 1 before the run).OS_TEST_TIERS=queue:vitest list --filesOnly→ 212 files, scratch absent (0 hits); the narrowedvitest run <scratch>→No test files found, exiting with code 1; the whole package →Test Files 1 failed | 211 passed (212)/Tests 2719 passed | 6 expected fail | 5 skipped (2730)— the one failure is not the scratch file (0 mentions in the log) butsrc/commands/secret/orphans.driver-contract.test.tsrefusing to boot without a crypto key (see "Findings" below).OS_TEST_TIERS=nightly: list → 61 files, scratch present (1 hit); the narrowed run →FAIL |unit| test/zz-nightly-tiers-ablation.e2e.test.ts … AssertionError: expected 'collected under OS_TEST_TIERS=nightly' to be 'NIGHTLY_TIERS_ABLATION_MARKER_RED', exit 1.queuewith the scratch file on disk:Test Files 1 passed (1)/Tests 22 passed (22).git reset --hard 770dd18205→ HEAD770dd18205,git status --porcelainempty, scratch file absent (ls: No such file or directory). The scratch commit was never pushed.The partition pin itself, on the committed tree:
queue→ 22/22 green; unset → 22/22 green;nightly→No test files found, exiting with code 1— the pin carries no tier name, so under the nightly setting it is not collected, which is the ruled behaviour (the nightly runs exactly the two tiers), not a red.Nightly smoke run (ruling 5)
Two real-runner runs on
770dd18205, both green, both with the board write skipped (notmain):pull_request, the paths-filtered trigger the dispatch-gates pin requires): https://github.com/objectstack-ai/objectstack/actions/runs/34080518326 —Nightly tiers (1/2)17m11s,Nightly tiers (2/2)15m53s,File or refresh the red card0m18s (success; summary: "Board write skipped: this run is on refs/pull/16481/merge, not main").workflow_dispatchon the branch (REST answered 403Resource not accessible by integrationfor the container token; the MCP route queued it): https://github.com/objectstack-ai/objectstack/actions/runs/34081750890 —Nightly tiers (1/2)16m50s,Nightly tiers (2/2)13m07s, report job 0m15s,success.So the whole e2e tier (60 files, two slices) finishes in under 18 minutes per shard on a hosted runner; no real e2e suite is red on this head.
Serial with flight V (ruling 7)
V's branch
claude/issue-16453-merge-group-affected-setwas pushed (heade6fe28e9) before this branch's first content push;git merge-tree --write-tree HEAD refs/flight/16453→ exit 0, tree2a13194c9a; the same with-c merge.os-regen.driver=false→ exit 0, same tree. V'sci.ymlhunks (lines 350 / 415) and this PR's (line 575) do not overlap. V's PR #16471 has since merged;origin/main(4500bc5d79) is merged into this branch as2fe914aa48(merge commit, no rebase) and the shard step composes:OS_TEST_TIERS: queuesits in the "Run this shard's tests"env:beside V'sscripts/ci/select-shard-packages.shstep. Flight C3 (#16466,claude/issue-16466-repo-conformance-task) also touchesturbo.json:git merge-tree --write-tree HEAD refs/flight/16466against its pushed head is recorded in the report (twice, the second with-c merge.os-regen.driver=false).Gates (ruling 6)
All on
770dd18205unless stated.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 77 commands (from git, off the merge base); every one ran with its exit captured before any pipe (one log per command).d2fab7ab77; the four non-zero and their disposition:check:declared-population-live(1) — a real finding onscripts/nightly-tiers.mjs(avitest/package.jsonliteral read as a population declaration): fixed in770dd18205(resolve vitest's root by walking up;no-path-populationmarker) → re-run exit 0 (223 of 272 families … every one reaches this tree's 8088 tracked files).check:pm-dispatch-gates(1) — a real finding: a family reachable only from a scheduled workflow is refused; fixed by thepull_requestpaths trigger + the marker → re-run in progress on770dd18205at PR time (its self-test drives ~1,500 cases and takes ~14 min); result in the report.check:dual-build-cjs-loads(3,PREREQUISITE NOT MET: packages outside cli's closure have nodist/) — NOT MEASURED locally; CI builds fresh. This diff touches no package source or build config.check:type-check-debt(3, node heap crash under a neighbour's concurrent gate batch) — re-run alone under the lock with 6144 MB: exit 0 (5 ledger entries re-measured … none above its recorded number; surplus: none).test/vitest-tiers-partition.test.ts:queue22/22, unset 22/22,nightlynot collected (above).node scripts/check-shard-attestation.mjs --self-test→ exit 0 (144 assertions).node scripts/pm/check-governed-merges.mjs --test <final file list>→ exit 0:0 of 7 path(s) hit the register … ✅ NOT governed.pnpm check:nul-bytes→ exit 0 (scanned 8081 text file(s) … no raw ASCII control bytes); own control-char grep over the seven files: clean.pnpm lint(eslint . --no-inline-config) throughscripts/pm/os-verify-lock.sh(slotissue-16455, one acquisition, waited 27 s):LINT-EXIT=0, held 2m37s, on770dd18205with a clean tree.pnpm --filter @objectstack/cli typecheck→ exit 0 (check:test-typecheck: OK — 3 file(s) / 28 error(s) … held in test-typecheck-debt.json, unchanged).node scripts/nightly-tiers.mjs --self-test→ 46 cases;--check→1 tier-owning package(s) honour OS_TEST_TIERS (judged by what vitest collects);node scripts/partition-test-shards.mjs --self-test→ OK;check-declaration-mirrors→ OK (pairsscripts/nightly-tiers.d.mts).--dry-run=jsonshows@objectstack/cli#testspecified.env: ['OS_TEST_TIERS']and a different hash underqueuevsnightly(035285c9dd71f80cvs902cf67d871aad09);@objectstack/types#buildhash unchanged across modes (builds are not rehashed).Findings
src/commands/secret/orphans.driver-contract.test.tsreds underqueueon this box (the only red of 212). It setsNODE_ENV=productionand boots a real kernel;LocalCryptoProviderderives its posture fromNODE_ENValone and, in production, needsOS_SECRET_KEY,OS_DEV_CRYPTO_KEYor a persisted$HOME/.objectstack/dev-crypto-key; the test provides none. Alone withOS_DEV_CRYPTO_KEYset it passes 5/5; alone without, it refuses (exit 1). No file in the queue population writes that key (absent on this box after all 212 files ran) — whether main's green Test Core today rests on an e2e spawn minting it on the runner's$HOMEis what this PR's Test Core measures. ⛔ Not edited here (the ruling forbids editing any test file); resolved by the ruled rider edit above (test(cli): orphans.driver-contract.test.ts's production-posture boot depends on a sibling e2e spawn minting the dev crypto key — deterministic red once the e2e tier leaves the queue #16491).Confirmed on this PR's CI:
Test Core (5/6)(cli slice 1/2) https://github.com/objectstack-ai/objectstack/actions/runs/34080518324/job/101614831504 —Test Files 1 failed | 106 passed (107), the one failure exactly this test with the same refusal; the completeness guard is OK (12 of 12 packages, 8118 tests accounted for). Every other check on the head is green (43 check runs: 2 failed — this shard and its aggregate — 5 skipped, the rest success). The mechanism is documented inpackages/cli/test/helpers/serve-process.ts's header: a dev-mode boot persists a minted key to$HOME/.objectstack/dev-crypto-key, "put there by any earlier dev-mode boot in the same run … a cross-test ordering coupling through the runner's home directory, and under vitest's parallel workers it is nondeterministic". The helper's own spawns passOS_SECRET_KEYfor that reason; this test's in-process production-posture boot never did, and only an e2e sibling that still spawnsbin/run-dev.jswithout a key (e.g.test/start-port-banner-agreement.e2e.test.ts,test/migrate-meta.e2e.test.tsspawnos start) minted the file for it. Moving the e2e tier out of the queue turns that nondeterministic green into a deterministic red. That is the one-file edit recorded under the deviation heading above; on this branch the test now declares its own key and passes with or without the variable in the environment.Out of scope / open questions
nightly-tiers.mjs --checkmakes a package that adopts a tier without reading the switch red on the nightly; nothing makes it red on the pull request that introduces it (that would be acheck:script wired intolint.yml, outside this card's file surface). The seat filed that follow-up as ci(test-tiers): per-PR gate for a package that adopts a *.e2e.test.* or *.live.test.* file without reading OS_TEST_TIERS #16494; this PR is not widened..claude/agents/os-dev.mdstill names cli's tiers as CI's integration layer; flight Y owns that file and it is untouched here.Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox (body edited after the nightly runs completed and again for the seat's rider ruling; the footer below is the platform's edited form)
🤖 Generated with Claude Code
https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Generated by Claude Code