Skip to content
Merged
58 changes: 45 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,14 @@ jobs:
- *unpack_release_build

- name: 'Run Workspace Tests'
# GitHub's default for `run:` is `bash -e {0}` — no `pipefail` — so
# `npm … | tee` yields tee's status, the `||` handler never fires and
# every branch below is dead code: a shard with a genuinely failing
# test exits 0 and the release proceeds. Measured on release run
# 33806806226, job 100824085040, which reports
# `shell: /usr/bin/bash -e {0}`. Naming the shell is what makes `$?`
# npm's.
shell: 'bash'
Comment thread
yiliang114 marked this conversation as resolved.
env:
# Match the tunable per-process bound used by the main CI gate.
VITEST_MAX_THREADS: "${{ startsWith(runner.name, 'ecs-qwen-') && (vars.QWEN_CI_VITEST_MAX_WORKERS || '4') || '' }}"
Expand All @@ -558,6 +566,14 @@ jobs:
# release lane alone, and an empty variable just falls back to the
# default.
VITEST_RETRY: "${{ vars.QWEN_RELEASE_VITEST_RETRY || '2' }}"
# The guard below reads this log line by line. Vitest colours its
# output from the mere presence of CI, and a coloured summary sits
# escapes between a label and its value
# (`ESC[2m Tests ESC[22m ESC[1mESC[32m394 passed`), which no
# anchored pattern can match — on real release bytes three of the
# four conditions return 0 and the guard never reaches the fourth.
# ci.yml already sets this on three steps for the same reason.
NO_COLOR: 'true'
Comment thread
yiliang114 marked this conversation as resolved.
run: |-
retry_arg=()
if [ -n "${VITEST_RETRY}" ] && [ "${VITEST_RETRY}" != 'off' ]; then
Expand All @@ -567,29 +583,45 @@ jobs:
# killed by Vitest's own worker RPC timing out reads identically to
# a real break, and this release lost two attempts to that
# (run 33713579913). The status is re-raised untouched either way;
# `-o pipefail` is the step default, so `$?` is npm's.
# `shell: bash` above supplies `-o pipefail`, so `$?` is npm's.
log="${RUNNER_TEMP:-/tmp}/workspace-tests-${{ matrix.shard }}.log"
npm run test:release:workspaces -- --shard=${{ matrix.shard }}/3 --passWithNoTests "${retry_arg[@]}" 2>&1 | tee "${log}" || {
status=$?
if grep -qE '^[[:space:]]*FAIL ' "${log}"; then
: # A failing test names itself; an annotation adds nothing.
elif grep -q 'Timeout calling' "${log}"; then
# Passed only with proof the run reached its end and nothing
# else broke: a normal exit (a signal death is 128+N), a
# passing tally, no failing tally, and no unhandled error that
# is not the transport. Vitest's worker RPC giving up says
# nothing about the product, and --retry cannot cover it —
# retries re-run failing TESTS while an unhandled error fails
# the run outright. It has now cost this release three
# attempts (run 33713579913).
elif grep -q '\[vitest-worker\]: Timeout calling' "${log}"; then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R5-2: The branch entry was narrowed to [vitest-worker]: only, but the pinned vitest also emits [vitest-pool]: (four sites in the pool RPC channel — main timing out on a wedged or killed worker, the OOM-under-contention shape this lane documents) and [vitest-api]: variants of the same transport-death class. A pool-side death now lands in the else branch whose annotation asserts "No FAIL line and no transport timeout in the log" while the log carries one — a factually wrong diagnosis that sends the oncall hunting a mystery crash — and the rescue the old pattern granted is lost. The exit status is re-raised correctly either way (errors>=1 vs timeouts=0 refuses the pass-through), so this is a diagnostics regression and a lost rescue attempt, not an unsound release.

Witness:

HEAD guard vs 'Error: [vitest-pool]: Timeout calling "executeTests"' + tally + ' Errors  1 error':
  GUARD_EXIT=1 "::error title=Workspace tests exited 1 with no failing test::No FAIL line and
  no transport timeout in the log..."   (annotation factually wrong)
BASE-FORCED (old entry grep -q 'Timeout calling'):
  GUARD_EXIT=0 "...Vitest's own worker RPC timed out. Treated as a pass."
shapes in pinned vitest 3.2.7 dist: [vitest-pool] chunks/coverage.DfSpMS-b.js:2602,2735,3063,3183;
[vitest-api] chunks/cli-api.DVe0nWUx.js:5180 (birpc 60s DEFAULT_TIMEOUT)
Suggested change
elif grep -q '\[vitest-worker\]: Timeout calling' "${log}"; then
elif grep -qE '\[vitest-(worker|pool|api)\]: Timeout calling' "${log}"; then

The count grep at line 616 must stay [vitest-worker]:-only — counting pool timeouts into timeouts would let errors==timeouts hold for a run whose worker never completed its assigned files, contradicting the guard's stated premise. Add a row carrying Error: [vitest-pool]: Timeout calling "executeTests" beside the tally and Errors 1 error, stub exit 1, expecting the transport-warning title — it is red today (the ::error unexplained title is emitted) and must go green with the widened entry, red again if reverted.

— qwen3.8-max via Qwen Code /review (v0.23.0)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not applying this one as written — the premise is confirmed but the suggested widening opens a new false-pass route. Measured, not inferred.

Premise checks out on the pinned vitest 3.2.7: grep -ro '\[vitest-\(pool\|api\|worker\)\]' node_modules/vitest/dist/[vitest-pool] 6 sites, [vitest-api] 1, [vitest-worker] 1. And your diagnosis of today's behaviour is right: a pool-side death lands in the else leg, whose annotation asserts "No FAIL line and no transport timeout in the log" while the log carries one.

But running the verbatim-extracted step with only the elif widened to \[vitest-(worker|pool|api)\]: Timeout calling, keeping the count grep [vitest-worker]-only exactly as your comment requires:

log = ' Tests  10614 passed (10614)'
    + 'Error: [vitest-pool]: Timeout calling "executeTests"'
    + 'Error: Failed to load ./vitest.config.ts'    # later workspace, died before any summary
HEAD     EXIT=1  ::error  title=Workspace tests exited 1 with no failing test
WIDENED  EXIT=0  ::warning title=Workspace tests passed through a Vitest transport timeout

Because timeouts never counts pool lines, a log that enters the branch on one and carries no Errors summary gets errors=0 and timeouts=0: the parity check passes vacuously, and with an earlier workspace's passing tally already in the same log, all four legs clear. The release proceeds over a workspace that never ran its tests. That is the R5-1 pre-summary shape, made reachable through a branch entry that today routes it to the ::error leg and re-raises the status — so the widening trades a wrong-words annotation and a lost rescue for a silently green release.

Widening the entry needs a precondition that the branch was earned by something countable — [ "${timeouts}" -gt 0 ] alongside the widened grep, or the summary-coverage leg from R5-1, which closes this case too. Either is part of the detection-strategy decision the two Criticals are held on, so leaving this unresolved rather than landing the one-liner. Happy to take whichever variant the maintainer picks.


Left unresolved on purpose: the measured false-pass above means this cannot be landed as a one-liner, and the precondition it needs is the same detection-strategy decision the two Criticals are held on.

# Vitest's worker RPC giving up says nothing about the product,
# and --retry cannot cover it — retries re-run failing TESTS
# while an unhandled error fails the run outright. It has now
# cost this release three attempts (run 33713579913).
#
# Passed only with proof the run reached its end and that every
# unhandled error WAS the transport. That proof is Vitest's own
# count rather than a reading of the crash: it prints
# `Errors N errors` whenever unhandled errors occurred, so the
# guard compares that count with how many carried the
# transport's own `[vitest-worker]: Timeout calling` message.
# Recognising a crash by its header cannot be made to work — the
# header is producer-chosen (26 of this repo's 293 Error
# subclasses have no Error/Exception suffix, and four assign
# that bare name to err.name), so any pattern over headers is
# incomplete by construction, while the count needs no pattern.
#
# `--workspaces` prints one summary per workspace into one log,
# so both figures are whole-file sums and a passing tally cannot
# cover a later workspace's crash. Reaching this branch means at
# least one transport line, so a log with no `Errors` summary at
# all counts 0 against it and the pass is refused, not granted.
errors=$(awk '/^[[:space:]]*Errors[[:space:]]+[0-9]+ errors?$/ { total += $2 } END { print total + 0 }' "${log}")
Comment thread
yiliang114 marked this conversation as resolved.
timeouts=$(grep -cE '\[vitest-worker\]: Timeout calling' "${log}" || true)
Comment thread
yiliang114 marked this conversation as resolved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-1: [certifies-falsely] [new-surface] Re-asserted, corrected by this round's execution: the timeouts count greps the whole log unanchored, so any line containing the transport substring — a test's console output, or a code frame echoing source that contains it — can restore errors == timeouts parity over a real unhandled error and grant the pass-through. Two routes executed on real vitest 3.2.7 bytes: a passing test printing the shape beside one real transport death (timeouts=2 vs Errors 2 errors → exit 0 "Treated as a pass" while Vitest counted exactly 1 unhandled error), and a run whose only unhandled error is a real Error: write after end whose code frame echoes a source line containing the substring (exit 0; the byte-identical control without the substring exits 1). Correction to the round-4 comment: the claimed annotation mechanism (the auto-added github-actions reporter repeating the transport message) does NOT reproduce on the pinned vitest — that reporter skips errors with no module-graph frame, and probes show annotations=0, the pure-transport rescue works, and transport + real error is correctly refused; the round-4 suggested ^Error: anchor was also executed and does not close the test-print route (a column-0 printed line still matches it). Reachability is latent today — a repo-wide grep finds the substring only in scripts/tests/release-workflow.test.js, which does not run in the release lane — but the guard's own design premise is that test output is untrusted text, and the grant condition reads that text unanchored.

Witness:

probe — real vitest 3.2.7, verbatim-extracted HEAD guard (bash -e -o pipefail), npm stubbed:
P4 test-printed both shapes beside one real transport death:
   timeouts=2 errors=2 annotations=0 -> GUARD_EXIT=0 "...Treated as a pass." (Vitest counted 1)
P6 code-frame echo (only unhandled error: Error: write after end):
   matching line ' 10| // message: [vitest-worker]: Timeout calling "onUnhandledError" with...'
   -> GUARD_EXIT=0; flip (byte-identical run, substring absent from source) -> GUARD_EXIT=1
round-4 mechanism refuted:
   P1 pure transport -> annotations=0, GUARD_EXIT=0 (rescue works)
   P2 transport + real 'write after end' -> Errors 2 errors, annotations=0 -> GUARD_EXIT=1
round-4 suggested '^Error: ' anchor applied in scratch: P4 still exits 0

Key the count on provenance — count only transport messages Vitest itself printed inside its Unhandled Errors section (e.g. an awk state machine that enters on the section banner and counts header-shaped matches within it) rather than any whole-log line carrying the substring; note the ^Error: anchor alone was tested and is insufficient. Four release-lane workspaces set reporters (packages/core/vitest.config.ts:27 reporters: ['default', 'junit'], plus cli, web-shell and acp-bridge) and emit bare section lines with no annotation — the count must keep matching those. Add probe rows for both routes — a test-printed [vitest-worker]: Timeout calling line and the code-frame shape, each beside Error: write after end and Errors 2 errors, expecting stands — they must go red if the provenance keying is removed, and the pure-transport row must stay green.

— qwen3.8-max via Qwen Code /review (v0.23.0)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding for a maintainer design decision rather than another route-by-route patch. R1-1 is fix-induced and re-asserted across rounds 3→5: each fix closes one false-pass-through route and the next round finds another — here the unanchored timeouts grep over the whole log, inflated by Vitest's auto-added github-actions reporter. The robust fix is a design choice (anchor the count precisely / fail closed on any unhandled error regardless of parity / consume Vitest's structured output instead of grepping text), each with different release-safety tradeoffs. See the PR-level note. Leaving unresolved.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this round; leaving unresolved as held.

This thread already carries the author reply holding it for a maintainer design decision, and nothing in this round's commits touches the question it raises. What landed instead is orthogonal and test-side only: pins on shell: 'bash' and NO_COLOR: 'true', a discriminating row for the ^[[:space:]]*/$ anchors on the Errors sum, a row for the count-mismatch refusal, and that refusal's annotation now naming the fifth cause and printing both figures it compared. timeouts=$(grep -cE '\[vitest-worker\]: Timeout calling' …) is byte-identical to what this comment reviewed.

One thing worth recording against the options listed here: the ^Error: anchor is confirmed insufficient (your own execution, P4 still exits 0), and the provenance-keyed state machine you suggest is the same class of change as "consume Vitest's structured output" — a redesign of the guard's detection strategy, not a patch to it. That is the decision being held, and it should be made once rather than per route.


Held unresolved. This round pushed 73ff43f7 (refusal annotation names the fifth cause and prints both compared counts) and dc4e64ca (test-side pins only); neither changes the timeouts detection strategy this Critical is about, so it stays open for the maintainer decision.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-1: [certifies-falsely] [new-surface] Still standing from round 5 — this line is byte-identical to what that round reviewed. The pass-through grant derives timeouts from an unanchored whole-log grep, so log text carrying the transport substring can buy errors == timeouts parity over a real unhandled error and publish a green release behind an annotation asserting the opposite.

The asymmetry sits inside this diff's own stated design intent. The sibling figure one line above is deliberately anchored (^[[:space:]]*Errors…$) precisely so that words a test merely printed cannot move it, and the probe row transport timeout, and a summary-shaped line a test printed exists to pin that. This line has no equivalent protection, so the same class of printed text can move timeouts. The rationale in the comment above argues that header patterns are incomplete by construction and that the count needs no pattern, and it argues the no-Errors-summary case explicitly; it does not answer a grant bought by untrusted text.

Concretely: a shard log carrying a real unhandled Error: write after end plus one line of test console output echoing [vitest-worker]: Timeout calling "fabricated" at column 0, with Errors 2 errors, clears all four legs — status 1 < 128, passing tally present, no failing tally, errors=2 == timeouts=2 — so the step exits 0 and the release publishes over a real break.

Reachability is latent today rather than live, and that is worth stating plainly: an authoritative sweep finds no producer of that literal in the workspace_tests lane, so the trigger needs a future workspace test, a dependency's test output, or a captured-log fixture to print Vitest's internal worker string.

Witness:

probe at dc4e64ca — step lifted verbatim by `qwen review extract-step`, run under
`bash -e -o pipefail` with npm stubbed; every write in a scratch tree, the review
worktree left clean:

f1  (real `Error: write after end` + a fabricated transport line at column 0 + ` Errors 2 errors`)
  PR head:        STATUS=0  "::warning …Every test passed and all 2 unhandled error(s)
                    Vitest counted were its own worker RPC timing out. Treated as a pass."
  BASE (bash -e): STATUS=0, no annotation   <- the guard is dead code at the merge base
  BASE+pipefail:  STATUS=1                  <- the removed header grep REFUSED this log
f1b (fabricated transport line only + a real `TypeError:` + ` Errors 1 error`)
  PR head:        STATUS=0  "…all 1 unhandled error(s)… Treated as a pass."
anchored-header variant of the count: f1 STATUS=1, f1b STATUS=1; 13/15 matrix rows unchanged

reachability sweep — escaped pattern `\[vitest-worker\]: Timeout calling` over all 7857
tracked files plus the three vitest dist chunks that mention it:
  5 matching lines in exactly 3 files — release.yml (the guard),
  node_modules/vitest/dist/chunks/rpc.-pEldfrD.js (the producer), and
  scripts/tests/release-workflow.test.js — which is reached only by `npm run test:scripts`
  in the quality_scripts job, not by workspace_tests, whose command is
  `npm run test:ci --workspaces` (npm's --workspaces excludes the root package)

Anchoring the count on the transport's own header shape closes the demonstrated route. This is a regular code block rather than a one-click suggestion on purpose: the fix spans two lines, and applying it here alone is exactly the one-sided edit that opens a vacuous errors=0 == timeouts=0 grant.

elif grep -qE '^[[:space:]]*Error: \[vitest-worker\]: Timeout calling' "${log}"; then
...
timeouts=$(grep -cE '^[[:space:]]*Error: \[vitest-worker\]: Timeout calling' "${log}" || true)

Measured: that flips exactly the two fabricated rows and nothing else, and over a real vitest 3.2.7 log it counts 1 line where the unanchored message grep counts 3. It does not close R5-1 and it does not close the onUnhandledError payload route — those are separate holes. The full close remains provenance keying (count only transport messages Vitest itself printed inside its Unhandled Errors section, or take the decision from a small reporter's per-error structured output), which is the design decision this thread is held on.

The fix must keep counting the bare section lines emitted by the four workspaces that set reporters non-empty and therefore never get the auto-added github-actions reporter — packages/core/vitest.config.ts:27 reporters: ['default', 'junit'], plus packages/cli/vitest.config.ts:186, packages/web-shell/vitest.config.ts:26 and packages/acp-bridge/vitest.config.ts:44. Round 5 also established by execution that the ^Error: anchor alone does not close the test-print route when the printed line itself starts with Error: , so this is a narrowing rather than a close and should not be presented as one.

Please add two rows to the names which failure this is, and never changes the exit code table in scripts/tests/release-workflow.test.js — a real transport death beside a column-0 [vitest-worker]: Timeout calling "fabricated-by-test-output" line, a real Error: write after end and Errors 2 errors, expecting stands; and the same with a real TypeError: header in place of write after end — then remove the anchor from the count and confirm both rows go red, and that transport timeout, run completed, four transport deaths, four unhandled errors and two workspaces, both lost to the transport still pass through.

— qwen3.8-max via Qwen Code /review (v0.23.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still held for the maintainer design decision named in the PR-level note — no autonomous patch this round. The ^[[:space:]]*Error: \[vitest-worker\]: Timeout calling anchor is measured and would close the fabricated-line route, but this thread's own record says it is a narrowing, not a close: round 5 executed the ^Error: anchor and a test printing Error: [vitest-worker]: Timeout calling … at column 0 still bought errors == timeouts parity (P4 → exit 0). Landing it would leave this finding standing and re-mint it on the sibling route next round — the route-by-route loop the hold exists to stop. The pending decision (anchor precisely / fail closed on any unhandled error regardless of parity / consume Vitest's structured output) is unchanged, and your note that the full close is provenance keying is carried into it. Your two probe rows are the right witnesses and are recorded for whichever option lands. Thread left unresolved on purpose.

中文说明

仍然挂起,等待 PR 级说明中所述的维护者设计决策 —— 本轮不做自动化补丁。^[[:space:]]*Error: \[vitest-worker\]: Timeout calling 这个锚定经过实测,确实能关闭伪造行路线;但本讨论串自己的记录表明它只是收窄而非关闭:第 5 轮实际执行过 ^Error: 锚定,测试在第 0 列打印 Error: [vitest-worker]: Timeout calling … 时仍能买到 errors == timeouts 平价(P4 → exit 0)。落地它只会让本发现继续存在,并在下一轮沿姊妹路线被重申 —— 这正是挂起所要终止的逐路线循环。待决的选项不变(精确锚定计数 / 对任何未处理错误一律 fail closed 而不论计数是否相等 / 消费 Vitest 的结构化输出),你关于完整关闭应按出处键控的注记已纳入该决策。你给出的两个探针行是合适的见证,已为最终选定的方案记录在案。本讨论串刻意保持未解决。

if [ "${status}" -lt 128 ] \
&& grep -qE '^[[:space:]]*Tests[[:space:]]+[0-9]+ passed' "${log}" \
&& ! grep -qE '^[[:space:]]*(Tests|Test Files)[[:space:]]+[0-9]+ failed' "${log}" \
&& ! grep -E '^[[:space:]]*Error:' "${log}" | grep -qv 'Timeout calling'; then
echo "::warning title=Workspace tests passed through a Vitest transport timeout::Every test passed and no other error was reported; Vitest's own worker RPC timed out. Treated as a pass."
&& [ "${errors}" -eq "${timeouts}" ]; then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R5-1: [certifies-falsely] [new-surface] The pass-through fires over a workspace that died before printing any Vitest summary, because none of the four legs can see it. This round settled the disputed premise by execution: npm run test:ci --workspaces --if-present CONTINUES past a failing workspace, so one shard log accumulates every workspace's output. An earlier workspace's transport death contributes the passing tally + Errors 1 error + the transport line; a later workspace's pre-summary crash (a config-load failure, a runner-internal uncaught exception) contributes no Errors count, no FAIL line and no tally: status 1 < 128 ✓, passing tally ✓, no failing tally ✓, errors=1==timeouts=1 ✓ → exit 0 "Treated as a pass", and the release proceeds over a crash whose tests may never have run. The removed header-based leg rejected exactly this shape (the crash prints an Error: header); the count replacement cannot. The diff's rationale ("a passing tally cannot cover a later workspace's crash") argues only the sibling state where the later crash IS counted in Errors — the pre-summary sibling is unargued and false-passes. For completeness: the merge-base production shell (bash -e {0}, no pipefail) was dead code exiting 0 on every shape, and the old logic made executable refused this log — it never ran in production, but it did reject.

Witness:

npm run test:ci --workspaces --if-present (two-workspace scratch repo):
  ws-a failed exit 1 -> ws-b ran and printed WS-B-RAN; NPM_EXIT=1   (npm continues)
verbatim-extracted HEAD guard, driven through the real npm chain:
  ws-a: transport death + ' Tests  10614 passed (10614)' + ' Errors  1 error' (exit 1)
  ws-b: 'Error: Failed to load ./vitest.config.ts' (exit 1, no summary)
  -> GUARD_EXIT=0 "::warning ... Every test passed and all 1 unhandled error(s)
     Vitest counted were its own worker RPC timing out. Treated as a pass."
BASE-FORCED (old logic made executable): exit 1 "the failure stands"

Fail closed when a workspace produced no summary: npm prints a > <pkg> test:ci banner into the same tee'd log for every workspace it runs, so compare banners against summaries, e.g. ran=$(grep -cE '^> .* test:ci$' "${log}" || true) and summaries=$(grep -cE '^[[:space:]]*Test Files[[:space:]]+[0-9]+' "${log}" || true), then add && [ "${ran}" -eq "${summaries}" ] to the pass-through condition (verify the banner shape against a real run before landing). package.json:51"test:release:workspaces": "cross-env NODE_OPTIONS=\"--max-old-space-size=3072\" npm run test:ci --workspaces --if-present -- --coverage.enabled=false"; npm continues past failing workspaces (executed this round), so every workspace's summary lands in one log but a crashed workspace contributes none — a summary-count fix must expect one per workspace defining test:ci, not all workspaces. Add a row to scripts/tests/release-workflow.test.js: transport timeout + passing tally + Errors 1 error + a bare crash line (Error: Failed to load config) with no summary after it, stub exit 1, expecting stands — it must go green with the summary-coverage leg and red again if that leg is removed.

— qwen3.8-max via Qwen Code /review (v0.23.0)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same non-converging class as R1-1: the guard cannot see a workspace that died before printing any Vitest summary, so the pass-through fires over a real pre-summary crash. Closing this route-by-route keeps spawning siblings (R1-1 recurred rounds 3→5). The converging fix is a design decision about the guard's whole detection strategy (anchor / fail-closed-on-any-unhandled-error / structured output), which needs maintainer judgment given the release-safety blast radius. See the PR-level note. Leaving unresolved.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No change this round; leaving unresolved as held.

This round's commits are test-side plus one annotation message, so the pass-through condition this comment is about — including the absence of any summary-coverage leg — is unchanged at the new head. The settled premise (npm run test:ci --workspaces --if-present continues past a failing workspace, so one shard log accumulates every workspace's output and a pre-summary crash contributes no Errors count, no FAIL line and no tally) is not disputed here, and it is precisely why this is held for a design decision rather than patched route-by-route.

Noting one interaction for whoever takes the decision: the summary-coverage leg you propose (ran banners vs summaries, && [ "${ran}" -eq "${summaries}" ]) would also close the new false-pass route that the R5-2 suggestion opens — a log entering the transport branch with no summary at all currently satisfies errors=0 == timeouts=0 vacuously. So the two threads want the same fix, and the banner shape needs verifying against a real run before either lands.


Held unresolved — no summary-coverage leg landed this round (73ff43f7 is annotation text only).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R5-1: [certifies-falsely] [new-surface] Still standing from round 5 — this condition, and the absence of any summary-coverage leg, are unchanged at this head. The pass-through fires over a workspace that died before printing any Vitest summary, because none of the four legs can see it: errors is summed only from Errors N errors lines, so a workspace that contributes no summary contributes 0 to that figure and nothing to any other leg.

npm run test:ci --workspaces --if-present continues past a failing workspace, so one shard log accumulates every workspace's output. An earlier workspace's transport death contributes the passing tally, Errors 1 error and the transport line; a later workspace's pre-summary crash — a config-load failure, a runner-internal uncaught exception — contributes no Errors count, no FAIL line and no tally. All four legs then clear and the step exits 0, so the release proceeds over a crash whose tests may never have run. Unlike R1-1 the trigger here is an ordinary event rather than adversarial content: a workspace whose vitest.config.ts fails to load is routine breakage.

Witness:

round 5's execution, unrefuted and re-confirmed at dc4e64ca:

npm run test:ci --workspaces --if-present (two-workspace scratch repo):
  ws-a failed exit 1 -> ws-b ran and printed WS-B-RAN; NPM_EXIT=1   (npm continues)

verbatim-extracted HEAD guard, driven through the real npm chain:
  ws-a: transport death + ' Tests  10614 passed (10614)' + ' Errors  1 error' (exit 1)
  ws-b: 'Error: Failed to load ./vitest.config.ts' (exit 1, no summary)
  -> GUARD_EXIT=0 "::warning ... Every test passed and all 1 unhandled error(s)
     Vitest counted were its own worker RPC timing out. Treated as a pass."
  BASE-FORCED (old logic made executable): exit 1 "the failure stands"

premise re-established this round from npm's own source: execWorkspaces wraps each
workspace in try/catch, sets process.exitCode, prints the lifecycle error and continues
the loop (npm/lib/commands/run-script.js:49-81), so every workspace's summary lands in
one log while a crashed workspace contributes none

Fail closed when a workspace produced no summary. npm prints a > <pkg> test:ci banner into the same tee'd log for every workspace it runs, so compare banners against summaries. Again a regular code block rather than a one-click suggestion: it adds two lines above and a fifth leg here.

ran=$(grep -cE '^> .* test:ci$' "${log}" || true)
summaries=$(grep -cE '^[[:space:]]*Test Files[[:space:]]+[0-9]+' "${log}" || true)
...
  && [ "${ran}" -eq "${summaries}" ]

Because package.json:51 runs npm run test:ci --workspaces --if-present, a workspace without a test:ci script prints no banner and produces no summary, so this must expect one summary per workspace that DEFINES the script rather than one per workspace, or it will refuse every run; and the existing rows two workspaces, both lost to the transport and four transport deaths, four unhandled errors must still pass through. Verify the banner shape against a real release run before landing it.

Please add a row to scripts/tests/release-workflow.test.js carrying a transport timeout, a passing tally, Errors 1 error and then a bare crash line (Error: Failed to load config) with no summary after it, expecting stands — it is green-by-omission today, and must go red once the leg lands and red again if that leg is removed.

Worth recording against the design decision this thread is held on: this same leg also closes the vacuous errors=0 == timeouts=0 grant that a one-sided widening of R5-2 opens, so the two threads want one fix rather than two.

— qwen3.8-max via Qwen Code /review (v0.23.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still held for the same maintainer design decision — no autonomous patch this round. The pre-summary-crash premise is undisputed (npm continues past a failing workspace, so one shard log accumulates every workspace's output), and the banner-vs-summary leg is the right shape; but the suggestion carries its own precondition — "verify the banner shape against a real release run before landing it" — which this environment cannot satisfy, and an unverified leg on a release gate is what this PR's history argues against. Your note that this leg and the R5-2 widening want one fix rather than two is recorded against the decision, as is the pre-summary-crash probe row. Thread left unresolved on purpose.

中文说明

同样挂起,等待同一个维护者设计决策 —— 本轮不做自动化补丁。pre-summary 崩溃这一前提没有争议(npm 会越过失败的 workspace 继续执行,因此一个分片日志会累积所有 workspace 的输出),banner 对 summary 的计数检查也是正确的形态;但该建议自带先决条件 —— "落地前先用真实 release 运行验证 banner 形态" —— 本环境无法满足这一点,而在 release 门禁上落地未经验证的检查正是本 PR 的历史所反对的。你关于这条检查与 R5-2 的放宽应当合并为一个修复而非两个的注记,以及 pre-summary 崩溃探针行,都已纳入该决策记录在案。本讨论串刻意保持未解决。

echo "::warning title=Workspace tests passed through a Vitest transport timeout::Every test passed and all ${errors} unhandled error(s) Vitest counted were its own worker RPC timing out. Treated as a pass."
exit 0
fi
echo "::warning title=Workspace tests exited ${status} on a Vitest transport timeout::A transport timeout with no passing tally to back it, so the failure stands. Rerun the job."
echo "::warning title=Workspace tests exited ${status} on a Vitest transport timeout::A transport timeout the run cannot account for — no passing tally, a failing tally, a signal death, an unhandled error that was not the transport, or the two counts disagreeing for a reason this log does not show. The failure stands (status ${status}, ${errors} counted error(s) vs ${timeouts} transport line(s)); rerun the job."
else
echo "::error title=Workspace tests exited ${status} with no failing test::No FAIL line and no transport timeout in the log. Look for a Vitest Unhandled Errors section, or a worker killed before it could report."
fi
Expand Down
185 changes: 174 additions & 11 deletions scripts/tests/release-workflow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,20 @@ describe('release workflow', () => {
expect(testStep.env.VITEST_RETRY).toBe(
"${{ vars.QWEN_RELEASE_VITEST_RETRY || '2' }}",
);
// `shell: 'bash'` is this step's only source of `-o pipefail` — the
// workflow has no `defaults:` block — so it is what makes the guard live
// rather than dead code. Drop it and GitHub falls back to `bash -e {0}`,
// where `npm … | tee` reports tee's status 0, the `||` handler never
// fires, and a shard with genuinely failing tests exits 0 into the
// release. The probe harness below passes `-o pipefail` itself, so no
// behavioural row would notice the line being cleaned up as redundant.
expect(testStep.shell).toBe('bash');
// Vitest colours its summaries from the mere presence of CI, and a
// coloured summary sits escape bytes between a label and its value, so
// every anchored pattern in the guard matches nothing: the pass-through
// is never granted again and each transport timeout reddens the release.
// Quoted in YAML, so it parses to the string rather than a boolean.
expect(testStep.env.NO_COLOR).toBe('true');

const workspacePackages = getTestCiWorkspaces();

Expand Down Expand Up @@ -643,44 +657,193 @@ describe('release workflow', () => {
// A shard that died on Vitest's own worker RPC timing out reads
// identically to a real break, and this release lost two attempts before
// anyone could tell them apart (run 33713579913). The annotation says
// which; the child's status is re-raised untouched either way, so no
// reading of the log can turn a failure green.
// which; the child's status is re-raised untouched, so no reading of the
// log can turn a failure green except the one deliberate pass-through.
//
// That pass-through is granted by Vitest's own count of unhandled errors
// (`Errors N errors`) matching how many carried the transport's message
// — not by recognising a crash from its header. The header is
// producer-chosen, so a pattern over headers is incomplete by
// construction; the rows below carry the shapes that defeat one
// (suffix-less class, no class at all) and the shapes that defeat the
// count (an extra error the timeouts do not account for, a summary the
// run never reached, words a test merely printed).
const testStep = releaseYaml.jobs.workspace_tests.steps.find(
(step) => step.name === 'Run Workspace Tests',
);
const script = testStep.run.replaceAll('${{ matrix.shard }}', '1');
const timeout = 'Error: [vitest-worker]: Timeout calling "x"';
const tally = ' Tests 10614 passed (10614)';
const passedThrough =
'::warning title=Workspace tests passed through a Vitest transport timeout::';
const stands =
'::warning title=Workspace tests exited 1 on a Vitest transport timeout::';

for (const [label, stub, code, annotation, expected] of [
// A failing test names itself; an annotation would only add noise.
['failing test', ' FAIL src/a.test.ts > boom', 1, null],
// Vitest's worker RPC giving up says nothing about the product, and
// --retry cannot cover it. Passed only with proof the run reached its
// end: a normal exit, a passing tally, no failing tally, and no other
// unhandled error. It cost this release three attempts before that.
// --retry cannot cover it — retries re-run failing TESTS while an
// unhandled error fails the run outright. Passed only with proof the
// run reached its end and that the transport accounts for every
// unhandled error Vitest counted. It cost this release three attempts.
[
'transport timeout, run completed',
'Error: [vitest-worker]: Timeout calling "x"\n Tests 10614 passed (10614)',
`${timeout}\n${tally}\n Errors 1 error`,
1,
'::warning title=Workspace tests passed through a Vitest transport timeout::',
passedThrough,
0,
],
[
'transport timeout, killed by a signal',
'Error: [vitest-worker]: Timeout calling "x"\n Tests 10614 passed (10614)',
`${timeout}\n${tally}\n Errors 1 error`,
137,
'::warning title=Workspace tests exited 137 on a Vitest transport timeout::',
],
// One more error than the transport accounts for: the run broke on
// something else as well, whatever its header said.
[
'transport timeout beside a real one',
'Error: [vitest-worker]: Timeout calling "x"\nError: write after end\n Tests 10614 passed (10614)',
`${timeout}\nError: write after end\n${tally}\n Errors 2 errors`,
1,
stands,
],
// The counts can also disagree with no crash in the log at all — the
// same transport message on two lines inflates `timeouts` past what
// Vitest counted. That is a fifth way to reach this refusal, so the
// annotation names it and prints both figures it compared; without
// them the oncall is told one of four things happened when none did.
[
'transport timeout counted twice against one unhandled error',
`${timeout}\n${timeout}\n${tally}\n Errors 1 error`,
1,
'::warning title=Workspace tests exited 1 on a Vitest transport timeout::A transport timeout the run cannot account for — no passing tally, a failing tally, a signal death, an unhandled error that was not the transport, or the two counts disagreeing for a reason this log does not show. The failure stands (status 1, 1 counted error(s) vs 2 transport line(s)); rerun the job.',
],
// The two shapes no header pattern reaches. A class whose name carries
// no Error/Exception suffix is not hypothetical — 26 of this repo's 293
// Error subclasses are named that way, four of them assigning the bare
// name to err.name — and a bare string throw prints under Vitest's own
// `Unknown Error:` heading, which a header matcher misses on the space.
// The count sees both, because it never looks at the header.
[
'transport timeout beside a suffix-less crash header',
`${timeout}\nPoolTimeout: worker pool exhausted\n${tally}\n Errors 2 errors`,
1,
stands,
],
[
'transport timeout beside a bare string throw',
`${timeout}\nUnknown Error: a bare string, no class header\n${tally}\n Errors 2 errors`,
1,
stands,
],
// Ordinary `Error:` lines are test output, not evidence of a break: the
// log of the run this guard was written for carries three of them as
// fixture data. A matcher over headers refuses the pass-through on
// those and reddens a release the guard exists to save; the count is
// unmoved by them.
[
'transport timeout beside Error: lines a test printed',
`${timeout}\nError: boom\nError: Not implemented: navigation\n${tally}\n Errors 1 error`,
1,
passedThrough,
0,
],
// `--workspaces` prints one summary per workspace into one log, so both
// figures are whole-file sums: a passing tally cannot cover a later
// workspace's crash, and two transport deaths in two workspaces still
// pass.
[
'tally, then a later workspace crashing',
`${timeout}\n${tally}\n Tests 8 passed (8)\n Errors 2 errors`,
1,
stands,
],
// The shape of the log this guard was written for: several transport
// deaths in one run, and the plural summary Vitest prints for more than
// one of them (run 33713579913).
[
'four transport deaths, four unhandled errors',
`${timeout}\n${timeout}\n${timeout}\n${timeout}\n${tally}\n Errors 4 errors`,
1,
passedThrough,
0,
],
[
'two workspaces, both lost to the transport',
`${timeout}\n Tests 5 passed (5)\n Errors 1 error\nError: [vitest-worker]: Timeout calling "y"\n Tests 7 passed (7)\n Errors 1 error`,
1,
'::warning title=Workspace tests exited 1 on a Vitest transport timeout::',
passedThrough,
0,
],
// Absent evidence refuses the pass rather than granting it: no summary
// line at all, no passing tally, or a failing tally.
[
'transport timeout, no error summary to count',
`${timeout}\n${tally}`,
1,
stands,
],
[
'transport timeout, no tally to back it',
'Error: [vitest-worker]: Timeout calling "onTaskUpdate"',
1,
'::warning title=Workspace tests exited 1 on a Vitest transport timeout::',
stands,
],
// Vitest's own summary is the only thing that grants the pass, so a
// run that never printed a tally does not get one even when its error
// count is all transport.
[
'error summary with no tally to back it',
`${timeout}\n Errors 1 error`,
1,
stands,
],
// `Timeout calling` in a test's own output is not the transport dying:
// the branch is entered on Vitest's own `[vitest-worker]:` message, so
// a log carrying only the words is unexplained, not passed through.
[
'Timeout calling printed by a test',
`Timeout calling the vendor API\n${tally}`,
1,
'::error title=Workspace tests exited 1 with no failing test::',
],
// ...and the count is anchored on the same message, so those words
// beside a real transport death do not inflate it into a mismatch.
[
'transport timeout, and Timeout calling printed by a test',
`${timeout}\nTimeout calling the vendor API\n${tally}\n Errors 1 error`,
1,
passedThrough,
0,
],
// ...and the summary sum is anchored on the section-line shape for the
// same reason. Vitest echoes a test's console output at column 0, so a
// workspace printing summary-shaped fixture data lands there; an
// unanchored `Errors N errors` match would add it to the sum, inflate
// `errors` past `timeouts`, and refuse a pass-through every test
// earned — the false-red this PR exists to remove, back again.
[
'transport timeout, and a summary-shaped line a test printed',
`${timeout}\nErrors 2 errors occurred in fixture data\n${tally}\n Errors 1 error`,
1,
passedThrough,
0,
],
[
'transport timeout, failing tally',
`${timeout}\n Tests 3 failed | 10611 passed (10614)\n Errors 1 error`,
1,
stands,
],
// One workspace can print a passing tally while a later one fails
// without ever emitting a FAIL line, so the failing tally is checked
// across the whole log rather than trusted to the branch above.
[
'passing tally in one workspace, failing tally in another',
`${timeout}\n Tests 10 passed (10)\n Test Files 1 failed (3)\n Errors 1 error`,
1,
stands,
],
[
'unexplained',
Expand Down
Loading