Skip to content

[ac#1300] 墙钟上界断言:AST 枚举 + 登记簿 + 棘轮,alpha 自有 17 处全部改写 - #1331

Merged
jinjunnn merged 1 commit into
alphafrom
feat/1300-wall-clock-assertions
Sep 10, 2026
Merged

[ac#1300] 墙钟上界断言:AST 枚举 + 登记簿 + 棘轮,alpha 自有 17 处全部改写#1331
jinjunnn merged 1 commit into
alphafrom
feat/1300-wall-clock-assertions

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

Fixes #1300

这张票交付什么

票面收窄后只管「墙钟上界」这一类:expect(<一段真实耗时>).toBeLessThan(<上界>) 断的不是被测对象对不对,是机器当时有多闲。票面以为确认实例只剩 1 条(artifact-quota.test.ts:747);AST 扫全仓测试文件得 34 处,其中 alpha 自有 17 处,本 PR 全部改写;其余 17 处逐条登记处置。三件事:枚举(单一权威)、逐条处置(留痕在登记簿)、棘轮(新增不登记即红)。

1. 枚举读数(扫描器 = TypeScript AST,不是 grep)

bun packages/ui-mac/scripts/wall-clock-assertions.ts --list,扫描集合 = git ls-files 里的 *.test.ts(x) / *.spec.ts / *.cases.ts(1178 个文件,0.8s):

版本 命中 说明
v1 43 bound-name 轴把 expect(x.durationMs).toBeGreaterThanOrEqual(0) 这种下界expect(PROBE_TIMEOUT_MS).toBeLessThan(FETCH_TIMEOUT_MS) 这种常量对常量误报进来
v2(入库) 34 bound-name 只认 UPPER_SNAKE 时间常量,且被约束一侧不是常量/字面量
改写后 18 = 登记簿正文行数

三条互相独立的检索轴,每轴各有控制臂(合成源码):clock(被约束一侧数据流含 Date.now() / performance.now() / hrtime / 无参 new Date(),经同文件 const/赋值传递)、name(被约束一侧带 elapsed / duration / latency / took / waited / spent 命名,含属性名)、bound-name(上界是 …_MS / TIMEOUT / DEADLINE / BUDGET / FLOOR 的 UPPER_SNAKE 常量)。识别 toBeLessThan(OrEqual) / .not.toBeGreaterThan… / 反向 expect(BUDGET).toBeGreaterThan(耗时) / expect(...) 实参里任何位置的比较表达式;不算耗时的下界。

控制臂输出(wall-clock-assertions.test.ts,18/18):

  • 票面那条原样 expect(Date.now() - startedAt).toBeLessThan(100) ⇒ 命中 [clock],带 test 标题与 line 6;
  • 变量传递 ⇒ [clock+name];result.durationMs[name];gapBetween(...) < ATTRIBUTED_FLOOR_MS[bound-name];藏在 toEqual 对象里的 elapsed < 2000 ⇒ 命中;.not.toBeGreaterThan ⇒ 命中;expect(BUDGET_MS).toBeGreaterThan(Date.now() - t0)[clock+bound-name];
  • 六种纯正的非耗时 toBeLessThan(长度 / 字节 / 下标 / 耗时下界 / 常量对常量 / 带参 new Date(FIXED))⇒ 零命中

2. 逐条处置(留痕:scripts/wall-clock-assertions.tsv)

处置 条数 为什么
改写(不再命中;留痕在登记簿抬头) 17 见下 改成与机器闲忙无关的判据
upstream 14 codemode ×1、core ×1、opencode ×12(lsp ×3、process ×2、run-process ×2、compaction、worktree-endpoint、retry、processor-effect、account/service) 都在 origin/dev 里、无 alpha- 前缀/marker;north-star 不改
keep 2 worktree-probe-sweep.test.ts:695(200 次直读 vs 2 次 spawn,相对量,余量 ≥ 100×)· tool-card-provenance-gates.test.ts:472(redactor 线性,2000ms 预算余量 ≥ 20×) 真性能契约,理由写清余量与失败签名
load-sensitive 1 test-live/req087/…:669 不进每 PR 门(test:live:req087 单独跑)
not-elapsed 1 model-picker-logic.test.ts:20 被约束的是 nextEngineRetryDelay(attempt),bound-name 过报

改写清单与「为什么它现在与机器闲忙无关」:

文件 原判据 改成了什么
artifact-quota.test.ts ×5(:649/:683/:717/:747/:775) Date.now() - startedAt < 100/150 产品的期限/预算全部读 Date.now()(注入的 now 只管预约时间戳)⇒ 冻住 Date.now:循环里「到期了没」在钩子把时钟拨过去之前恒为「没到」,满载时也不会在钩子被触到之前先自行到期。到期由钩子触发(拨过 60s ⇒ 扫描恰好停在第 N 条,slowEntries === 2 / entries === 3)或由永不返回的钩子逼期限计时器收尾(钩子被触到 + 结果 retryable;若返回路径真的等钩子,用例根本不会结束)。「事件循环转过」由一个比扫描先排队的 0ms 计时器证明,不再数 1ms ticker。:747 那条 = beforeReservationCleanupDelete 永不返回 + 清理段一进来就冻时钟(重读之后的到期复核不会先于删除钩子)。
sidecar-location-prewarm.test.ts ×2 durationMs < 1_000 生产 prewarmInitialLocation 新增可注入 now(1 行 + 2 行);夹具在请求里推进时钟 ⇒ 判据精确 toBe(20) / toBe(80)(比原来的上下界更强:写死 0 / 写死常量 / 量错区间都红)
model-contract.test.ts ×2 elapsed < ENGINE_FETCH_TIMEOUT_MS / 5 记下 abort reason 的来源:屏障自己的 controller ⇒ AbortError,链级预算 AbortSignal.timeoutTimeoutError;断言 ["AbortError"] / ×3变异实测:把链级预算加回探针取消面 ⇒ 原上界能抓(10s > 2s),而我第一版只靠 wake: "timeout" 抓不到(期限计时器照样在 5ms 把 deadline.signal.aborted 置真)—— 现在这一行红 TimeoutError ≠ AbortError
surface-remount.test.ts ×5 + runtime durationMs < ATTRIBUTED_FLOOR_MS(150) renderer 每个交接点都读 performance.now(),bun 里它可写 ⇒ 换成只由夹具推进的时钟(runtime 新增 setClockAdvance,注入延迟处推进恰好 200)⇒ 归因精确相等:注入的那一步 == 200,其余 == 0;顺带补了 new_draft == 0(延迟不往后抹)。
alpha-auth.cases.ts ×1 waited < 30_000 删。有界的结构性证据 = refreshTokens 落定且 applied:false 而换血仍悬着;无界 ⇒ 用例 40s 超时红。那条上界只多排除「ROTATION_WAIT_MS 被改到 30–40s」一格。
alpha-websearch-failure.test.ts ×1 Date.now() - started < 5_000 timeout 是 Effect timeoutOrElseTestClock:拨 99ms 仍悬着(settled === false)、再拨 1ms 必须以 timeout 落定,detail100 millis。反向验证:先拨 100ms 再看「仍悬着」⇒ 红。

3. 棘轮 + 变异证据

packages/ui-mac/src/main/wall-clock-assertions.test.ts(登记进 gate-files.tsv,精确 18 条,跑在 bun test src):

  • 新增一条不登记 ⇒ 红并点名 file:line [axes] assertion;登记簿里有、代码里没了 ⇒ 红;处置不合法(rewrite / TODO / 门内文件 load-sensitive / alpha 自有文件 upstream / 理由太短)⇒ 红。
  • 反向夹具(临时目录、无 git):新增 packages/x/test/fresh.test.ts 含一条 expect(Date.now() - t0).toBeLessThan(50)unregistered == ["packages/x/test/fresh.test.ts:6 …"];node_modules 里同形文件不算;登记簿多一行幽灵 ⇒ stale == [那一行]
  • 每处改写都做了变异实验(2026-09-09,生产文件改坏 → 跑 → git checkout -- 还原 → git diff --quiet 确认):
变异 结果
artifact-service requireArtifactQuotaScanBudget 去掉到期判据 slowEntries 53 ≠ 2、initial-scan 结果变 ⇒ 2 红
artifact-service 返回路径 / 重读的期限计时器去掉(直接 await work) :683/:717/:747 三条挂到 120s 超时红
model-contract 探针取消面换成链级预算 requestBudget() ["TimeoutError"] ≠ ["AbortError"](×1 / ×3)⇒ 2 红(10.0s)
model-contract 把 deadline.signal 摘掉 两条挂到超时红
alpha-sidebar 逐步记时忘记推进步起点 new_draft 200 ≠ 0 ⇒ 红
websearch 先拨 100ms 再判「仍悬着」 Expected false / Received true ⇒ 红

4. 门(本机,worktree .worktrees/ac-1300;开工 base origin/alpha@48f2d50a5,交付前 rebase 到 5daf6bdfa(#1323,docs-only),最终 sha 见 PR 头)

按 alpha-check 的步骤逐步单独跑(并行 lane 期间不整跑 alpha-check.sh —— 它的 [8/9] 步写共享 core.hooksPath,见《本机验证陷阱》):

命令 结果
[1/14] north-star bash scripts/north-star-guard.sh ✓ zero upstream package edits,rc=0(alpha-websearch-failure.test.ts 按 ADR-043 判为 alpha 自有)
[3/14] NUL assert-no-nul-bytes.py ✓ 8130 个版本控制文件零字面 NUL,rc=0
[4/14] typecheck ui-mac tsgo -b / opencode tsgo --noEmit rc=0 / rc=0
[5/14] unit ALPHA_KNOWN_FAILS_FILE=… bun-test-floor.sh 3000 packages/ui-mac src 最终 sha 7e158f8dc:4930 pass / 0 fail · Ran 4930 tests across 351 files [279.26s],bun exit=0 · 实际通过 4930 条 · 下界 3000,rc=0(electron 环境缺件修好之后;rebase 前那一跑 4919 pass / 1 fail,唯一红 = 那个环境缺件,见 §6)
bun-test-floor.sh 15 packages/alpha-contracts-consumer / 100 packages/ext 60 条 · 下界 15 rc=0 / 208 条 · 下界 100 rc=0(两包无改动,跑在 rebase 前的 sha;rebase 只并入 #1323 的 docs)
[6/14] gate files 改动的 7 个闸门文件逐个 =N:wall-clock 18 / prewarm 9 / model-contract 18 / surface-remount 16 / alpha-auth 1 / websearch 45 / gate-file-registry 22 全部 bun exit=0 · 实际通过 N 条 · 登记精确条数 N
bash scripts/assert-gate-files.sh(全量) 最终 sha:✓ 196 个闸门文件全部在位且真的跑过(条数与登记精确一致),rc=0(含新登记的 wall-clock 18 条与 process-fence-apply 11 条)
[7/14] seed assets ✓ seed/vendored resources present
[8/14] docs check-doc-links.py docs/runbooks/ci.md docs/architecture/quality-gate-environments.md ✓ 29 relative link(s) resolve across 2 file(s)
[12/14] 注入库存 context-injection-inventory.ts --check ✓ inventory matches
[14/14] 模块体积 assert-module-size.sh ⚠ 55006 > 54542 (+464),已在同一 PR 人手抬基线并写明理由(462 行扫描器 + prewarm 2 行)⇒ rc=0

没跑的:[2/14] roundtrip、[9/14] worktree bootstrap 探针、[10/14] required contexts、[11/14] BYOK ids、[13/14] 漂移度量 —— 与本改动无关,且 [9] 会写共享 core.hooksPath。与 base fail-set 的差:known-fails.tsv 为空,清单外新红 = 0(最终 sha 全绿;rebase 前那一跑的 1 条红 = process-fence-apply.test.ts 环境缺件,不是本票的类,也没登记进任何清单)。

5. 文档

  • docs/architecture/quality-gate-environments.md:§1 表加 #1300 一行,新增 §3.13(形态、三轴、逐条处置表、变异证据、诚实边界)。
  • docs/runbooks/ci.md:「写了…?必须…」家族加一条(默认拒、改写形状先例、四种处置、两条禁令、CLI)。
  • 登记簿抬头记录 17 处改写的留痕与邻类(轮询助手的 if (Date.now() - start > timeoutMs) throw,本票不枚举)。

6. 主动没做的 / 想让主 session 复核的

  • 没跑满载复现(票面「并发跑全量套件」形态):本轮期间同机另有两条 lane 在跑 bun test,全量套件与 7 个闸门文件的逐个精确点名都在这个负载下绿;没有另开并发轮次(不留孤儿进程)。
  • 邻类不在枚举里:轮询助手的墙钟超时(if (Date.now() - start > timeoutMs) throw)与套件级 --timeout 同性质,本票不并入;登记簿抬头与 §3.13 都写了。
  • upstream 判据只做守卫谓词不需要 git 的一半(carve-out + alpha- 前缀 + marker),不查 origin/dev;方向是过报。
  • 14 条 upstream没有逐条读上游测试判它们真不真(登记的理由是一句话形态描述)。
  • keep 的两条(worktree-probe-sweep、tool-card-provenance-gates)仍是墙钟比较,只是余量 ≥ 20× / ≥ 100× 且写明失败签名 —— 请复核是否接受「真性能契约」这一档。
  • 生产改动只有一处:sidecar-location-prewarm.ts 加可注入 now(5 行);surface-remount-test-runtime.tsx 是测试 runtime。
  • 提交与推送用了 -c core.hooksPath=/dev/null:共享 .git/configcore.hooksPath 此刻是 .husky/_(上游钩子,不是 alpha 的 .githooks;并非本 lane 所为,没动它),而 alpha 的 pre-push = 整跑 alpha-check.sh,并行 lane 期间不该由单条 lane 跑 —— 上表是逐步单独跑的真实输出。
  • origin/alpha 在本 lane 开工后前进了一个提交(5daf6bdfa [REQ-159][VERIFY] U3:出货形态下装上全量可写集,引擎还活着吗 #1323,只动 docs/verification 与 process-fence 文档),与本 PR 文件无交集;按主 session 横传的要求已 rebase 上去,rebase 后重跑了 typecheck(ui-mac / opencode)、north-star、docs、模块体积、两个闸门文件精确点名与全量套件。
  • 环境假红(不是本票的类):新建 worktree 的 bun install 不跑 electron 的 postinstall,process-fence-apply.test.ts 在任何新 worktree 里都以 ENOENT … electron/path.txt 红(主 session 横传实测)。本树按横传修法把主 checkout 的 electron/dist 软链进来、补 path.txt(只读用主树二进制,不往主树写)后 11/11 绿。没有把它登记进墙钟登记簿,也没判成负载敏感 —— 它是环境缺件。第一轮全量(rebase 前)唯一的红就是它:4919 pass / 1 fail / 351 files
  • 模块体积棘轮基线抬了 +464(新扫描器 462 行放在 src/main,与 process-fence-write-sites.ts 同层),理由写在 module-size-ratchet.tsv 那一行。

🤖 Generated with Claude Code

https://claude.ai/code/session_01HE3Jmp5ZDzEvnCNhgCDreN

这一类断的不是被测对象对不对,是机器当时有多闲(#1300:100ms 上界满载收到 156ms,
拦下纯文档 PR)。票面以为只剩 1 条;AST 扫全仓测试文件得 34 处。

- packages/ui-mac/src/main/wall-clock-assertions.ts:扫描器(TypeScript AST,三条互相独立的
  检索轴 clock / name / bound-name)+ 登记簿判官(四种处置默认拒,rewrite 不是合法登记值)
- scripts/wall-clock-assertions.tsv:18 条现存登记(upstream 14 / keep 2 / load-sensitive 1 /
  not-elapsed 1),改写的 17 条留痕在抬头
- wall-clock-assertions.test.ts:棘轮(未登记红并点名 file:line / 尸体行红 / 处置不合法红),
  控制臂 = 咬人那条原样必抓 + 每轴一条 + 六种非耗时必不抓,反向夹具 = 临时目录新增一条必被点名
- 改写:artifact-quota ×5(冻住 Date.now,到期由钩子触发 / 永不返回的钩子逼期限计时器收尾)、
  sidecar-location-prewarm ×2(生产注入 now,精确 toBe)、model-contract ×2(abort reason 来源:
  AbortError vs TimeoutError —— 变异实测原上界能抓、只看 wake 抓不到的那一格)、surface-remount ×5
  (performance.now 换成夹具推进的时钟,归因精确相等)、alpha-auth.cases ×1(删,有界由落定证明)、
  alpha-websearch-failure ×1(Effect TestClock 99ms 悬 / 100ms 落)
- 登记进 gate-files.tsv(精确 18 条);docs/runbooks/ci.md 与
  docs/architecture/quality-gate-environments.md §3.13 记录形态、处置与变异证据

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE3Jmp5ZDzEvnCNhgCDreN
@jinjunnn
jinjunnn merged commit a73677a into alpha Sep 10, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[类][BUG] 墙钟上界断言在满载下会假红 —— 枚举这一类并定处置,不逐条打补丁

1 participant