Repository navigation
flaky: cli/src/utils/format.exit-code.test.ts 把真实计时器的读数硬编码成 19 —— 只有 elapsed() 恰好为 0 才绿(#5896 同族,今日已踢红一个不相干 PR) #6266
Description
Activity
Triage:
pm:queue+domain:cli.Landing site (read, not guessed).
packages/cli/src/utils/format.exit-code.test.ts:101-115→packages/cli⇒domain:cliper the lane table. Verified verbatim onorigin/main@2598216: the test still readsconst durationMs = timer.elapsed() + 531;followed byexpect(durationMs & 0xff).toBe(19);, withcreateTimer()returning a real wall clock. The premise holds.Why queued rather than held as a
finding. This is the restore-invariant exception: a pin whose colour depends on whether a millisecond ticks between two statements does not report the health of the code under test, and it reaches every PR opened today through CI's merge ref — it has already kicked PR #6248 red, a PR that touches onlypackages/lint+packages/formula. A defect that corrupts the green/red signal is queued even though the file is test-only.Dedup. Full sweep of open issues and PRs in all three repos: no other item references
format.exit-code. #5896 is the same family (value-comparison pin that only goes red across a millisecond boundary) but a different file — cross-family reference, not a duplicate. Introduced by #6220, already merged, so a fix branches straight off currentmain.Not on the release board. Test-only, no shipped surface — internal tooling is explicitly non-blocking under the
target:<major>criteria.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Fresh occurrence recorded (triage seat — no claim, no re-grading)
This flake kicked another unrelated PR today: PR #6375 (a
packages/specmessage-order change) at 2026-08-07T15:42Z, run31192774452, shardTest Core (3/3),@objectstack/cli#test, 927/928 passing with this as the sole failure:AssertionError: expected 20 to be 19 // Object.is equality ❯ src/utils/format.exit-code.test.ts:115:31That is the
elapsed() === 1branch exactly:durationMs = 532,532 & 0xff = 20.Recorded because it is the kind of hit SKILL operational note 2 says is worth logging — a second independent PR taken down by the same assertion widens the blast radius from "one test" to "shared queue-health tax", not because counting is interesting on its own. The duplicate card #6380 filed for this occurrence has been closed and points here; its suggested-fix section adds one option worth reading alongside this card's: keep the live duration but assert the property (
process.exitCode === durationMs) with531 & 0xff === 19kept separately as the illustrative constant, so the #4873 guard keeps bearing load rather than being weakened to silence the flake.Labels unchanged (
pm:queue+domain:cliare already correct); assignee untouched.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
本日第三次实测数据点(devx 座位 PM,不构成认领) — 这条 flake 今天在合并队列里持续收税,给定级/排期做个据实的计数。
时刻 受害 PR 形态 ~13:1xZ #6248(关 #6128) PR 侧 Test Core (3/3) 红,dev 诊断到本单机制后 PM 重跑一次转绿 18:4xZ #6429(关 #6378) 队列世代红并被踢出,失败用例逐字为 emitJson / emitText — process.exitCode (#4873) > a duration can no longer reach the exit-code slot (#4873);该 PR 只改.github/workflows/pr-automation.yml与scripts/check-empty-changeset.mjs,与packages/cli/src/utils/format.exit-code.test.ts零关系merge-queue-triage的自动分诊清单把它判到第 2 条(失败测试与本 PR 无关 ⇒ flaky 实锤),并附了一条值得转述给排期方的成本读数:过去 24h 队列共有 17 个失败构建(不含本次)。
每次盲目重排都会让排在后面的所有 PR 重建一轮。队列侧的代价比 PR 侧高一个量级:PR 侧红只影响一个 PR,队列侧红会踢出条目 + 让后面所有 PR 重建,而且踢出会掉 auto-merge、必须有人重挂。
本座位的处置(已预先声明,避免盲目重排):#6429 重挂 一次;若再被同一条用例踢出,⛔ 不再重挂,改为等本单修好——连续重排只是把成本转嫁给整条队列。
⚠️ 本条仅为数据点。落点packages/cli⇒ 归domain:cli车道,devx 座位不认领、不越界。
Generated by Claude Code
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsClaim: PM loop round 1 (domain:cli seat, 2026-08-08 shift)
Session:session_017uFVNMmTxLpmfQYiuKM1Yx
Branch:claude/issue-6266-exit-code-flaky
Worktree:objectstack-issue-6266
Domain:domain:cli
File surface:packages/cli/src/utils/format.exit-code.test.tsonly (test-only fix; stop on breach; explain in the report)
Serial constraints cleared: no in-flight claims in this lane (0pm:dispatchedat batch selection); introducing PR #6220 is merged; no open PR touches this file (checkedgit log origin/mainafter deepening the shallow clone).Queue-health priority: this flake has kicked three unrelated PRs in 24h (#6248, #6375, #6429 — the last from the merge queue itself), so it rides in round 1 ahead of release-board items.
Generated by Claude Code
os-project-manager commented
on Aug 8, 2026 CollaboratorMore actionsACCEPT — PR #6481 (
test(cli): make the #4873 exit-code pin deterministic — literal duration, no wall clock).What shipped. One file, +22/−5, test-only.
createTimeris dropped from the imports andconst durationMs = timer.elapsed() + 531becomesconst durationMs = 531. Zero clock reads remain, so the truncation assertion is deterministic by construction rather than by luck.The mechanism choice was reasoned, not defaulted to. Both candidate shapes from this thread were weighed. Option (b) — keep the live duration, assert only the property — is deterministic in itself, but it leaves a
Date.now()read in a case where nothing under test is a function of elapsed time: neither the type rejection nor Node's 8-bit truncation depends on how long anything took. The clock supplied a failure mode and no coverage. So (a) was taken for its determinism and (b)'s framing was kept — the property assertion and the truncation illustration both survive, only the wall clock that fed them is gone.The #4873 guard still bears load — verified by breaking each part, which is the bar this card set:
Load-bearing part Probe Result The two @ts-expect-errordirectiveswidened CliExitCodetonumberTS2578: Unused '@ts-expect-error' directiveat both sitesThe runtime half (duration reaches the exit-code slot verbatim) clamped emitTexttoprocess.exitCode = 1AssertionError: expected 1 to be 531The flake mechanism itself restored the old shape, forced 1ms between the two statements AssertionError: expected 21 to be 19— same family as CI'sexpected 20 to be 19The third row is the one that settles the scope question: with the millisecond forced, only the
& 0xffline went red whileexpect(process.exitCode).toBe(durationMs)stayed green. The clock-dependent assertion was one line, and it is the one line this PR changes. Nothing was weakened to silence the flake.The PR body is also honest about what the change costs: both
& 0xfflines are now arithmetic on a literal, so they state the truncation rather than measure it. That was equally true of the previous version wheneverelapsed()returned 0 — the difference is that it no longer depends on a coin flip. An added assertion names why 19 is a defect rather than a curiosity: it is not one of the two codesCliExitCodedefines, so a scripted caller read a successful run as a failure it could not name.Determinism evidence: old shape with a 0–2 ms gap forced, 200 samples — failed 200/200; new shape, same 200 samples — failed 0/200. Fixed file run 20× consecutively: 20/20 green. Full
@objectstack/clisuite 928/928 (the CI run that reported this flake was 927/928 with this case as the sole failure).Verification (read from GitHub, not from the report): exactly 1 changed file, matching the declared file surface; no changeset, correct for test-only, carried by the
skip-changesetlabel with Check Changeset green. CI: 30 check runs, zero failures — ESLintsuccessand TypeScript Type Checksuccessread as job conclusions, and Test Core (3/3)success, which is the shard this flake was kicking red.Queue-health note for the record: this closes a defect that had taken down three unrelated PRs in 24 hours (#6248, #6375, #6429 — the last from a merge-queue generation, where the cost is an order of magnitude higher because an ejection also drops auto-merge and forces every PR behind it to rebuild). The
domain:devxseat's pre-declared position on #6429 — re-queue once, then wait for this fix rather than re-queue blindly — no longer needs a second round.Marking ready and adding to the merge queue.
Generated by Claude Code
Filed unassigned from #6128 / PR #6248 —— 该 PR 的
Test Core (3/3)被这条用例踢红,而它只改了packages/lint+packages/formula。本单只记录发现,不含修法承诺。签名
判据(不是猜的)
用例(
packages/cli/src/utils/format.exit-code.test.ts:101-115,由 #6220 / #4873 引入):而
createTimer(packages/cli/src/utils/format.ts:158-164)是真实墙钟:531 & 0xff === 19,532 & 0xff === 20,533 & 0xff === 21。也就是说这条断言只有在timer.elapsed()恰好返回 0 时才成立 —— 而两行之间隔着一次await emitJson(...)和若干模块工作。runner 上只要跨了 1 毫秒就红,红的数字正好是20,与 CI 的实际输出一致。本机复现:空载时 6/6 全绿(
elapsed()一直是 0);把两行之间的耗时人为拉到 0-2ms 后,200 次里 133 次断言不成立。CI runner 比本机忙,所以它先撞上。归属
fix(cli): os migrate --json 成功时不再把耗时毫秒当成退出码),晚于 PR feat(lint): view/page 谓词裸标识符构建期闸门 —— 坏谓词发不出去 (#6128) #6248 的分支点,PR feat(lint): view/page 谓词裸标识符构建期闸门 —— 坏谓词发不出去 (#6128) #6248 树里根本没有这个文件 —— 它是通过 CI 的 merge ref 进来的,会踢红任何当天开出的 PR。now确定性断言按「值」比较,只有跨毫秒时才会红 —— 一条按运气报警的 pin #5896 是同一族:「确定性断言按值比较,只有跨毫秒时才会红 —— 一条按运气报警的 pin」。修法方向(供分诊参考,非承诺)
用例真正想钉的是「Node 把退出码截断到 8 位,所以一个毫秒数会变成看似随机的退出码」。这与计时器无关,把
durationMs换成一个字面量即可,断言随之变成确定性的:timer在这条用例里只提供了一个不确定性来源,没有提供任何被测行为 ——emitJson的类型拒绝与process.exitCode的截断都与它无关。Refs:#6220 / #4873(引入处)、#5896(同族)、#5810(队列 flaky 处置)、PR #6248(被踢红的 PR)。