Files
HomeAgent/cmd/gui
JianFeeeee ee5cb75e00 fix(gui): 401 重试无限递归 —— 我上一个优化放大的 bug
## 真机实测发现的

Xvfb + Electron + CDP 真跑,发现 `api()` 的 401 分支**无限自我递归**:

    第1次: lock=false → 重登 → lock=false → return api()   ← 递归
    第2次: lock=false → 重登 → lock=false → return api()   ← 又递归
    …

那把锁的语义本该是「已经重登过一次,别再登」,但它在递归**之前**就被
清掉了 ⇒ 每层递归看到的都是 `false`。真机实测 **fetch 被调 13 次、
重登 12 次**才被我的探针上限截断。

## 为什么这与我的上一个提交直接相关

`1ef1998` 把 401 分支的固定等待从 800ms 降到 30ms(修「认证过期时每个
请求白等 0.8s」)。但重试**没有次数上限** ⇒

- 改前:每 800ms 慢速空转
- 改后:每 30ms 快速烧 CPU + 反复打服务端

**我的优化把这个 bug 放大了。** 真机上探针调用 `api()` 直接挂死,
我起初还以为是我的测试写法问题。

## 修法

两处递归点(真 401 / 门户返回 200 但内容是登录页)都改成:

    try {
      return await api(p, o);
    } finally {
      window._haReloginLock = false;
    }

`finally` 保证递归抛错时也释放锁 —— 否则会把后续所有请求都锁死成直接 401。

## 判据 retry-guard.test.mjs

在 `node:vm` 沙箱里跑**真实抽出的 `api()`**,用恒回 401 的 `fetch` 驱动,
统计真实调用次数。

### 写这条判据时踩的四个坑(都记在文件里)

1. **先给了假绿灯**:把 401 分支当独立函数体执行,但那块以
   `return api(p,o)` 结尾、外面没有调用它的上下文 ⇒ 我从未真正进入那个
   `if` ⇒ `api 递归=0` ⇒ 什么都没测到。改成跑**真实 api()** 才对。
2. **递归时忘了保持 `r.status===401`**:真实场景是「重登后凭据仍是错的」。
   漏了它 ⇒ 递归那层不进 401 分支 ⇒ 又一次假绿灯。
3. **沙箱 `setTimeout` 只记录不执行**:`api()` 用它做超时控制
   (`setTimeout(() => ctl.abort(), to)`)⇒ AbortController 永不被 abort
   ⇒ 表现为「fetch 只调 1 次、8000ms 被当成 401 等待」。
4. **沙箱缺 `clearTimeout`** ⇒ 抛 `clearTimeout is not defined` ⇒
   整段在 fetch 之后就断了 ⇒ 永远走不到 401 分支。

★ 共同点:**沙箱不完整 ⇒ 静默地什么都没测 ⇒ 假绿灯**。
判据自己给假绿灯比没有判据更危险。

### 变异测试(精确锚点,验证判据真能抓)

把第一处 try/finally 退回「递归前清锁」⇒ 判据立刻红
(`fetch 被调 13 次`);恢复后全绿。

★ 第一次做这个变异时我误判「判据漏抓」—— 实际是我的变异脚本用了模糊
锚点、**压根没改到文件**(`grep` 显示 return await 从 2 变 1,但 `sed`
命中的是另一处)。两个信号矛盾时先坐实文件状态,别急着改判据。

## 顺带把 `make test` 的门禁修好(9c3d285 / f59a3cf 之外)

新判据已接入 `npm test`,实测 14 项通过、`make test-gui` 全绿。
2026-09-28 09:11:58 +08:00
..