Skip to content

Commit 3083906

Browse files
ci(test-core): carry the shard slice in a task env so the slice leg's hash sees its upstream closure (#19278) (#20824)
Fixes #19278 Clause-②: no Seat review ACCEPT on the landing head `e00136407`: comment `5909822071`. No contract-review record is owed, because the diff touches no review face and `Clause-②: no` (`contract-review.md`). ## What it changes Test Core's file-level slice leg (`.github/workflows/ci.yml`) ran `pnpm turbo run test --filter=$PKG --only --force … -- --shard=$SLICE`. - `--only` drops the dependency tasks, so the slice's task hash sees only the package's own files and the hand-declared `$TURBO_ROOT$` inputs. An upstream change does not move it. - `--force` (PR #19271) keeps such a slice from being replayed from cache, but the hash stays blind. That is #19278. This branch carries the slice in a task-declared env instead: - `turbo.json` declares `OS_TEST_SHARD` on `test` and on `@objectstack/cli#test`. A package-specific task replaces the generic one, so both need it. - `packages/cli/vitest.config.ts` reads it into vitest's `shard`, typed through vitest's CLI-options type. A literal `test.shard` fails typecheck with TS2769. - The slice leg runs `env OS_TEST_SHARD=$SLICE pnpm turbo run test --filter=$PKG …`, with no passthrough, no `--only` and no `--force`. - `scripts/partition-test-shards.mjs --self-test` gains a battery. It fails any package the partitioner can slice whose turbo task or vitest config does not carry the variable. - `scripts/measure-test-shard-timings.mjs` reads an env-carried slice back from the run summary's `environmentVariables.configured` digest, and keeps the `cliArguments` path that nightly-tiers still uses. Without this, a timings refresh would record one slice as the package's whole cost (#16173's defect). - Comments the change made false are rewritten: the slice leg's block and the closure-build step in `ci.yml`, and `report-test-timings.mjs`. ## Measured (round 1, the dev's report `5907562928` on #19278, turbo 2.10.10, vitest 4.1.11) | | old leg | new leg | |:--|:--|:--| | `cli#test` hash after an upstream edit in `spec` / `types` | unchanged | moves each time | | an edit inside `cli` itself | moves | moves | | control edit outside cli's closure | — | unchanged | | slice after an undeclared upstream edit, no `--force` | `cache hit, replaying logs` (not run) | `cache miss, executing` (runs) | | planned tasks, HIT / MISS | 1 task (the `--only` leg) | 62 tasks: 59 HIT / 3 MISS | ## Evidence on the final head `e00136407d` (handover steps 1 to 4) Final head: `e00136407d04e3b2871b30a9c6463a2f9f108f6c`, a merge of `origin/main` `4b45afaed5`. The six paths are byte-identical to `9df0b71851`, except one comment block in `scripts/measure-test-shard-timings.mjs` (`099e5ff641`, comment only). ### 1. The reader's new self-test cases, ablated - Every mutation went through `scripts/ablation-replace.mjs`: the anchor hit 1 then 0, the blob moved, the file was restored to the HEAD blob, and `git diff HEAD` was empty afterwards. A shell trap also guarded each run. - The reader blob was `453a072feedb` on the final head. The same table was measured first on `cb42313d89e6` (`9df0b71851`) and gave the same result. - Each mutation got two readings: - the real `--self-test`, its exit code and its first red; - an instrumented copy of the harness that records every red case instead of stopping at the first one. - Control: the instrumented harness with no mutation records 0 reds. | mutation | real `--self-test` | red cases in `env-carried slices (#19278)` | |:--|:--|:--| | A0: the digest path is dropped (`sliceOfLeg` never reads `OS_TEST_SHARD`) | exit 1, "read as null, not 2/3" | 1, 2, 4, 5, 6, 9, 10 | | A1: the env slice is computed but not returned | exit 1, same | 1, 2, 10 | | A2: the default map is unbound from `FILE_SHARDED_PACKAGES` | exit 1, the refusal on `@objectstack/cli` 2/2 | 2 | | A3: the `cliArguments` carrier is dropped | exit 1, older battery case 29 first | 3, plus older-battery cases 29, 31, 34 to 37 and 40 to 43 | | A4: the refusal is removed (an unmatched digest returns null) | exit 1, "not refused naming its candidates" | 4, 5, 6 | | A5: a package the partitioner does not slice ignores its digest | exit 1 | 5 | | A6: the empty-value guard is removed | exit 1 | 7 | | A7: the entry is picked without its `OS_TEST_SHARD=` prefix | exit 1 | 8 | | A8: the refusal of two disagreeing carriers is removed | exit 1 | 9 | - Every case goes red under at least one mutation. - The unmatched-digest case (4) goes red under A0 and A4. - Cases 6 and 10 have no mutation of their own. They are end-to-end pins over the code that cases 4 and 1 pin: case 6 checks that the refusal never reaches a dataset, and case 10 that three env slices sum to one package. - After the mutations the self-test reads OK and `git diff HEAD` is empty. - One comment was fixed as a result (`099e5ff641`). The battery comment said every case fails when the digest path is dropped. A0 shows that cases 3, 7 and 8 are controls that pass either way, and the comment now says so. ### 2. A real new-leg summary through the pipeline - This PR's CI summaries could not be read from this container. The artifact download redirects to blob storage, and the container's egress proxy refuses that with 403. The summaries were therefore produced locally with the leg's own shape, after the closure build: `OS_TEST_TIERS=queue env OS_TEST_SHARD=k/2 pnpm turbo run test --filter=@objectstack/cli --summarize --log-order=stream`. - Added for this box: `--concurrency=2`, `VITEST_MAX_WORKERS=2` and a private `--cache-dir`. None of them enters a task hash. - Commit: `95b97ef2e`. Its six paths and every `packages/cli` file are identical to the final head's. - Slice 1/2: `cache miss, executing 90ddc44dcc24b458`; 60 tasks, 59 cached; 152 files; 2069 tests passed and 1 skipped. In the summary, `cliArguments` is empty, and `environmentVariables.configured` holds `OS_TEST_SHARD=d939926f…` (the sha256 of `1/2`) and `OS_TEST_TIERS=00b109cf…`. - Slice 2/2: `cache miss, executing 52f80464736e668b`; 151 files; 1865 tests. - CI shows the same thing. In `Test Core (6/6)` on `9df0b71851` (job 109819361192, whose log was read through the MCP job-log tool): `@objectstack/cli:test: cache miss, executing 874a69822b22ac22`, 60 tasks, 59 cached, 151 files, 1865 tests. That is the same slice with the same counts. | reader | slice 1/2 | slice 2/2 | cli in the dataset | |:--|:--|:--|:--| | this branch | 644.411 s, read as slice 1/2 | 572.636 s, read as slice 2/2 | **1217.05 s** (the two slices summed) | | `origin/main` `810d42b69c` | 644.411 s, no slice | 572.636 s, no slice | 608.52 s (the median of two "whole" samples, which is #16173's defect) | - `report-test-timings.mjs`, with `--capture` run twice and then `--merge`: - cli reads 1217.05 s, "all 2 slices", complete, with 303 of 303 file lines. - With only slice 1/2's capture present, this branch labels the row "slice 1/2 — PART, not a total". The `origin/main` script prints 644.41 s as cli's total, with no note. - `partition-test-shards.mjs --check-drift`: - It predicts **229.1 s** for cli, one slice's share of the 458.15 s weight, against 644.4 s measured. That is DRIFT at 2.81x, exit 1. The ratio is this shared container, about 2.7 times slower than CI. The number that matters is the prediction. - The `origin/main` script predicts 458.1 s, the whole package, and reports OK at 1.41x. It compares one slice against the whole package's weight. - An unmatched digest on a real summary. One hex digit of the `OS_TEST_SHARD` digest in slice 1/2's summary was flipped: - `measure-test-shard-timings.mjs` exits 1, with the refusal naming the candidates `1/2, 2/2`, and writes no dataset. - `--check-drift` exits 1 with the same refusal. - `report-test-timings.mjs` exits 0, as designed, because it may not fail the job it reports on. It lists the refusal under "Problems while reading this run" and shows cli as "slice 2/2 — PART, not a total". It never shows cli as a whole sample. ### 3. The merge of `origin/main` - Two merge commits, no rebase: `95b97ef2e` merged main at `810d42b69c`, and `e00136407` merged main at `4b45afaed5`. - The second merge was needed because `dispatch-gates` flagged the first derivation as stale. Main had changed `scripts/pm/fleet-write/dispatch.mjs`, one of the files the derivation reads. - Neither merge conflicted. Main touched none of the six paths. After each merge the six paths were byte-identical to the branch's own. - Main has since moved to `22e584c9d`. Neither of the two new commits touches the six paths or anything the derivation reads, and the derivation says so. ### 4. Gates and the card's acceptance, on `e00136407` **Gates.** - `dispatch-gates --commands --repo objectstack-ai/objectstack` derived 89 commands, and all 89 were run. - `check:dual-build-cjs-loads` first exited 3 with PREREQUISITE NOT MET: nine packages outside cli's closure had no `dist`. Those nine were built and the gate re-run: exit 0. The four other `dist` readers (`dts-closure`, `lean-entry-closure`, `published-files`, `sourcemap-no-sources-content`) were re-run on the fuller tree: exit 0. - `check:pm-dispatch-gates` ran detached: exit 0, 1976 cases, 1058.8 s. - Verdict: `✓ dispatch-gates --ran: 89 derived famil(ies) accounted for — 89 run, 0 NOT-MEASURED (a DERIVED zero — all 89 recorded an exit code and none of them is 3)`, with 0 UNRUN. - Nine families that the derivation names as taking a value from the workflow cannot run locally, because their argv or env has no value outside CI. They are the `check-shard-attestation` emit and verify steps, `check-test-completeness` on the two CI logs, `check-issue-citations --census` and `check-required-contexts --verify-required-set`. They are not in the 89, and CI runs them. - Beyond the derived list: - `pnpm --filter @objectstack/cli typecheck`: exit 0; `check:test-typecheck` OK, holding the same 3 files, 28 errors and 6 pinned signatures. - `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: 237 files and 3370 tests passed. - The whole cli suite at the queue tier (both slices above, 303 files) passed on `95b97ef2e`. - `check-test-completeness.mjs --self-test` passed. That script imports the partitioner. - eslint over the four changed lintable files: 4 files, 0 errors, 0 warnings. eslint's own config places all four in its population. The count comes from `--format json`. The config enables no type-aware linting, so this diff cannot change eslint's verdict on any untouched file. **Acceptance 2: the hash moves.** These are `--dry=json` hashes of `cli#test`. Each edit was a one-line marker, planted and then restored. | edit | new leg (`OS_TEST_SHARD=1/2`) | old leg (`--only --force -- --shard=1/2`) | |:--|:--|:--| | clean | `5fe85e3cd60398b1` | `d26a68b9c44aac59` | | `packages/spec/src/ui/view.zod.ts` | `f57477faa9b85239`, moves | unchanged | | `packages/types/src/env.ts` | `b64907aa07f030ad`, moves | unchanged | | `packages/cli/src/utils/one-shot-exit.ts` | `123e600f94cc13c8`, moves | `297d412288a9c65b`, moves | | `packages/client/src/index.ts` (no `$TURBO_ROOT$` input names it) | `32e74945bc85d7e1`, moves | unchanged | | connector-slack (control, outside cli's closure) | unchanged | unchanged | - The new leg plans 62 tasks: 59 HIT and 3 MISS. All 61 of its build hashes equal those of `turbo run build --filter=@objectstack/cli`, so the closure replays. - The second merge moved the new leg's clean hash from `6a3fe12522c74bb9` (on `099e5ff641`) to `5fe85e3cd60398b1`. The old leg stayed at `d26a68b9c44aac59`. **Acceptances 1 and 3: an affected slice executes without `--force`.** The runs used cli slice 1/16 for speed; turbo's replay decision is a hash lookup and does not depend on k/n. `OS_TEST_TIERS=queue` was set throughout. - New leg, unchanged tree: `cache miss, executing 8cc2f107d21814cf`, then `cache hit, replaying logs 8cc2f107d21814cf` in 86 ms. That replay is legitimate, because nothing changed. - Old leg (`--only`, no `--force`), unchanged final tree: `cache hit, replaying logs dc666be1735ca19a`. That cache entry was written on `099e5ff641`, before the second merge brought in upstream changes; the closure rebuild after that merge executed 58 of 59 builds. **This is the defect, reproduced by a real merge.** - Next, an edit was planted in `packages/client/src/index.ts`, followed by CI's closure rebuild (`client#build` and `cli#build` executed; 57 of 59 were cached): - Old leg, no `--force`: `cache hit, replaying logs dc666be1735ca19a` in 118 ms. The slice was not run. - New leg, no `--force`: `cache miss, executing 61f0a4c75daa610c`, 19 files passed. The slice ran. - The same sequence ran on `099e5ff641` with the real partition slice 1/2: - unchanged tree: the slice replays `90ddc44dcc24b458`; - after the client edit: `cache miss, executing 2df7b9311ca50a57`; 152 files; 2069 tests passed and 1 skipped; 10m22.8s. - After each run the edited file matched its HEAD blob and `git diff HEAD` was empty. ### Changeset disposition: `skip-changeset`, applied by the seat - `Check Changeset` on `e00136407` fails at "Require a changeset (or the skip-changeset label)". The PR adds no changeset and has no label. It lacks a disposition; the gate did not misread anything. - `check-empty-changeset.mjs --base origin/main`: exit 0. No declaring changeset was added, and no existing changeset was modified or deleted. - `check-changeset-no-major.mjs --base origin/main`: exit 0, no major bump. - Presence: 0 `.changeset/*.md` files were added against merge base `4b45afaed5`. - No published file changes: - `packages/cli` ships `files: [dist, README.md, CHANGELOG.md]`. Its `tsconfig.build.json` compiles only `src`, so `vitest.config.ts` is test configuration and is never shipped. - In the built cli `dist`, README and CHANGELOG, `OS_TEST_SHARD` appears in 0 files. The positive control, `exitOneShotCommand`, appears in 6 files. There is no `vitest.config*` in `dist`. - The other five paths are CI configuration, the root `turbo.json`, and scripts of the root package, which is private. ## Acceptance notes - The config-level shard is typed through vitest's CLI-options type. If a vitest upgrade stops merging the test block with the CLI options, both slices will run the whole suite: extra cost, not lost coverage. The partitioner battery checks only spelling, so it cannot catch this. - Landing moves 52 test-task hashes once. Any `turbo.json` edit already makes all 80 packages affected on the landing PR. - The Dogfood gate's shard leg has the same `--only` passthrough shape and no `--force`. It is filed separately as #20820. - The unmatched-digest refusal reaches `report-test-timings.mjs` as a stated problem, not as an exit code. That line names the summary file twice, because `readSummaries` prefixes the entry and `samplesFromSummary` also labels the error with it. Every refusal that function relays already has this shape. It is left alone. - The local timings in section 2 are shared-container seconds, about 2.7 times CI's. Read the prediction, not the drift ratio. - `Shard Timings Refresh` went red on `e00136407` (run 36701198568): "candidates existed and none was eligible". - That lane reads main's scheduled CI runs through the GitHub run-listing API, not this diff. Its inputs here (`select-shard-timings-run.mjs`, the workflow, and the reader) are byte-identical to the three earlier heads, where it was green. - Reproduced locally at 10:22Z: the listing served a stale index (70 scheduled runs, the newest from 2026-09-27T14:00Z, every artifact expired), so every candidate was rejected. - The same query at 10:23Z returned the 10:01Z and 09:01Z runs, both ELIGIBLE, and the selector exited 0 in three straight attempts. - The details are in the report on #19278. Rounds 1 and 2 were written by session `session_014EJ1ED8X4MMrT18BhVx4tx`. The evidence above, the second merge and the comment fix are by session `session_01TdiauJaVCHuj45EzZGUxHh`, the `domain:devx` seat 2 dev, on 2026-09-30. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1741c5d commit 3083906

6 files changed

Lines changed: 536 additions & 95 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 82 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -556,14 +556,19 @@ jobs:
556556
echo 'Items on this shard (a package name, or a package plus a k/n file-level slice):'
557557
cat "$RUNNER_TEMP/shard-packages.txt"
558558
559-
# ⛔ A FILE-LEVEL SLICE BUILDS ITS DEPENDENCY CLOSURE HERE, IN A RUN THAT
560-
# CARRIES NO PASSTHROUGH, so that the sharded run in the next step can be
561-
# `--only` (#16395).
562-
#
563-
# Turbo folds a run-level passthrough into the hash of EVERY task in the
564-
# run, not only the task that receives it -- and `-- "--shard=k/n"` is the
565-
# whole reason a slice gets its own invocation at all (the next step's
566-
# comment says why it cannot ride the shared run). Measured on turbo
559+
# ⛔ A FILE-LEVEL SLICE BUILDS ITS DEPENDENCY CLOSURE HERE, IN ITS OWN
560+
# GUARDED STEP, so the slice leg in the next step REPLAYS it. Since #19278
561+
# that leg carries its slice in `OS_TEST_SHARD`, which only the `test`
562+
# task declares, so its build tasks hash exactly as they do here (61 of 61
563+
# identical, measured) and it schedules its own closure: this step is no
564+
# longer what makes the leg correct, only what keeps the closure build off
565+
# the test step's stall-guard site (see the last paragraph below).
566+
#
567+
# Why the step first existed (#16395, when the slice was still a
568+
# passthrough and the leg was `--only`): turbo folds a run-level
569+
# passthrough into the hash of EVERY task in the run, not only the task
570+
# that receives it -- and `-- "--shard=k/n"` was then the whole reason a
571+
# slice got its own invocation at all. Measured on turbo
567572
# 2.10.10, `--filter=@objectstack/cli`, `turbo run test ... --dry=json`
568573
# (60 tasks: 59 `build` + 1 `test`):
569574
#
@@ -677,12 +682,15 @@ jobs:
677682
678683
# Split the shard's ITEMS into the whole packages, which share one
679684
# turbo run as they always have, and the file-level slices, which
680-
# cannot: `--shard=k/n` is passed through to vitest by turbo as a
681-
# RUN-level argument, so it would reach every package in the run —
682-
# and on any package with fewer test files than n that is a hard
683-
# vitest failure (or, with --passWithNoTests, silently no tests at
684-
# all). A slice therefore gets its own invocation, filtered to the one
685-
# package the partitioner sliced.
685+
# cannot: a slice's `k/n` travels as `OS_TEST_SHARD` in the turbo
686+
# run's environment, and a run has ONE environment — set on the
687+
# shared run it would reach every package's `test` task, which
688+
# turbo.json declares it on. (Before #19278 it travelled as a
689+
# `--shard=k/n` passthrough, which reached every package's vitest: a
690+
# hard failure on any package with fewer test files than n, or with
691+
# --passWithNoTests silently no tests at all.) A slice therefore gets
692+
# its own invocation, filtered to the one package the partitioner
693+
# sliced.
686694
FILTERS=""
687695
SLICES=""
688696
while read -r PKG SLICE; do
@@ -727,74 +735,73 @@ jobs:
727735
PKG="${LEG%%=*}"
728736
SLICE="${LEG#*=}"
729737
LOG="$RUNNER_TEMP/test-core-slice-$(printf '%s' "$PKG" | tr -c 'A-Za-z0-9' '-').log"
730-
# `--only` (#16395): the step above already built this slice's
731-
# dependency closure in a passthrough-free run, so this run must
732-
# schedule the ONE task the passthrough is for. Without it turbo
733-
# re-hashes the whole `^build` closure under `--shard=k/n` and
734-
# rebuilds it -- that comment carries the measurement. ⚠ The build
735-
# step is load-bearing for this flag: a sliced package whose build
736-
# never ran fails LOUDLY here (its imports resolve to a missing
737-
# dist), never as a silent green.
738+
# THE SLICE RIDES IN `OS_TEST_SHARD` (#19278), a variable turbo.json
739+
# declares on this package's `test` task and its vitest.config.ts
740+
# reads into vitest's `shard` (vitest 4.1.11 reads no shard
741+
# variable of its own). Not a `-- --shard=k/n` passthrough, and so
742+
# neither `--only` nor `--force`. The partitioner's `--self-test`
743+
# fails a package it can slice whose config or task does not.
744+
#
745+
# Why the two flags existed, and why they are gone:
738746
#
739-
# ⛔ `--force` (#18671) IS NOT REDUNDANT BESIDE `--only` -- do not
740-
# delete it as a no-op. Read the paragraph above backwards: if
741-
# `--only` is what stops turbo re-hashing the `^build` closure,
742-
# then under `--only` this task's hash NO LONGER CARRIES that
743-
# closure, and the one path by which a change in a dependency
744-
# reaches this task is gone with it. What is left is the package's
745-
# own files plus the hand-declared `$TURBO_ROOT$` inputs in
746-
# turbo.json. So a slice that IS in the affected set -- and this
747-
# shard's package set is exactly the affected set, computed two
748-
# steps up -- can match a main-seeded cache entry across the very
749-
# change that put it in that set, and be replayed instead of run.
747+
# `--only` (#16395). Turbo folds a run-level passthrough into the
748+
# hash of EVERY task in the run (turbo 2.10.10, cli's plan, plain
749+
# vs `-- --shard=1/2`: 0 of 62 hashes identical), so the leg
750+
# re-hashed and rebuilt the `^build` closure the step above had
751+
# just built. `--only` scheduled the test alone -- and dropped the
752+
# closure out of the test's HASH with it (#18671), leaving the
753+
# package's own files plus the hand-declared `$TURBO_ROOT$`
754+
# inputs. A slice in the affected set (on a PR, queue or push run
755+
# this shard's set IS the affected set) could then match a
756+
# main-seeded entry across the very change that put it there.
757+
# Run 34746808828 did: it replayed `@objectstack/cli:test` on
758+
# this leg -- `Cached: 1 cached, 1 total`, `73ms >>> FULL
759+
# TURBO` -- out of a log a main push run had produced ~15
760+
# minutes earlier, on a commit that did not contain the PR under
761+
# test. All six shards reported success,
762+
# `check-test-completeness` graded the replay OK, the shard
763+
# attestation said "ran to completion", and the red reached
764+
# `main`. Neither of those two can tell a run from a replay.
750765
#
751-
# That is not a hypothetical. Run 34746808828 replayed
752-
# `@objectstack/cli:test` on this leg -- `Cached: 1 cached, 1
753-
# total`, `73ms >>> FULL TURBO` -- out of a log a main push run
754-
# had produced ~15 minutes earlier, on a commit that did not
755-
# contain the PR under test. All six shards reported success,
756-
# `check-test-completeness` graded the replay OK, and the shard
757-
# attestation said "ran to completion". Neither of those two can
758-
# tell a run from a replay; the red reached `main`.
766+
# `--force` (#18671, PR #19271). Made the leg execute; a constant
767+
# bypass cannot alarm, and the hash stayed blind to the closure.
759768
#
760-
# Measured on this tree (turbo 2.10.10) with a package whose
761-
# `test` task has the same shape (`dependsOn: ["^build"]`), after
762-
# a source change in an upstream package PLUS the closure rebuild
763-
# the step above performs -- i.e. exactly this job's sequence:
769+
# A declared env reaches ONLY the task that declares it: same plan,
770+
# plain vs `OS_TEST_SHARD=1/2` is 61 of 62 identical, the one that
771+
# moves is `cli#test`, and `1/2` vs `2/2` moves that one alone. The
772+
# closure keeps the hashes the step above gave it (61 of 61 equal
773+
# to `turbo run build --filter=$PKG`) and replays, while the test's
774+
# hash carries the closure again. `cli#test`, `--dry=json`:
764775
#
765-
# --only (before) 1 task `1 cached, 1 total` 81ms >>> FULL TURBO <- NOT RUN
766-
# --only --force (here) 1 task `0 cached, 1 total` 1.15s <- runs
767-
# no --only (option) 6 tasks `0 cached, 6 total` 1m54.963s <- runs, and rebuilds the closure
776+
# this leg, OS_TEST_SHARD=1/2 clean 1ee03ac2f26389a6
777+
# + packages/spec/src/ui/view.zod.ts 04eabd0b5a9db364 moves
778+
# + packages/types/src/env.ts 4b15652b8c493471 moves
779+
# + connector-slack/src/* (not in closure) 1ee03ac2f26389a6 still (control)
780+
# old leg, --only --force -- --shard=1/2
781+
# clean / + view.zod.ts d5e7ec263710fe59 still (the defect)
768782
#
769-
# The third row is why the repair is `--force` rather than
770-
# "drop `--only`": dropping it re-executes the closure the step
771-
# above just built, which is #16395's bill paid twice per job.
772-
# That bill re-measured TODAY at cli's real scale, same tree,
773-
# `--dry=json` against a cache the passthrough-free build had
774-
# just filled: the `--only` leg plans 1 task; dropping `--only`
775-
# plans 60 and every one of the 60 is a cache MISS, against 57
776-
# HIT / 3 MISS for the same plan without the passthrough (2 of
777-
# those 3 are `#build` tasks for packages that declare no `build`
778-
# script, so they never execute; the real miss is the test). Cost
779-
# of re-executing that closure here, `--concurrency=2` on a
780-
# SHARED container: 5m11.262s, against 60ms `>>> FULL TURBO` for
781-
# the same command when its hashes are left alone.
783+
# Executed (cli, slice 1/16 for time; `packages/client/src/index.ts`
784+
# edited -- a cli dependency no `$TURBO_ROOT$` input names -- then
785+
# the step above rebuilt the closure, 57 of 59 cached):
782786
#
783-
# `--force` re-executes THIS ONE TASK and nothing else, which is
784-
# why `--only` stays: the two are a pair. Precedent, for the same
785-
# reason in one sentence -- a gate must not be satisfiable by a
786-
# replayed artifact -- is the docs-build gate's `TURBO_FORCE`
787-
# below. Two things fall out of it that are worth keeping:
788-
# `--summarize` now records a REAL duration for the sliced
789-
# package (measure-test-shard-timings.mjs refuses to read one
790-
# from a replay), and the log says `cache bypass, force
791-
# executing <hash>` where it used to say `cache hit, replaying
792-
# logs <hash>`.
787+
# old leg, no --force `cache hit, replaying logs` 69ms FULL TURBO <- NOT RUN
788+
# old leg, --force `cache bypass, force executing` (same hash) <- runs
789+
# this leg `cache miss, executing`, 59 of 60 cached <- runs
793790
#
794-
# ⚠ What `--force` does NOT do is make the hash honest. It stays
795-
# blind to the closure, so nothing in this repo yet fails when a
796-
# slice leg's hash stops moving for an upstream source change.
797-
set -- pnpm turbo run test "--filter=$PKG" --only --force --concurrency=4 --summarize --log-order=stream -- "--shard=$SLICE"
791+
# and on an unchanged tree this leg replays (`60 cached, 60 total`,
792+
# 104ms): with the closure in the hash a HIT means these inputs were
793+
# already tested, which is why no `--force`. Cost: 62 tasks planned,
794+
# 59 HIT / 3 MISS against the cache the step above fills (the test,
795+
# plus two `#build` tasks of packages with no `build` script, which
796+
# never execute), where the passthrough without `--only` was 62 MISS.
797+
# The step above is no longer load-bearing for correctness -- this
798+
# run schedules its own closure -- but it keeps that build on its
799+
# own stall-guard site, and here it replays. `--summarize` records
800+
# the slice only as the sha256 of `OS_TEST_SHARD` (`cliArguments`
801+
# is empty); measure-test-shard-timings.mjs resolves that digest
802+
# against the k/n the partitioner can emit and refuses one it
803+
# cannot, so a slice is never timed as the whole package.
804+
set -- env "OS_TEST_SHARD=$SLICE" pnpm turbo run test "--filter=$PKG" --concurrency=4 --summarize --log-order=stream
798805
fi
799806
LOGS="$LOGS $LOG"
800807
node scripts/run-with-stall-guard.mjs --log "$LOG" --stall-minutes 10 \

‎packages/cli/vitest.config.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@
643643
// `node_modules` exclusion: an exact-path list matches nothing it does not name.
644644
import { defineConfig } from 'vitest/config';
645645
import path from 'path';
646-
import { parseCLI } from 'vitest/node';
646+
import { parseCLI, type TestUserConfig } from 'vitest/node';
647647
import {
648648
runFilterPreflight,
649649
runProjectCliOverridePreflight,
@@ -710,6 +710,35 @@ runProjectCliOverridePreflight({
710710
parse: parseCLI,
711711
});
712712

713+
// #19278 — THE FILE-LEVEL SLICE ARRIVES AS AN ENV VAR, NOT AS A PASSTHROUGH.
714+
// `scripts/partition-test-shards.mjs` slices this package (`FILE_SHARDED_PACKAGES`),
715+
// and Test Core runs each slice as `OS_TEST_SHARD=k/n turbo run test`. The
716+
// value reaches vitest HERE because vitest 4.1.11 reads no shard variable of its
717+
// own (no `VITEST_SHARD`: the variables it reads are enumerable in its dist),
718+
// and it reaches this process at all only because `turbo.json` declares
719+
// `OS_TEST_SHARD` in this package's `test` task `env` — which is also what
720+
// puts the slice in the task hash. Unset (every local run, the whole-package
721+
// leg, the nightly) it is `undefined`, and the run is unsharded as before; a
722+
// `--shard` on the command line still wins, because vitest merges the CLI
723+
// options OVER this block.
724+
//
725+
// Why a passthrough (`-- --shard=k/n`) is no longer the carrier: turbo folds a
726+
// run-level passthrough into the hash of every task in the run, so the slice
727+
// leg had to be `--only`, and `--only` drops the `build` closure out of the
728+
// test's hash — a slice could replay across the very upstream change that put
729+
// it in the affected set. An env declared on the task reaches only the task.
730+
//
731+
// ⚠️ Typed against vitest's CLI-options type (`TestUserConfig`), spread rather
732+
// than written as a literal key: vitest declares `shard` on its CLI options and
733+
// NOT on `InlineConfig`, the type of this `test` block (a literal `shard:` is
734+
// TS2769 here: "'shard' does not exist in type 'InlineConfig'"), yet it
735+
// resolves the two as one object (`deepMerge(configDefaults, test, cliOptions)`)
736+
// — measured on 4.1.11, it honours this key, projects included. The
737+
// partitioner's `--self-test` fails when a sliced package's config stops
738+
// reading the variable — it looks for the read in code position, so the read
739+
// lives at its use site in the `test` block below, not in a helper binding
740+
// that could outlive the spread.
741+
713742
export default defineConfig({
714743
resolve: {
715744
// Array form with an ANCHORED pattern, per the trap the gate documents:
@@ -784,6 +813,9 @@ export default defineConfig({
784813
],
785814
},
786815
test: {
816+
// The file-level slice, when Test Core runs one (#19278) — see the section
817+
// above `export default` for why it is spread and typed this way.
818+
...({ shard: process.env.OS_TEST_SHARD } satisfies Pick<TestUserConfig, 'shard'>),
787819
// A late console.* must not redden a green suite (#10374): vitest's worker
788820
// forwards console output over RPC and discards the promise, and a write
789821
// landing after teardown's rpcDone() snapshot is rejected into an unhandled

0 commit comments

Comments
 (0)