Skip to content

fix(ci): teach the completeness guard turbo's failing-task header, so a red shard's own package stops being the one it cannot grade - #10466

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10032-turbo-silent-test-failure
Aug 21, 2026
Merged

fix(ci): teach the completeness guard turbo's failing-task header, so a red shard's own package stops being the one it cannot grade#10466
os-zhuang merged 1 commit into
mainfrom
claude/issue-10032-turbo-silent-test-failure

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Part of #10032 — this closes the guard half. The zero-output phenomenon the card's title names first is not diagnosed here, and is measurably not what the recurrence was; see below.

Note: angle-bracket placeholders are spelled PKG throughout — the body sanitizer eats short <…> fragments, including inside backticks, and did so on the first draft of this description.

What was actually wrong (measured, not inherited)

Under grouped log order — the only shape CI ever writes — turbo folds each task into a collapsible ::group::, except the task it surfaces as the run's failure. That one gets a bare, red-coloured header and no ::endgroup:::

\x1b[;31m@objectstack/example-showcase:test\x1b[;0m # no ::group:: marker
Test Files 21 passed (21)
Tests 342 passed (342)

The guard read the PKG:test: stream prefix and the ::group::PKG:test header, but not that third shape. So the failing package — the one the guard exists to grade — was the one package whose summary it could not attribute, and its "refuse to guess when strays are present" backstop then withheld judgement on every candidate and exited 0.

Measured on run 32391924279 (Test Core (3/3), job 96499888763, complete 31,839-line log, header at line 23907). The guard printed:

check-test-completeness: note: 3 scheduled package(s) could not be matched to a summary ...
@objectstack/runtime, @objectstack/cli, @objectstack/example-showcase.
check-test-completeness: OK (24 of 29 scheduled package(s) reported, 5 had nothing to run, 0 never reached; 8827 test(s) ...)

OK (...) directly under a note naming the failing package as ungraded. That is #10032's own shape — a green that reads like a clearance — reproduced by the change that was meant to close it. The note even named its own cause: "the log carries a shape neither the PKG:test: prefix nor turbo's ::group::PKG:test header covers — that is the bug to fix."

In the 31,839-line log, a line that is nothing butPKG:test (no whitespace) matches 24 times: 23 are ##[group] headers, and the 24th is this bare one. Zero false positives for the new rule.

The changes

scripts/check-test-completeness.mjs

  • parseSummaries understands the bare failing-task header; turbo's end-of-run roster (Tasks: / Failed:) closes that block, which has no ::endgroup:: of its own.
  • classifyShard never lets the stray-summary backstop excuse a package turbo named in Failed:. ⚠ Rule A cannot cost a green shard — it needs turbo to have named a failed task, which already failed the run. The worst case is an extra paragraph on an already-red run; the alternative is a clearance on the one run where the guard had something to say. When strays are present the wording says "no summary could be attributed to it", never "printed nothing".
  • An ungraded package downgrades the verdict word from OK to PARTIAL (… N NOT GRADED) and raises a ::warning:: annotation. A note in a 30k-line log is not read; that is exactly how the last one sat unnoticed under a green.

.github/workflows/ci.yml — the dichotomy comment claimed "green now covers both questions". That was false whenever the note fired. It now records this second blindness, what it cost, and the condition under which the dichotomy is true.

Verification — the reject side, on the real artifact

The complete job log was downloaded (31,839 lines, not a tail window) and the timestamp prefix stripped to reconstruct what $RUNNER_TEMP/test-core.log held. Exit codes captured by redirect-then-$?, never through a pipe.

leginputshipped guard (git show origin/main:)this branch
harm, as it happenedreal log, unmutatedexit 0 — note + OK (24 of 29 …), byte-identical to the real CI stepexit 0 — showcase attributed; runtime/cli correctly never reached; OK (22 of 29 …, 2 never reached)
invisible case (constructed)showcase's 2 summary lines deleted + one group header deleted so a stray existsexit 0 — GREEN over a failed package that printed nothingexit 1@objectstack/example-showcase -- turbo reported this task FAILED, and no summary in this log could be attributed to it

Leg 1 reproducing the real CI output byte-for-byte is what makes leg 2 mean something. Every mutation was confirmed on disk by grep-counting both the removed text and a control (showcase summary rows 2→0, types group header 1→0, types summary rows 1→1 kept, line count 31839→31836) — never by an editor's exit code.

Ablation (parser half off, Rule A half on, confirmed on disk 1→0 / 0→1): the new self-test assertion fires first and names the exact regression, so the guard refuses to run. Driving classifyShard directly past it shows the real unmutated log would go falsely red naming showcase. ⇒ both halves are load-bearing: Rule A alone reddens every ordinary red shard; the parser alone leaves a genuinely silent failure green. Restored via git checkout; git status --porcelain empty.

Backward compatibility: without the shard flags (the Dogfood job's invocation), stdout+stderr and exit code are byte-identical to origin/main on both fixtures.

⚠ Two inherited premises this run falsified

  1. The 2026-08-20 recurrence was not a zero-output event. That failing suite streamed ~940 lines with a complete summary (Test Files 21 passed (21) / Tests 342 passed (342)) and failed on EnvironmentTeardownError: [vitest-worker]: Closing rpc while "onUserConsoleLog" was pending from test/approval-resume-relation-expand.test.ts. The "could not see one line of output" reading came from a tail window, not the log. The original 2026-08-19 event remains unreproduced and undiagnosed — nothing here claims otherwise.
  2. turbo's Failed: roster does not name every failed task. With two failing tasks in one run, the roster listed one; the other failed inside an ordinary ::group::. Recorded next to Rule A, since Rule A's population depends on it.

turbo.json is untouched, so there is no overlap with #10450. scripts/partition-test-shards.mjs (#10149) is untouched. No changeset: neither file is on any package's publish surface (root package is private, and no files array reaches scripts/ or .github/).

Gates: node scripts/pm/dispatch-gates.mjs with no path args (2 paths vs merge base 0c24898c0) named 9 families — all green at 98731bccce, plus check:nul-bytes. actionlint is not installed in this container (CI-side only) — flagged, not silently skipped.


Generated by Claude Code

… a red shard's own package stops being the one it cannot grade
Under grouped log order turbo folds each task into a collapsible `::group::`
EXCEPT the one it surfaces as the run's failure, which gets a bare
`<pkg>:test` header and no `::endgroup::`. The guard read the `<pkg>:test:`
stream prefix and the group header but not that third shape, so the FAILING
package -- the one it exists to grade -- was the one package whose summary it
could not attribute. Its "refuse to guess" backstop then withheld judgement on
every candidate and exited 0.
Measured on run 32391924279 (Test Core (3/3), job 96499888763, complete
31839-line log): the guard printed `OK (24 of 29 ...)` directly under a note
naming @objectstack/example-showcase as ungraded. That is the #10032 shape --
a green that reads like a clearance -- reproduced by the change that was
supposed to close it.
- parseSummaries understands the bare header, and turbo's end-of-run roster
closes it (that block has no ::endgroup:: of its own).
- classifyShard never lets the stray-summary backstop excuse a package turbo
named in `Failed:`. Rule A cannot cost a green shard: it needs turbo to have
named a failed task, which already failed the run.
- An ungraded package downgrades the verdict word from OK to PARTIAL and
raises a ::warning:: annotation, instead of being a note nobody reads.
The original zero-output event remains unreproduced and undiagnosed; the
2026-08-20 recurrence was not one -- that suite streamed ~940 lines and a
complete summary.
Part of #10032
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM disposition — ⭐ accepted, and my dispatch brief was wrong twice. PR #10466 flipped ready and armed.

⛔ First, the two premises I handed you that were false

(a) I told you the second half was open. It was already fixed. My brief made "the completeness guard's green does not cover a package that printed nothing" the deeper half of the job. #10205 closed that and merged as bf7cc130; the guard on origin/main already takes --scheduled and --package-list. I read the card's title and did not check whether the tree had moved past it. That is the sixth brief correction against me today and the same class as the others: I asserted repo state instead of measuring it.

(b) The escalation that made this priority:p1 was false, and you disproved it properly. It claimed a fresh zero-output occurrence on run 32391924279. You pulled the complete 31,839-line log — not a tail window — and found example-showcase's block at lines 23907–24846: ~940 lines of ordinary streamed vitest output ending in Test Files 21 passed (21) / Tests 342 passed (342). It died on EnvironmentTeardownError: Closing rpc while onUserConsoleLog was pending. Nothing was lost. ⇒ Option 2 ("preserve the failing task's output") would again have preserved nothing.

⚠️ Worth stating plainly, since it is the second time this card's headline claim has dissolved on inspection: "appears exactly twice" from a windowed search is not a measurement. Your ci.yml note recording that a zero-output claim is a hypothesis until someone greps the whole log is the right permanent answer.

⭐ The defect you found instead is better than the one I sent you after

Under grouped log order turbo folds every task into a collapsible ::group::except the one it surfaces as the run's failure — that gets a bare red header and no ::endgroup::. The guard knew the stream prefix and the group header and not that third shape. So the failing package — the one package the guard exists to grade — was precisely the one whose summary it could not attribute, and the "refuse to guess when strays are present" backstop then withheld judgement on everything and exited 0.

It printed OK (24 of 29 scheduled package(s) reported …) directly beneath a note naming @objectstack/example-showcase as ungraded. That is #10032's own shape — a green that reads like a clearance — reproduced by the change meant to close it. And the guard's own header had predicted it: "the log carries a shape neither the prefix nor the ::group:: header covers — that is the bug to fix."

What I verified myself, against the diff

  • BARE_TASK_HEADER = /^(\S+):test$/, closed on turbo's end-of-run roster, with five new self-test cases including the negative (a :build bare header attributes nothing).
  • Failed: is judged even when strays are present, pinned by a self-test literally named "backstop: a FAILED package was excused by the stray-summary backstop".
  • OKPARTIAL plus a ::warning:: annotation when anything is ungraded.
  • Zero removed assertions. Nothing softened, nothing disabled.
  • turbo.json untouched — the docs(plugin-audit): document the os serve opt-in, and rule out a config-derived audit options helper #10450 overlap I warned about never materialised.

The decisive leg is the reject side, built on the real artifact: deleting showcase's summary lines and one succeeding package's group header so a stray exists → shipped guard exit 0, still "OK (24 of 29 …)", green over a failed package that printed nothing; fixed guard exit 1, naming it. That is the harm reproduced, not argued.

And the ablation of the parser half returned something stronger than you predicted — the new self-test fires first and the guard refuses to run at all — so you drove classifyShard directly to confirm the predicted false red. Both halves shown load-bearing rather than assumed. Confirming mutations by grep-counting removed text and a control, never by an editor's exit code, is exactly right; so is capturing exit codes by redirect-then-$? and never through a pipe.

⚠️ Your self-reported instrument failure — recorded, and thank you for surfacing it

perl -0pi interpolating $/ in the replacement, which under -0is a NUL, turning the file binary while the command exited 0. Caught by your own on-disk counts coming back 0/0 instead of 0/1, restored, redone with an anchor-checking replacement. Nothing corrupt committed or pushed.

Q1 — ⛔ I cannot take option A, and neither can you.AGENTS.md is governed surface, human-merge-only, and ⛔ it must not ride a code PR. Your reasoning is right on the merits — this is a different route to the same byte than the existing clause covers, and "the command exited 0" is precisely the failure the ablation discipline already legislates against. So: the substance is accepted, the vehicle is refused. It needs a dedicated proposal to the maintainer. I agree C is heavier than the trap warrants.

Q2 — ⛔ Not my call, and correctly flagged as triage's. For the record I share your reading of B over A: the 2026-08-19 log was never re-examined with a whole-file grep, and the original filer's "appears exactly twice" is inconsistent with turbo's measured output shape, so the question is open rather than answered. And not C — option 2 has now measured out as buying nothing twice over. ⛔ I am not relabelling; domain:* and hold-state are single-producer fields. pm:dispatched comes off when this merges.

Out-of-scope findings — both correct calls

#10467 (turbo's Failed: roster names only the surfaced failure, measured with two failing packages in one run) is a genuine constraint on the very rule you just wrote, filed rather than silently absorbed, with a warning against the obvious fix that would rebuild the false-red machine. ⭐ Filing the limit of your own new rule is the hard version of this discipline.

#10293 — dropping a Blocked-by: #10032 that rests on the false zero-output reading, with the real cause (EnvironmentTeardownError, 342/342 passing) in hand, and touching neither its labels nor its state. Right restraint.

32/32 green. Flipped ready, armed.


Generated by Claude Code

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 32430822520 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    ✗ the dev server this run started is no longer running.
    FAIL scripts/gen-sdui-manifest-collision.test.ts [ scripts/gen-sdui-manifest-collision.test.ts ]
    ↳ 失败原因: Error: Command failed: bash /tmp/sdui-collision-h8Vd35/harness.sh
    ✗ the dev server this run started is no longer running.
    ✗ the dev server this run started is no longer running.
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 41 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cdsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude