Uh oh!
There was an error while loading. Please reload this page.
fix(ci): make the test-completeness guard see a package that reported nothing - #10205
Merged
os-zhuang merged 3 commits intoAug 20, 2026
Merged
Conversation
… nothing The guard builds its rows by regex over vitest summary lines PRESENT in the log, so a package emitting no summary at all contributes no row: it is neither counted nor missed, it is invisible. Its green therefore means "every package that reported was internally consistent", never "every package on the shard reported" -- and ci.yml's own note read that green as "so these are real test failures", which is how one triage went to a wrong hypothesis and stayed there. Feed the guard the shard's scheduled package list (--scheduled, ci.yml's $RUNNER_TEMP/shard-packages.txt) plus the turbo ls document it was sharded from (--package-list, for each package's directory) and a scheduled-but-silent package becomes a named red. Two measured false-red sources shape the rules rather than a naive scheduled-minus-reported join: - 5 of 77 packages declare no `test` script and 16 more run `vitest run --passWithNoTests`, which prints no summary with no test files, so a package is expected to report only if it has both a test script and at least one test file; - turbo stops scheduling on the first failure, so an ordinary red suite leaves later packages unrun and silent through no fault of their own (measured: one failing task in a 4-task run printed `Tasks: 1 successful, 4 total`). So a missing summary is red only when turbo named the package in its `Failed:` roster (the case this fixes) or when the run completed with every task successful (a suite that went green having reported nothing). A package the run never reached is a note, never red. Behaviour without the new flags is unchanged, so the dogfood job's invocation is untouched. The self-test runs on every invocation rather than from a lint step, which keeps it inside this change's file surface and is the one placement it cannot rot in. Part of #10032 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
…st-completeness-shard-awareness
… its line prefix The first version of this guard read package attribution only from the `<pkg>:test:` line prefix. Measured on its own PR (Test Core (1/3), run 32376757655): `@objectstack/spec` reported `Test Files 415 passed (415)` and `Tests 11045 passed (11045)`, and the guard said it had reported nothing. Why: turbo uses STREAM log order locally, which prefixes every line, but switches to GROUPED order in GitHub Actions, which emits ::group::@objectstack/spec:test <- GitHub renders this ##[group] Test Files 415 passed (415) ::endgroup:: and no per-line prefix at all. Grouped is therefore the ONLY shape CI ever writes, so the prefix-only join attributed nothing there and would have reddened every multi-package shard, not just the single-task one that happened to be affected here. Verified on turbo 2.10.10 in both orders. The group header is turbo's own statement of whose output follows, so it is now the primary attribution, with the line prefix still winning when present. Only a `:test` group attributes: `:build` groups and GitHub's own `Run <script>` step groups lend nothing. A summary that neither spelling can attribute is a backstop for a third log shape, and it refuses to guess: one remaining candidate is unambiguous, more than one is not graded at all and says so out loud. Attributing a stray summary to the wrong package would mark a silent package as having reported, which is worse than the gap. Fixture coverage missed this because every local leg used stream-order logs. The self-test now pins both orders, including the exact three lines from the failing CI job, plus the grouped multi-package shape and the group-leak, build-group and step-group cases. Verified: the #10032 true positive still goes red in BOTH orders while origin/main stays green on both; the real CI log that produced the false positive is green; and behaviour without the flags is byte-identical to origin/main on all five fixtures, so the Dogfood invocation is untouched. Part of #10032 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
os-zhuang
marked this pull request as ready for review
August 20, 2026 14:53
This was referenced Aug 20, 2026
Uh oh!
There was an error while loading. Please reload this page.
os-zhuang
deleted the
claude/issue-10032-test-completeness-shard-awareness
branch
August 20, 2026 15:19
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #10032
check-test-completeness.mjsbuilds its rows by regex over vitest summary lines present in the log. A package that emits no summary at all contributes no row, so it is neither counted nor missed — it is invisible. The guard's green therefore means "every package that reported was internally consistent", never "every package on the shard reported", andci.yml's own note read that green as "so these are real test failures". That inference sent one triage to a wrong hypothesis and kept it there.This feeds the guard the shard's scheduled package set, so a scheduled-but-silent package becomes a named red.
What changed
scripts/check-test-completeness.mjstakes--scheduled(ci.yml's$RUNNER_TEMP/shard-packages.txt) and--package-list(theturbo lsdocument it was sharded from, which resolves each name to a directory). Both or neither; one without the other is a usage error..github/workflows/ci.ymlpasses both in the Test Core guard step, and the step's comment — the load-bearing sentence that documented the false dichotomy — is rewritten to match the guard's new meaning.⛔
scripts/partition-test-shards.mjsis not touched. It is the fenced surface of #10149. This change only reads the file it produces.Test Core (1/3)on487f6228went red on this PR: the guard said@objectstack/spec"reported nothing" while the same log carriedTest Files 415 passed (415)andTests 11045 passed (11045).Root cause, measured on turbo 2.10.10 in both orders. The first version read package attribution only from the
<pkg>:test:line prefix. turbo uses stream log order locally, which prefixes every line — but switches to grouped order under GitHub Actions, which emits a group header and no per-line prefix at all:Grouped is therefore the only shape CI ever writes. The prefix-only join attributed nothing there, so it would have reddened every multi-package shard, not merely the single-task one that happened to be affected — reproduced locally with three packages in grouped order, where all three summaries arrive anonymous.
@objectstack/specdoes not appear only once in that job log.##[group]@objectstack/spec:testis at line 4305 of the ~5000-line log; the "appears exactly once" reading came from a 400-line tail window that starts below the header. That header is precisely what makes the fix possible.The fix. The group header is turbo's own statement of whose output follows, so it is now the primary attribution, with an explicit line prefix still winning when present. Only a
:testgroup attributes —:buildgroups and GitHub's ownRun <script>step groups lend nothing.And it does not guess. A summary neither spelling can attribute is a backstop for a third log shape: one remaining candidate is unambiguous and is accepted; more than one is not graded at all and says so out loud. Attributing a stray summary to the wrong package would mark a silent package as having reported — worse than the gap.
Fixture coverage is extended, since every original leg used stream-order logs. The self-test now pins both orders, including the exact three lines from the failing CI job, the grouped multi-package shape, and the group-leak / build-group / step-group cases.
Two measured false-red sources shape the rules
A naive scheduled-minus-reported join is a false-red machine. Both measured before the rules were written:
shard-packages.txtlists every package on the shard, including ones turbo runs notesttask for. 5 of this repo's 77 packages declare notestscript; 16 more runvitest run --passWithNoTests, which prints no summary when a package owns no test files. So a package is expected to report only if it has both atestscript and at least one test file.Tasks: 1 successful, 4 total, and the two cancelled packages produced no summary. Charging those to the guard would put noise on every ordinary red suite.So a missing summary is red only under one of two rules — turbo named the package in its
Failed:roster, or the run completed with every task successful and a package still reported nothing. A package the run never reached is a note, never red.Verification — five legs, both log shapes, plus the real CI log
⛔ A green guard proves nothing here, since the subject is a guard that was green while blind. So the invisible case is constructed in both log orders, and
origin/mainis run against the same logs as a control.sdui-parser+embedder-openai3 of 3 scheduled package(s) reported3 of 3 … reported1 of 1 … reported; 11045 test(s) declared and all accounted forBefore/after on the real bytes. The failing job's test-step output was extracted from the job log (timestamps stripped, i.e. what
test-core.logholds) and replayed against both versions from inside the repo:The true positive survives.
origin/main's guard on the same two failing logs is green in both orders (OK (1 package(s), 356 test(s) …)) — still blind — while this version is red. The value is intact.Anti-noise. On an aborted run with two no-script packages scheduled, only the genuinely silent failed package is red; cancelled ones are notes and no-script ones are silently exempt.
Backward compatibility. Without the new flags, stdout+stderr and exit code are byte-identical to
origin/mainon all five fixtures, so the Dogfood job's invocation is unaffected.Cache replay, verified
Measured on turbo 2.10.10: a cached leg printed
cache hit, replaying logs 07765db4f3648082followed by both summary lines and>>> FULL TURBO. A>>> FULL TURBOshard therefore still reports and does not go red.turbo.jsonsets nooutputLogssuppression ontest— the premise #10032 stated, re-verified.The self-test runs on every invocation, not from a lint step
The rules are pure functions over strings and now carry ~45 assertions, running at the guard's own startup (~1ms) rather than from a
lint.ymlstep:lint.ymlis outside this change's declared file surface, and this repo has already paid for the alternative —partition-test-shards.mjscarried a--self-testthat nothing ran from the day it was written, which lint.yml itself records as "a pin nobody runs is not a weaker pin, it is no pin".--self-testalso stands alone for local use.Scope
Option 1 of #10032 only. Option 2 is filed as #10203 with the measurements that changed its value — notably that it would have captured nothing in the incident that motivated it. Option 3 is out of scope per the card ("worth doing only if it recurs").
⛔ No root cause is claimed for the original zero-output event. It was never reproduced and is not diagnosed here; this only makes CI able to say that it happened, and to which package.
Gates
mainmerged first (04096f17).node scripts/pm/dispatch-gates.mjswith no path args, derived from the real diff at7f71e56e, named the same 9 families. All green at that commit, each quoting its own verdict line:check:cross-package-test-inputsOK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check:node-versioncheck-node-version: OK (29 setup-node step(s) across 26 workflow(s), all on Node 22).check:required-contextsci.yml:test-gate → 'Test Core'check:shard-attestationcheck-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).check:workflow-status-functionsOK (scanned 26 workflow file(s), 45 job(s), 23 job-level if: expression(s); 10 read needs.*.outputs.*, all naming a status function).check-test-completeness.mjs(the guard itself)check-test-completeness: self-test OKPlus
check:nul-bytes→OK (scanned 6075 text file(s) … no raw ASCII control bytes).@objectstack/spec's own suite (8m16s) was not run locally. It is pulled in only by spec'sscripts/**turbo input glob, and no spec test reads this script — the sole repo reference is a prose comment instrict-object.test.ts. CI runs it regardless.No changeset: the diff is
scripts/plus a workflow, and publishes nothing.skip-changesetapplied.Generated by Claude Code
Generated by Claude Code