Files
goodbuddy/docs/quality/recurring-defects.md
T
mesalogo 925d410247
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
release: prepare Desktop 0.15.5
Document incremental Smart heartbeat reviews, evidence-backed suggestions, interrupted-turn history, floating checklists, and note save behavior. Record schema 50 backup requirements and real Host cancellation validation; Agent remains 0.15.1.

Release note: 智能心跳按变化回顾并提供有依据的建议,修复取消后续聊、清单展开和笔记保存体验;升级前请备份数据库及笔记。
2026-09-30 11:23:45 +08:00

21 KiB
Raw Blame History

复发缺陷与防复发规则

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

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

普通结构升级再次触发历史回收

症状:历史已经转换完成,后续升级仍显示清理历史并执行耗时的全库回收。

原因:只修复启动检测不够;普通旧 schema 仍会进入升级 Worker,若共享升级方法 把任意空闲页当作回收依据,就会在以后每次结构升级时重现。

规则:共享启动决定分别描述内容转换与回收,普通结构升级默认不回收。只有具体 旧内容转换允许回收,同次启动重试沿用内存决定;下一次启动重新检测,不增加持久标记。 具体规则见迁移指南。

防复发:schema 45/46 的真实 SQLite fixture 保留正常写入产生的空闲页,断言 无历史转换、无 VACUUM、内容与完整性保留;真实 Worker 验证取消及重试, 启动调度测试分别覆盖允许和禁止回收的决定在重试中不变。

历史:8dce1d7 和 38d0923 先后修正启动检测,后者随 0.13.9 发布; 2026-09-26 检查 0.13.15 的 schema 46→47 路径发现共享 Worker 仍无条件按空闲页回收。 本次修复在共享操作层分离结构升级与回收,而非给 46→47 增加特例。

异步界面测试只等待部分状态

症状:本地通过的界面测试在完整 CI 中找不到预期控件,或在前一步更新后立即读取到旧状态。

规则:复用 Testing Library 的 act、waitFor 和现有 deferred Promise, 按请求完成和最终可见状态同步,不以另一个元素出现或消失代替完整操作结束。 测试请求重试时显式控制后一次响应,分别断言等待中和响应完成后的状态; 同一异步操作的多个最终断言放入同一个 waitFor。不要只延长全局超时或放宽断言。 Electron 原生拖拽测试应确认窗口焦点、拖拽状态建立、宽度更新及拖拽结束后再断言; 移动事件明确携带左键按下状态,不用固定睡眠代替输入处理完成。

历史:2026-09-26 检查 Desktop 0.13.14 标签 CI 36165805814 与 main CI 36164698647:原生能力重试用例在启动状态未稳定时打开选项, 浏览器关闭用例只等待页签消失便同步断言错误已清除。两处测试改用上述已有同步机制; 是否消除 CI 失败以修复提交的 Linux CI 结果为准。 同日 Windows 全量测试及独立复跑中,Magic Notes 原生拖拽分别停在索引列 168 和 AI 列 280。测试同步原生输入与实际状态后独立复跑通过,保留原有宽度和持久化断言。 2026-09-27 的 0.15.0 主分支 CI 中,会话采集测试向隐藏 Electron 窗口发送 Escape, 未返回笔记列表。原生键盘辅助函数同样必须显示窗口、取得焦点并等待 document.hasFocus(),再发送输入;不靠跳过 Linux 或关闭沙箱处理。 随后 Linux CI 与共享 Host 的实测表明窗口焦点并不足够:缩窄窗口会收起工作栏, 隐藏面板仍有非零宽度,Escape 实际发给工作栏开关。测试须等待响应式状态完成, 按真实入口重新打开工作栏,并确认目标输入框取得焦点,再测试键盘返回和导航。 验证输入框自动聚焦时,Electron 测试窗口也应从开始即显示并取得焦点,不要到后续 键盘阶段才显示;失败信息应包含实际焦点、输入框禁用/可见状态及保存状态。 取消边界测试不使用 setTimeout(0) 猜测异步 I/O 的完成次序。Continue 启动前取消 用例改在既有启动环境回调处触发中止,仍断言未启动子进程及临时配置清理。 2026-09-28 完整候选 CI 又暴露两处同类等待缺陷:采集用例以 activeElement.textContent 包含消息文字判断导航完成,body 也会满足,随后真正的 消息聚焦会关闭刚打开的菜单。应同时断言焦点元素是消息 article,不只匹配后代文本。 原生 Runtime 控件用例须等当前项目的能力请求及启动渲染完成,再打开失焦关闭的选项菜单。 2026-09-29 的 0.15.4 候选 CI 中,问答轮次用例仅等“发送”按钮存在便点击; Runtime 状态尚未匹配当前选择时按钮仍禁用,点击不会发出请求。发送前应以现有 waitFor 等待按钮可用,而不是将按钮存在等同于 Runtime 就绪。保留问答保存、 跳过与重载的全部断言,不延长全局超时。 2026-09-30 发布验证中,连续新建会话用例只等 Runtime mock 收到请求,尚未等 已用会话渲染完成就触发新建。沿用 waitFor 等待已发送用户消息可见,再保持 两次同批新建及最终数量断言;mock 调用不代表界面状态已提交。

整窗遮罩与原生拖动区域

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

原因:.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" 递归扫描 Renderer CSS,按 position: fixed 与 inset 声明发现遮罩,排除不接收指针的透明宿主,并校验共享 no-drag。

历史:e9679fe、ca4a58c 各修了一部分被人工点到的弹窗,同一批改动又新增了两个未覆盖 的遮罩;后续复查改为共享规则加自动发现测试。 2026-09-27 复查发现自动发现仍依赖 backdrop/modal 后缀,漏掉知识引用的 .knowledge-citation-dialog。补入共享规则并移除后缀限制;真实 App 的 Windows WM_NCHITTEST 回归验证修复后为 HTCLIENT,临时清除该规则的对照为 HTCAPTION。

共享控件样式依赖页面祖先

症状:放弃笔记草稿弹窗中的危险按钮显示为原生按钮,其他页面的危险确认也缺少样式。

原因:唯一的 .danger-solid 定义限定在 .magic-notes-page 下;Portal 挂到 body 后失去该祖先,心跳、监督者、工作区文件删除和工具环境设置也无法命中。

规则:共享按钮在全局定义并复用基础控件规则,不向每个调用处补页面类。 具体状态和令牌见 UI 设计系统。

防复发:magic-notes-capture.electron.test.ts 在真实 App 中触发草稿确认,检查 按钮不属于页面祖先时的浅深主题 computed style、尺寸、字体、禁用及原生 Tab 焦点; 悬停通过 Chromium 调试协议强制伪状态检查,不作为原生鼠标命中验证。 共享令牌测试检查默认和悬停危险背景上的文字对比度。

历史:2026-09-27 同次检查确认草稿确认及上述四处调用均受同一作用域错误影响。

同日后续全局审查发现,控件已有全局定义仍会被容器与状态规则覆盖:深色通用输入规则替换 控件边框,普通图标 hover 覆盖危险图标,菜单按钮规则覆盖危险确认;.field input 还将 Radio 与 Switch 当成文本字段。共享规则现已排除二元控件,恢复全局焦点轮廓,降低菜单 默认样式特异性,并移除深色控件兜底覆盖。Continue 独立字段通过 .field-control 复用 输入规则。shared-controls.electron.test.ts 检查浅深主题、设置容器、body Portal、菜单 组合、默认/悬停/禁用/选中态及原生 Tab 焦点,并渲染实际 Continue 组件。令牌测试同时检查 主按钮 hover 白字与控件背景的对比度。审查修复记录 保留本次范围、验证结果和未测项。

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

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

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

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

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

历史:先前浏览器多实例修正只覆盖了终端关闭确认;2026-09-13 用户在知识库实例 Modal 再次复现,同一检查确认其他应用级 Modal 均缺少处理。 2026-09-27 同行帮助的 tooltip 仍未进入选择器。现已纳入同一相交判定和尺寸观察; Electron 回归从运行记录页打开真实 InlineHelp,并通过工作栏分隔条形成相交与非相交布局, 检查实际 WebContentsView 可见性及恢复后的页面标记,确认网页没有重建或刷新。

Modal 内的菜单与通知被遮挡或隔离

症状:设置中的“测试解析”菜单落在遮罩后面;设置、应用中心等全局 Modal 打开时, 应用通知被遮挡,关闭按钮也无法点击或聚焦。

原因:菜单挂在 body,却使用低于 Modal 的层级;通知留在被 Modal 设为 inert 的应用壳层内,同时层级也低于 Modal。单独增大 z-index 无法解除模态隔离。

规则:菜单与通知共用 FloatingPortal。稳定的非模态 Popover 宿主挂入所属/当前 Modal,兼顾 top layer 绘制、辅助技术的模态归属及焦点循环;没有 Modal 时才挂在 body。保留背景隔离,不在页面各自加层级常量,不因移动通知重置其计时。

防复发:FloatingPortal.test.tsx 覆盖宿主移动和内容不重挂载; overlay-layering.electron.test.ts 使用真实组件、Portal、鼠标命中及原生键盘,覆盖 解析菜单/二级结果、设置、应用中心、本机推理监控和原生 dialog,以及浅深主题和短窗口。

历史:2026-09-19 同次检查确认解析测试菜单和应用级通知两处挂载机制错误,后者影响 多个全局 Modal;统一宿主后补完整 App 的真实保存通知与 TXT 解析复验。

2026-09-27 在真实 App 的“设置 → 能力与工具 → MCP → 自定义 MCP → 添加 Server” 再次复现子层遮挡:MCP body Portal 沿用 z-index: 70,低于设置的 130,输入框中心 实际命中父设置页签。编辑器仅手动聚焦,父设置未设为 inert。添加与编辑统一接入 --z-dialog 和 activateModalFocus;遮罩关闭阻止默认鼠标焦点转移,Escape 停在子层。 新增 Electron 回归检查真实鼠标命中、输入、Tab 循环、各关闭入口、生产 IPC 保存及重读, 并检查浏览器在父子 Modal 期间隐藏、最后恢复同一未刷新的网页。子 Modal 应使用共享模态 机制;上述 FloatingPortal 仍用于非模态菜单与通知。 同次审查确认解析结果打开图片时也沿用低于父层的 z-index: 80,现复用 --z-dialog。 解析正文使用稳定的组件与图片回调,避免 App 打开大图时重建触发节点而丢失返回焦点。 窄侧栏打开项目 Modal 也已接入共享背景隔离,避免一次 Escape 同时关闭两层。 本阶段实现、键盘边界和 Electron 证据见浮层审查修复记录。

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(每个实例过滤一次)。

将累计过程当作增量事件反复落库

症状:聊天正文很小,执行记录数据库却增长到几十 GiB;只增加几个字节的子任务输出, 又写入接近 1 MiB 的旧过程副本。

原因:本机与远程 OpenCode 共用完整进度快照,两个桌面落库入口都按流式频率新增 完整 JSON;正文流的缓冲器没有覆盖这种累计快照。

规则:界面快照不直接作为追加式日志的存储表示。子任务事件通过共享存储层记录变化, 保留逐条还原和远程来源去重;会话仍保存完整显示结果。旧用户数据通过同一编码器无损 转换后回收空间,不靠删聊天或单独修本机数据库解决。

防复发:使用含大工具输出的多次更新检查总存储量,并验证重启、重复远程事件、 消息事务回滚及旧库中断续跑。工具对象须包含生产路径中的 undefined 可选字段, 经过真实 JSON 往返后仍识别为无变化,不能按对象键存在性比较。真实模型验证须包含 子代理正常使用工具、修改文件及运行测试,不能只测禁用工具的文本流。 真实旧库需验证逐条等价和物理文件缩小,已升级 schema 的重复数据同样要覆盖。

历史:2026-09-13 检查到 305,551 条子任务事件占用 90.11 GiB; 本机 appendTaskEvent 与远程 insertRemoteTaskEventOnce 同时存在重复保存路径。 0.13.1 发布后的真实工具任务再次复现:同批 928 条事件有 7,787 次重复工具块写入; 只统一 JSON 空字段后,事件负载从 10,467,622 降为 452,391 字节,逐条还原一致。

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

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

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

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

历史:对话检索曾调用 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 会拒绝后者。

解析警告被当作提取正文

症状:OCR 没有识别文字,但缺图警告让图片附件切成文字发送,或让仅图片结果被判为 可发送正文。

规则:HTTP OCR、显式图片提取及结果完整程度共用 hasExtractedDocumentText。 图片引用和解析器生成的缺图警告不计入正文;预览中的警告保留,不靠删除诊断掩盖失败。

防复发:真实 loopback HTTP 回归同时检查有/无识别文字、有效图片与缺图并存、 显式提取结果及保存后的完整程度;无正文时必须保留原图发送方式。

历史:2026-09-19 同次复验确认 DocumentParsingService.extractImage 与 parsedCompleteness 两处误计警告,HTTP Provider 自身的检查却正确,故统一判定函数。

列表刷新重建历史消息并触发重复保存

症状:焦点刷新后,正文完全未变,却再次序列化并保存全部会话消息;历史越多, 前端和 Main 的短时内存、主线程工作量越大。

规则:会话消息的图片状态使用同一保留引用的合并函数,三个合并分支都必须在 语义未变时返回选定原对象。列表、活动跳转、任务创建刷新都使用摘要或按 ID 读取, 不以完整历史列表作为单条查询。搜索只保留命中 ID;视图淘汰同时释放已确认保存的 详情引用,不把摘要误当成完整模型上下文或替换式保存输入。

防复发:App 回归检查 1,600 条历史刷新后保存消息数为 0;真实 SQLite 回归覆盖 未加载正文搜索、较早会话、501 条完整消息及 header-only 保存。另覆盖后台审批 晚于摘要请求到达、未打开会话复制、加载失败重试及恢复请求的问题投影。

历史:2026-09-15 在 f8d0741 引入的三个图片合并分支同时确认消息引用重建; 同次检查还发现活动跳转与任务创建继续整包读取历史,均改为共用的摘要/详情读取。