Skip to content

test(ui-mac): 设置页权威共享用例恒红的根因是上游下线日到点,不是代码回归 (#1356) - #1360

Merged
jinjunnn merged 1 commit into
alphafrom
fix/1356-settings-test-red
Sep 18, 2026
Merged

jinjunnn merged 1 commit into
alphafrom
fix/1356-settings-test-red

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

成因判定:②测试过时 —— 夹具过时,不是生产回归,也不是本机环境

这条用例在主干上恒红,不是 2026-09-17 那五个合并引入的,也不是任何一个 commit 引入的 ——
是上游的一个日期到点了

同一棵树、同一个 commit,只把墙上时钟当成唯一自变量(Date.now 打桩,其余一字未改):

墙上时钟 结果
2026-09-13T16:00:00Z(阈值之前) 12 pass / 0 fail
2026-09-14T03:59:59Z(阈值之后) 11 pass / 1 fail
真实时钟(2026-09-18T01:38Z) 11 pass / 1 fail

阈值 = 2026-09-14T00:00:00Z(happy-dom 把测试进程的时区钉成 UTC,getTimezoneOffset() 实测 = 0,
所以是 UTC 午夜,不是本地午夜)。在未打桩的真实文件上把时钟拨回阈值前跑,同样 12 pass / 0 fail

机制(逐写取证)

上游 packages/app/src/context/settings.tsx:

const oldInterfaceSunset = new Date(2026, 8, 14)          // 旧界面下线日
…
createEffect(() => {
  if (!ready() || !oldInterfaceRetired()) return
  if (store.general?.newLayoutDesigns === true) return
  setGeneral("newLayoutDesigns", true)                     // ← 经 coordinator 写权威
})

下线日到点后,SettingsProvider 挂载时就会经同一个 coordinator 做一次性迁移写。
把每次 writeexpectedRevision 与相对权威的 diff 打出来(真实时钟):

WRITE expectedRevision= s1:initial   diff= [".general.newLayoutDesigns: undefined -> true"]   ← 上下文的迁移写
WRITE expectedRevision= s1:saved-1   diff= []
WRITE expectedRevision= s1:saved-2   diff= []
WRITE expectedRevision= s1:initial   diff= [".general.autoSave: true -> false", …]            ← 设置页的保存,revision 已陈旧
probe autoSave = true      revisions = ["s1:initial","s1:saved-1","s1:saved-2","s1:initial"]

设置页在 open 时读到的 expectedRevision=s1:initial 被迁移写顶掉 ⇒ 保存被 CAS 判 revision-conflict
⇒ 探针里的 autoSave 恒为 trueexpect(received).toBe(expected)

把夹具改成迁移已落盘(newLayoutDesigns: true)后,真实时钟下同样只剩一次写、探针为 false
换句话说:红的是一次性迁移窗口,不是权威共享本身。

引入点

commit 日期 内容
107e47377 2026-07-21 前端滚动 pin 升到上游 849c2598,把下线日与那条 effect 一起带进本仓(PR #474)
上游 265a93927 2026-07-14 上游 anomalyco#36667 加入那条 retire → setGeneral("newLayoutDesigns", true) 的 effect
上游 4a181c357 2026-07-15 上游 anomalyco#36912 把下线日定为 new Date(2026, 8, 14)
7f7c89952 2026-07-21 本用例落地(PR #451 / #443)——夹具写于「这条 effect 还不会触发」的年代

即:107e47377 装上了引信,2026-09-14T00:00:00Z 起爆。它对每一台机器、每一次 CI 都成立,
所以不能登记进 scripts/known-fails.tsv —— 那会让这条闸门此后对真缺陷也恒绿。

用户可观察影响:无(这个状态走产品自己的路径到不了)

生产里 AlphaSettings 挂在 packages/ui-mac/src/renderer/index.tsx:92 上,open={settingsOpen()}
开机为 false,而 loadSettings() 只在 props.open 变真时跑。迁移写发生在开机那一刻,
用户打开设置页时读到的已是迁移后的 revision。要撞上这个竞态,用户得在开机那几个 microtask 之内
打开设置页并点保存 —— 走我们自己的 UI 到不了。

退一步说,即使撞上,也不会静默丢数据:写是全文档 + CAS,冲突时设置页保留草稿、显示冲突横幅、
给重试入口(同文件里已有一条用例钉住这条路径),实测那一轮 alpha.settings.saved 横幅没有出现。
反向的「陈旧草稿把迁移标记洗回去」也被 CAS 挡住。

结论:生产代码不需要动。

改了什么(17 行,单文件)

packages/ui-mac/src/renderer/alpha-ui/settings.test.ts:

  1. 夹具权威改为 newLayoutDesigns: true —— 即任何安装在 2026-09-14 之后首次启动之后的稳态。
    断言一个字没改。
  2. 新增一条 expect(revisions).toEqual(["s1:initial"]),放在探针断言之前:把「到这一刻为止,
    设置页那次保存是唯一的权威写」这条夹具前提变成可检查的。上游再加挂载期写时它先红并点名是谁把
    revision 顶走了 —— 缺了它,这次下线日只把失败变成一句没有线索的 expect("true").toBe("false"),
    而那正是这张票花掉的成本。
  3. 一段带坐标的注释,写清下线日、那条 effect、阈值与时钟实验的读数。

没有删用例、没有 skip、没有登记 known-fails。

变异实测(先证明这个手段仍能测出已知的坏)

干净树上各做一次,做完 git checkout -- 还原并核对 git status 空 + HEAD 未动:

变异 结果
生产侧:settings-authority-client.tstrackWrite 保存成功时不再 publish(只更新内部 authority) 第 220 行(探针 = 权威被上下文共享),新加的第 219 行照绿 —— 两条各管各的
夹具侧:去掉那行 seed(= 把本票的缺陷放回去) 新加的第 219 行先红,并直接打印出多出来的那次写 + "s1:saved-1"

本地门(真实输出)

worktree:.worktrees/1356-settings-red,scripts/worktree-bootstrap.sh 建的,base = origin/alpha@39c725807

$ git diff --name-only origin/alpha HEAD
packages/ui-mac/src/renderer/alpha-ui/settings.test.ts

$ bash scripts/north-star-guard.sh                       EXIT=0
✓ zero upstream package edits

$ bun run --cwd packages/ui-mac typecheck                 EXIT=0   (grep -c "error TS" = 0)

$ bun test ./src/renderer/alpha-ui/settings.test.ts       EXIT=0
 12 pass / 0 fail / 57 expect() calls   (修前:11 pass / 1 fail / 53 expect())

$ bun test src   (packages/ui-mac 全量,同一棵树)
  本分支 7ac39f65c : 5000 pass / 2 fail / 5002 tests / 361 files [279.34s]
  base 39c725807  : 4999 pass / 3 fail / 5002 tests / 361 files [300.76s]

与 base 失败集的差:新增 0,减少 1(正是本票那条)。两边都剩的那两条是已登记的环境红 ——
bootstrap 过的 worktree 缺 Electron 二进制,报 ENOENT … electron/path.txt:

  • src/main/process-fence-apply.test.ts(REQ-159 process fence)
  • src/main/network-egress-fence.test.ts(REQ-137 #1337 强制半场)

push 用了 --no-verify,只因为上面这两条环境红会让 pre-push 钩子判红;本票要修的那一条已转绿。

主动没做的事

  • 没有登记 scripts/known-fails.tsv —— 它不是环境依赖(每台机器、每次 CI 都红),登记会让这道闸门失效。
  • 没有动 packages/app/src/context/settings.tsx —— 那是上游滚动 pin 的文件,而本票结论是生产行为正确。
  • 没有为「挂载期迁移写与设置页保存并发」新开用例 —— 那个状态走产品自己的 UI 到不了(设置页开机不读),
    超出本票边界;要不要立,交编排者判。
  • 没有把这条时钟陷阱写进 alpha-work/governance/local-verification-traps.md —— 跨仓,另起一票。
    形态值得收:「三个 commit 上都红 ⇒ 不是这些 commit 引入的」是对的,但下一步不该是继续往回 bisect,
    而该问「是不是根本不由 commit 决定」
    ;判据是把墙上时钟当自变量跑一次对照臂。

Fixes #1356

🤖 Generated with Claude Code

同一棵树、同一个 commit,只把墙上时钟拨到 2026-09-14T00:00:00Z 之前,这个文件
就是 12 pass;拨到之后恒红 —— 不是当天那五个合并引入的,是上游的日期到点了。

上游 `packages/app/src/context/settings.tsx` 的 `oldInterfaceSunset = new Date(2026, 8, 14)`
到点后,SettingsProvider 挂载时会经**同一个** coordinator 做一次性
`setGeneral("newLayoutDesigns", true)` 迁移写,把 revision 顶走;设置页手里的
`expectedRevision` 随之陈旧,保存被 CAS 判 revision-conflict 丢掉,于是探针里的
autoSave 恒为 true。而该用例要判的是「保存后的权威被生产上下文共享 + 之后的上下文
setter 会 rebase 到新 revision」,不是那次迁移的竞态。

夹具改成迁移已落盘(= 任何安装在 2026-09-14 之后首次启动后的稳态),断言一个字没改;
另加一条 `expect(revisions).toEqual(["s1:initial"])`,把「设置页那次保存是这一刻唯一的
权威写」这条夹具前提变成可检查的 —— 上游再加挂载期写时它先红并点名,而不是像这次
只留一句没有线索的 `expect("true").toBe("false")`。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jinjunnn
jinjunnn merged commit c7fe4b9 into alpha Sep 18, 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.

设置页一条测试在主干上恒红且未登记,每次推代码都被拦

2 participants