跳转至

[ISSUE-0041] 第 7 轮 agent 验证 v0.10.58 — 还有 4 处漏

v0.10.58 修了 ISSUE-0040 6 个 bug + 沉淀 scan:error-handling + 写 bug-fix-full-dictionary-scan.md 后,第 7 轮独立 agent 验证仍找出 4 处 scan 工具漏抓的同家族 bug。

关键发现:自动化扫描覆盖了一级模式(直接 .catch(()=>{})),但二级形态(writeChain = X.then(...).catch(()=>{}) 把 catch 提到链尾)完全在盲区

4 个 bug

Bug 1 (高):content-search「打开搜索」按钮静默失败 — ISSUE-0040 A2 反向

src/sections/content-search/index.tsx:246

// ❌ v0.10.59
browser.runtime.sendMessage({ type: 'open-page-main' });

但 background src/entrypoints/background/index.ts:115 是用 webext-bridge 注册的:

onMessage('open-page-main', async () => {
  await openWindow('main');
  return { success: true };
});

chrome 原生 sendMessage 不会触发 webext-bridge 的 handler,会落到下面 runtime.onMessage.addListener 的 default fallback → 用户点「打开搜索」按钮静默失败

这是 ISSUE-0040 A2 的反向同款(A2 是「webext-bridge 调 chrome 原生」,本 bug 是「chrome 原生 调 webext-bridge」)。两个方向都漏过 — 自动化扫描没扫这个匹配关系(caller 协议 ↔ handler 协议)。

// ✅ v0.10.60
import { sendMessage } from 'webext-bridge/content-script';

onClick={async () => {
  const resp: any = await sendMessage('open-page-main', {}, 'background');
  if (!resp?.success) {
    console.warn('[content-search] open-page-main failed', resp);
  }
}}

Bug 2 (中):writeChain 二级形态漏修 — page-log + task-progress

ISSUE-0040 A1 修了 task-store.ts 的 addTask/updateTask/removeTask 把 catch 提出来:

// ✅ task-store.ts 已经是这个形态
const next = writeChain.then(async () => { ... });
writeChain = next.catch(() => {});   // chain 自愈
return next;                          // caller 见错误

page-log.ts:46 + task-progress.ts:46/73 仍是老形态:

// ❌ v0.10.59
writeChain = writeChain
  .then(async () => { ... })
  .catch(() => {});      // 把错误彻底吞掉
return writeChain;        // caller 拿到 resolved Promise — 永远不知错

为何 scan:error-handling 没扫到:脚本只匹配单行 .catch(() => {}),但这里的 .catch 是在 .then 链尾。hit_key 用 file+line+snippet 当 key — 链尾 catch 命中后归到 fire-and-forget 基线,所以视为合理设计直接放过了。

:照 task-store.ts 模板 next+writeChain 分离。

Bug 3 (中):apiCountryLocations API 形状假设 — country-stats + simple-locations-select

umi-request 在某些拦截器配置下会把外层 { code, data } 脱掉(特别是 ISSUE-0034 修 baseConfig 之后)。

// ❌ v0.10.59 假设永远 { items: [...] }
const items = (r?.items || []) as ...;
//   r 实际可能直接是 [...]  → items = []  → "无数据"

命中位置: - src/utils/country-stats.ts:59 - src/components/locations-select/simple-locations-select.tsx:79

注:task-create-dialog.tsx:116location-picker-dialog.tsx:308 在前几轮已用 Array.isArray 兜底 — 这两个是漏改的同款。修了 4 处中 2 处,没扫剩下 2 处

const items = (Array.isArray(r) ? r : r?.items || []) as ...;

Bug 4 (低):location-picker retryStat / loadStates React lifecycle race(v0.10.61 修)

MUI Dialog 默认 keepMounted=false → 关闭即卸载。两类 race:

  1. unmount race:用户点 ↻ 重试国家点位 → invalidate + preloadAll 在跑 → 用户关 dialog → 异步回调 setState → React 警告 + 内存浪费。同款问题在 loadStates(apiCountryLocations.then/.catch/.finally 都 setState)。

  2. stale-data race(顺手修):用户 country A→B 快速切换 → A 的 in-flight 响应后到 → setStatesData(A's items) 覆盖 B 的视图 → 一闪而过的错误数据。

const aliveRef = useRef(true);
useEffect(() => {
  aliveRef.current = true;
  return () => { aliveRef.current = false; };
}, []);

// retryStat: 用 aliveRef 守卫
.then(() => preloadAll([iso2], (k, v) => {
  if (!aliveRef.current) return;
  setCountryStats((s) => ({ ...s, [k]: v }));
}))

// loadStates: aliveRef + currentLoadRef 双守卫
const currentLoadRef = useRef<string | null>(null);
const loadStates = (iso2: string, force = false) => {
  currentLoadRef.current = iso2;
  ...
  .then((r) => {
    if (!aliveRef.current || currentLoadRef.current !== iso2) return;
    ...
  })
  ...
};

为什么 v0.10.58 + scan 工具仍漏

scan:error-handling 盲区
Bug 1 不扫 caller 协议 ↔ handler 协议匹配关系
Bug 2 只匹配单行 .catch(()=>{}),不识别 writeChain.then(...).catch(...) 链尾形态
Bug 3 不在 silent_catch / catch_to_empty / send_without_check 三类内 — 完全跨类

元洞察:自动化扫描固化了当前已知的模式。新模式(即使概念上同家族)需要新工具或扩展现有规则。

修复总结

Bug 文件 变更
1 content-search/index.tsx 改用 webext-bridge sendMessage + 检查 resp.success
2a utils/page-log.ts next+writeChain 分离
2b utils/task-progress.ts 同上(recordTaskProgress + clearTaskProgress)
3a utils/country-stats.ts Array.isArray defense
3b components/locations-select/simple-locations-select.tsx 同上
4 components/locations-select/location-picker-dialog.tsx aliveRef 守 unmount + currentLoadRef 守 country 切换 race(v0.10.61)

audit_grep 三道闸(已写入本文 frontmatter)

audit_grep:
  - pattern: "writeChain\\s*=\\s*writeChain\\s*\\.\\s*then[\\s\\S]{0,400}\\.\\s*catch\\(\\s*\\(\\s*\\)\\s*=>\\s*\\{\\s*\\}\\s*\\)"
    description: "writeChain 链尾 catch  必须改 next+writeChain 分离"
  - pattern: "browser\\.runtime\\.sendMessage\\(\\s*\\{\\s*type:\\s*['\"]open-page-main['\"]"
    description: "open-page-main 必须走 webext-bridge"
  - pattern: "apiCountryLocations\\([^)]*\\)[^;]*\\.\\s*then\\(\\s*\\([^)]+\\)\\s*=>\\s*[^A]*\\?\\.\\s*items"
    description: "apiCountryLocations 形状假设  Array.isArray 兜底"

后续 pnpm scan:issue-coverage 每次 pre-commit 自动反查 — 防新代码再引入。

元规则提案

每隔 N 轮 ISSUE,独立 agent 应专门审查 scan 工具的盲区 — 不是再找 bug,而是问「我们的工具能不能再发现这些 bug?」

工具是凝固的智慧,但凝固也是凝固 — 同家族的不同形态它认不出来。

相关

  • [[0040-round6-agent-6-bugs-incomplete-patch|0040-第6轮agent发现6个真bug-补丁不彻底家族]] — 第 6 轮父集
  • 修bug全字典扫描 — 第 6 轮沉淀的规则(本轮证明:规则在,工具在,仍能漏)
  • 独立agent审查 — 跨视角防 anchor bias