Skip to content

fix(ui): drive the update card + consent from one atomic snapshot (stale-expectation recovery)#45

Merged
Wangnov merged 1 commit into
mainfrom
fix/snapshot-lease-stale-expectation
Jun 10, 2026
Merged

fix(ui): drive the update card + consent from one atomic snapshot (stale-expectation recovery)#45
Wangnov merged 1 commit into
mainfrom
fix/snapshot-lease-stale-expectation

Conversation

@Wangnov

@Wangnov Wangnov commented Jun 10, 2026

Copy link
Copy Markdown
Owner

问题(用户实测截图)

手动把已装 Codex 降级到旧版后,Manager 能识别"有新版本",但点「立即更新」报错 update engine error: 已装版本已变化(确认时 build 3722,现在 build 3685):请重新检查后再试,且卡片出现「当前 26.608.12217 → 新版 26.608.12217」这种自相矛盾的显示。

根因:主界面持有两份独立刷新的状态——status(macStatus:本地检测 + 托管状态)和 report(macPlanUpdate:检测 + plan)。「重新检查」只刷新 report,但渲染和 perform 的 expectation 都优先从 statusinstalled。带外安装变更后,卡片把过期的 installed 版本和新的 plan 配在一起,点更新发出的 from/to/path 是跨两个时刻拼凑的,被后端的同意完整性护栏正确拒绝——但用户撞上的是一个无法操作的死错误。

修复:把 plan 当作「对单一快照的租约」

  • Home.tsx 单快照report 存在时,它是卡片和 perform expectation 的唯一来源(installed 与 plan 同时检测);status 只在首次检查前兜底、并驱动 managed/external 徽章。check() 现在用 Promise.all 同时刷新 plan 和检测。
  • errors.rs:新增 AppError::StaleExpectation(code stale_expectation),覆盖 perform 期的 TOCTOU 护栏——路径变、from-build 变、to-build 变,以及安装消失(原先是裸 engine_error)。这是"现实已变,请重检"的机器信号。
  • Home.tsx stale 自动恢复:收到 stale_expectation → 自动重检 + 发一条 notice(独立 notice 状态走 ResultBanner,不复用 error)。这样卡片能落到 update / 已最新 / none(Codex 被删)任一态,而不会被 error 驱动的「检查失败」页劫持。
  • Home.tsx 焦点刷新:窗口聚焦时静默重新检测;若安装 identity(build 或 path,任一方向)与快照不同 → 重检并关闭任何打开的确认弹窗(弹窗是为旧目标构建的,留着可能让点击对未见过的快照执行 perform,绕过 external→adopt 边界)。
  • managerApi.ts:新增 errorCode() 读取稳定的 CommandError code。
  • WinHome.tsx:同理优先 report.installed(一致性;Windows 暂无 stale-code 路径)。
  • i18nhome.stale.rechecked 11 语言。

真机验证(arm64)

带外降级 Codex → adopt → 在 Manager 眼皮底下把磁盘换回最新 → 点「立即更新」→ 后端返回 stale_expectationUI 自动重检,显示绿色信息条(而非红色错误),卡片正确落位,不再卡在过期 plan。同时 delta 成功路径与「已是最新」态均回归正常。

关系说明

本 PR 与 #43(vendor BinaryDelta)、#44(quit 确认框)独立但相关:#44 也改 mac_update.rs(quit 调用点),本 PR 改的是 expectation 校验点与 detect_installed,位置不同。建议合并顺序 #43#44 → 本 PR,本 PR 如遇 mac_update.rs 冲突仅为相邻 import/函数,易解。

codex review --uncommitted 迭代 5 轮至无意见;cargo clippy --workspace --all-targets -- -D warningscargo test --workspacenpm run checknpm run test(11)全部通过。

The home screen kept two independently-refreshed sources — `status`
(macStatus: local detect + adoption) and `report` (macPlanUpdate: detect +
plan). A re-check refreshed only `report`, but rendering and the perform
expectation read `installed` from `status` first. After an out-of-band install
change (manual downgrade, Codex self-update) the card could pair a stale
installed version with a fresh plan — e.g. "当前 26.608.12217 → 新版
26.608.12217" — and "立即更新" then sent a from/to/path stitched across two
moments, which the backend's consent guard rightly rejected with a dead-end
error the user couldn't act on.

Treat the plan as a lease over one coherent snapshot:

- Home.tsx: when `report` exists it is the single source for both the card and
  the perform expectation (installed detected together with the plan); `status`
  only fills in pre-check and drives the managed/external badge. `check()` now
  refreshes plan AND detect together (Promise.all).
- errors.rs: new `AppError::StaleExpectation` (code `stale_expectation`) for the
  perform-time TOCTOU guards — install path moved, from-build changed, to-build
  changed, and a vanished install (previously a bare engine_error). It is the
  machine signal for "reality moved, re-check".
- Home.tsx: on `stale_expectation`, auto re-check and post a NOTICE (separate
  `notice` state via ResultBanner, NOT `error`) so the card settles into update
  / up-to-date / none — instead of the `error`-driven "检查失败" hero hijacking
  the now-uninstalled case.
- Home.tsx: a window-focus listener silently re-detects; if the install
  identity (build OR path, either direction) differs from the snapshot it
  re-checks and drops any open confirm sheet (which was built for the old
  target — leaving it up could run perform against an unseen snapshot and bypass
  the external→adopt boundary).
- managerApi.ts: `errorCode()` helper to read the stable CommandError code.
- WinHome.tsx: prefer `report.installed` over `status.installed` for the same
  reason (consistency; Windows has no stale-code path yet).
- i18n: `home.stale.rechecked` in all 11 locales.

Verified live (arm64): downgrade Codex out-of-band, adopt, swap it back to the
latest under the manager's feet, then 立即更新 → backend returns
stale_expectation → the UI auto-re-checks, shows a green info banner (not a red
error), and the card correctly settles instead of stranding on a stale plan.
@Wangnov Wangnov merged commit b9226d8 into main Jun 10, 2026
3 checks passed
@Wangnov Wangnov deleted the fix/snapshot-lease-stale-expectation branch June 10, 2026 04:10
Wangnov added a commit that referenced this pull request Jun 10, 2026
Ships #43 (vendor Sparkle BinaryDelta so macOS delta updates actually run), #44 (surface Codex's quit-confirm dialog instead of stalling), #45 (single-snapshot update card + stale-expectation auto-recovery).
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.

1 participant