Files
goodbuddy/docs/quality/recurring-defects.md
T
mesalogo 1c22e1a06d fix: keep browser overlays and knowledge controls usable
Native browser pages could cover application dialogs and overlapping menus
or notifications. Suspend only the viewport while covered and restore the
same active page afterward, including nested dialogs and terminal prompts.

Keep knowledge source controls at their full height, constrain long library
cards and titles, and update automatic names when switching remote targets
without overwriting edited or saved names. Restore packaged release-note
loading now that the history exceeds its previous size limit.

Add focused regressions and real Electron UI evidence. Clean up temporary
Harness fixture directories at test completion instead of accumulating them.

Release note: 修复内嵌浏览器遮挡弹窗和浮层、知识库来源选项被压缩及长名称显示异常;切换远端知识库时自动名称正确更新,并恢复更新说明显示。
2026-09-13 19:46:37 +08:00

8.3 KiB
Raw Blame History

复发缺陷与防复发规则

本文档收录已经在本仓库重复出现过的缺陷类型。每条规则都来自至少一次真实修正,不是预设的 通用最佳实践。新增条目前,先确认该问题在提交历史中确实出现过一次以上,或一次修正中同时 出现在多处。

修正复发缺陷时,优先建立一条能覆盖整类问题的机制(共享选择器、共享函数、能发现全部实例 的测试),而不是逐个点名修补。逐个修补正是这些条目反复出现的原因。

整窗遮罩与原生拖动区域

症状:弹窗覆盖标题栏区域时,其关闭按钮无法悬停和点击。

原因:.brand 与 .topbar 声明了 -webkit-app-region: drag。整窗遮罩绘制在其上方 时,原生标题栏仍然截获指针。

规则:整窗遮罩必须 inset: 0 铺满窗口,并由 styles.css 中统一的遮罩选择器声明 -webkit-app-region: no-drag。新增遮罩时扩展该共享选择器;不要在各自规则里复制 no-drag,也不要用 inset: 38px 0 0 之类的偏移绕开标题栏。完整要求见 UI 设计系统。

防复发:WorkspacePrimitives.test.tsx 的 "releases the native drag region for every full-window overlay" 从 styles.css 自动发现 全部 position: fixed 遮罩并逐个校验,新增遗漏会直接失败。

历史:e9679fe、ca4a58c 各修了一部分被人工点到的弹窗,同一批改动又新增了两个未覆盖 的遮罩;后续复查改为共享规则加自动发现测试。

网页浮层遗漏原生浏览器视图

症状:知识库实例、项目设置等 Modal 的右侧被浏览器网页覆盖,通知或菜单也可能 被压在原生视图下面。

原因:WebContentsView 不参与 Renderer 的 CSS 层叠。先前只在关闭终端确认时 显式隐藏 viewport,没有覆盖其他页面的 Portal 和浮层。

规则:BrowserViewport 共用 browser-viewport-occlusion.ts,按 Modal 语义和 浮层相交状态释放显示租约,不销毁网页会话。不得再增加逐个弹窗的特殊隐藏条件。

防复发:组件回归覆盖 body Modal、嵌套 Modal、已打开 Modal、隐藏或非相交浮层、 通知、终端确认和非活动页签;实际 Electron 验证同时检查原生视图可见性,不能只看 DOM 的遮罩或截图。

历史:先前浏览器多实例修正只覆盖了终端关闭确认;2026-09-13 用户在知识库实例 Modal 再次复现,同一检查确认其他应用级 Modal 均缺少处理。

Flex 滚动内容压缩页签和分段控件

症状:知识库来源选择被长参数表单压成窄缝,标签和按钮文字被裁掉。

规则:PageTabs 与 SegmentedControl 在共享样式上固定 flex: 0 0 auto; 直接包装层也必须保留高度。不要按页面复制高度常量。

防复发:WorkspacePrimitives.test.tsx 检查共享分段控件不收缩,已有平台功能、 能力和 MCP 页签回归继续保留;Electron 复验必须包含长表单与短窗口。

历史:设置页签曾补过包装层不收缩规则;2026-09-13 知识库创建类型的共享 SegmentedControl 又出现同类问题。

逐行查询代替主键查询

症状:列表渲染或校验函数随数据量线性变慢,且慢在单次操作内重复发生。

原因:用 listAll().some(...) 或 listAll().find(...) 判断单行是否存在。表有主键或 唯一索引时,这是把 O(1) 查询写成了全表扫描加 JSON.parse。

规则:判断单行存在或取单行数据时,在 store 上提供按键查询方法(例如 hasBinding、 getBinding),不要在调用方过滤全量列表。确实需要按分组统计时,循环外取一次并构建 Map,不要在循环内反复查询。

历史:外部知识库绑定同时出现三处:requireLibrary(30+ 调用点,一处在循环内)、 getKnowledgeSnapshot(每个知识库两次全表扫描)、listInstances(每个实例过滤一次)。

生产路径复用“测试”函数

症状:正常用户操作触发了只应在配置校验时出现的错误码,或多发了一次网络请求。

原因:让生产路径直接调用为“测试连接”“保存前校验”准备的函数。这类函数通常带有额外的 能力预检和更严格的失败条件。

规则:生产路径与校验路径各自提供入口。校验入口可以在生产入口之上叠加预检,反向复用 不允许。共用的裁剪、归一化逻辑抽成独立函数由两者调用。

历史:对话检索曾调用 ExternalKnowledgeService.testRetrieval 并把用户问题放进 testQuery,导致每次提问多一次 detail 请求,且 EXTERNAL_KB_CAPABILITY_UNAVAILABLE 会出现在正常对话中。

事件迭代结束前释放底层订阅

症状:并行 OpenCode 会话已结束,但部分 SSE 响应流仍未关闭;只检查最终取消信号 会误判回收成功。

原因:OpenCode SDK 1.18.29 在迭代器 return() 时移除取消监听并释放 reader lock, 但不主动取消 reader。外层 for await 先结束 SDK 迭代器,外层 finally 再 abort 的 顺序过晚,带缓冲的响应流可能继续存活。聊天和原生 Compact 均曾使用该顺序。

规则:两条 SDK 消费路径共用 consumeEventSubscription,先 abort 对应订阅,再结束 底层迭代器;请求取消和 Server 生命周期仍保留各自作用域。不要修改依赖包、放宽回收 断言或关闭共享 Server 来代替单次订阅回收。

防复发:Runtime 回归检查正常结束和消费方提前返回时,底层迭代器清理前取消已经发生。 opencode-runtime-lifecycle.test.ts 使用真实二进制与 SDK,在 1、4、8 路并行、单请求 取消和原生 Compact 后检查打开的响应流归零,并验证并行请求不被误取消。

历史:b03c256 已增加请求级订阅取消;2026-09-13 候选检查仍复现 3 条响应流未关闭, 复查发现聊天与 Compact 都需要调整上述顺序。

快照比对代替取消

症状:为检测“操作期间配置被改动”而反复读回状态并 JSON.stringify 比较。

原因:把取消语义当成配置漂移检测来实现。桌面单用户场景下,改动配置的那次操作本身就能 取消在飞请求。

规则:用 AbortController 在配置变更点取消在飞请求,并让被取消的操作不写回任何结果。 不要引入前后快照比较、版本化内部凭据或多阶段状态机。唯一性约束交给数据库索引。

历史:外部知识库服务曾有 5 处快照比对与专用错误码 EXTERNAL_KB_CONFIG_CHANGED;全部 删除后行为反而更正确,因为旧实现会把“用户停用实例”记成 probeStatus: 'failed'。

删除疑似死代码时的验证顺序

症状:删除后类型检查通过,但运行时行为改变,或测试断言的是已不存在的行为。

规则:删除枚举取值、文案键、导出函数前,按此顺序确认:

  1. rg 全仓搜索引用,区分生产代码与测试 fixture。
  2. 确认该取值是否可能由生产代码写入。仅测试 fixture 使用即为死代码。
  3. 检查是否属于已持久化数据的解析契约。持久化契约即使当前不写入也必须保留。
  4. 删除后跑受影响的定向测试,再跑 npm test。

历史:probeStatus 的 'healthy' 仅存在于测试 fixture,可以删除;而 externalKnowledgeLocatorSchema 的 sourceUrl 虽然同样从不写入,但它解析已持久化的引用 数据,必须保留。两者表面症状相同,处置相反。

渲染期派生值未记忆化

症状:流式响应期间界面卡顿,或大列表操作延迟。

原因:在组件体内直接构建 Map/Set 或链式 filter/flatMap,而该组件在流式增量期间 每个 token 都会重渲染。

规则:组件体内的派生集合必须包在 useMemo 中,依赖收窄到真正影响结果的值。紧邻的 useMemo 之后又裸建集合,会让记忆化收益减半。

注意:React 允许在渲染期用守卫调用 setState 来响应 props 变化,这是官方推荐做法,不要 把它改写成 useEffect。仓库 lint 规则 react-hooks/set-state-in-effect 会拒绝后者。