fix(native): 钉牢 files 接管泄漏的守护断言 + 记录两条不变量 - #785
Conversation
#777 已修掉根因(清理循环跳过 FILES_KIND、注册失败回滚已占用的 id),但它的两处 断言写在「活着的 id 集合」上——泄漏的 id 恰好构成同一个集合,未修复代码上同样 通过;且槽位失败判据按 key 命中,而一个描述符的两个槽共用一个 key,所以 registerSlots 的「部分回滚」分支从未被执行。 - tests/native-registration.spec.ts:断言改写到注册表的事件日志(孤儿 id 会让键 集合看起来完全正常);槽位失败判据改为 name::key,从而可以只让某个描述符的 第二个槽失败、真正走到部分回滚;补 reportFailure 的 phase 断言。实测:在 v0.22.0 源码上 4/4 红,在修复后的源码上 4/4 绿。 - src/client/native/index.ts:给 disposeSafely 补注释,说明释放失败只写 console.error、不弹诊断条的理由(两个调用点都在 teardown 路径上;真被占住的 id 会在下一次注册尝试里经 reportFailure 显式报出)。 - AGENTS.md:§3.4 第 9 条补上这两条不变量(含真机日志实证的链接),§6 把 native-registration.spec.ts 纳入关键测试守护。 - docs/plans/2026-09-28-native-files-takeover-reload-leak.md:事故/证据/否决方案 记录(含「吸收」方案为何被否)。 验证:pnpm typecheck / pnpm lint 通过;pnpm test 122 files / 1292 passed / 9 skipped。
在 scratch profile 上用**已发布的 npm 0.22.0** 复现了线上那条报错链:页面内客户端
条目替换(宿主 HMR 的 500ms stat-poll 发现已安装 lib/client.js 的 rev 变化 →
teardown → 重新 import)期间,旧激活的 teardown 触发 sync(),让 files 接管在
inactive ctx 上重建 —— 控制台依次出现
native register files error: ... cannot create effect on inactive context
registerSlots ← registerFilesKind ← sync ← notify
native register files error: ... tab type id "dsh-better-sidebar:files" is already registered
register ← registerFilesKind ← sync ← notify
新用例 tests/e2e/native-reload.e2e.ts 把这条路径固化进挂载 lane(同一 lane 里
mount.e2e.ts 跑完后跑):utimesSync 已安装的 client bundle → 断言宿主元素被替换
(新激活)、无 native register … error、无诊断条、files 接管重新可用。
CLIENT_JS 从 e2e-mount.sh 已 export 的 DSH_HOME 推导,无需改脚本。
实测(同一台机器、同一份用例):
- npm dsh-better-sidebar@0.22.0:3/3 红(每次都是上面那两条栈)
- 本地打包的修复版:3/3 绿
AGENTS.md §2 补一条说明该门与触发方式。
追加:部署级回归门(真机红→绿实测)新增 触发方式: 断言:宿主元素被替换(说明新激活起来了)→ 无 实测(同一台机器、同一份用例、同一份 profile 形状)
这补上了单元用例测不到的一段:单测用假 ctx 模拟 inactive,只有真机能证明「宿主 HMR 的条目替换路径确实会走到那个窗口」。 |
独立 review(对照 606339f / 4099700 静态复核 + 日志逐行核对)提出的问题,逐条修正: 1. **补回被我自己抹掉的源码注释**:上次验证「未修复代码上 4/4 红」时用了 `git checkout 606339f -- src/client/native/index.ts`,随后 `git checkout HEAD -- <同一文件>` 把**尚未提交**的 `disposeSafely` 注释一起还原掉了 —— PR 描述里承诺的 「说明释放失败为何只走 console.error」因此在 diff 里根本不存在。已重新落盘。 2. **事故文档三处事实修正**:同一毫秒的 `already registered` 是 **7 条**(.339 一条 + .340 六条,合计 burst 9 条),不是 6;验证段的「114 files / 1136 passed」是修复分支 (base 早于 #781)的数字,补上口径并给出合入 main 后的 122/1292;`index.ts:353` 标注 为 v0.22.0 行号(main 上因 #777 顺移)。 3. **撤回一处措辞过头**:「部分回滚分支从未被执行」→ 回收集合虽然进了、但 disposer 列表恒为空(释放动作未执行),改后能真正覆盖。 4. **e2e 门不再可能静默通过**:`test.skip(CLIENT_JS === undefined, …)` 改成硬失败—— 这个门的价值就在于 harness 以后不再导出 DSH_HOME 时必须报错而不是变成 skip。 同时把「无 `[data-sidebar-right-unavailable]`」移到文件渲染断言**之后**(前者在空 pane 上恒真,不是真空但也没重量)。 5. **测试替身补齐保真度**:假注册表的 disposer 改为幂等(真宿主的 disposer 是幂等的 ——cordis effect 二次释放返回同一个 task),避免「二次 release 又被记一笔」这种与 真实宿主不一致的行为被后续用例当成事实。 6. **AGENTS §2 的 HMR 机制加版本限定**:`mtime/ctime/size` 的 sha1 rev 是**钉住的宿主 0.1.7-rc.1** 的行为;DSH 源码树新版本改成只比 `mtime+size` 并对内容取 sha1,pin 上调 时该触发器必须同步复核(否则会红在替换断言上,而不是静默失效)。 验证:typecheck / lint 通过;pnpm test 122 files / 1292 passed / 9 skipped(重启前机器上有 两个 scratch dsh 服务时的 fs-watch 超时是负载抖动,单独跑 5/5 绿、清空负载后全量绿)。
独立 review 结论已全部处理(commit
|
| # | review 发现 | 处理 |
|---|---|---|
| 1 | PR 声称的 disposeSafely 注释在 diff 里不存在(git diff --name-only 4099700 85b09d8 -- src/ 为空) |
真因是我自己的验证步骤干的:验证「未修复代码上 4/4 红」时跑了 git checkout 606339f -- src/client/native/index.ts,之后 git checkout HEAD -- 同文件 把尚未提交的注释一并还原。已重新落盘,现在 src/ 有且只有这一处注释改动 |
| 2 | 事故文档:同一毫秒的 already registered 是 7 条不是 6 |
已改(并说明 burst 合计 9 条 = 7 个内置描述符注销 → 7 次 notify) |
| 3 | 事故文档:#766 当时仍 OPEN(「已关闭」是错的) | 评论后已关闭,现为真 |
| 4 | 验证段 114 files / 1136 passed 与任何修订都对不上 |
补口径:那是修复分支(base 早于 #781)的数字;合入 main 后为 122 files / 1292 passed / 9 skipped |
| 5 | 行号未标注修订(index.ts:353 是 v0.22.0 的) |
已标注,并说明 main 上因 #777 顺移 |
| 6 | 「部分回滚分支从未被执行」措辞过头(进去过,但 disposer 列表恒空) | 已改成准确表述 |
| 7 | test.skip(CLIENT_JS === undefined) 有「静默通过」风险 |
改成硬失败:这个门的价值就在于 harness 以后不再导出 DSH_HOME 时必须报错 |
| 8 | [data-sidebar-right-unavailable] 断言无前置条件(弱) |
移到「文件真的渲染出来」断言之后 |
| 9 | 假注册表 disposer 非幂等(真宿主是幂等的) | 已改为幂等,避免二次 release 被记一笔这种与宿主不一致的行为被后续用例当真 |
| 10 | AGENTS §2 的 HMR 机制句没有版本限定 | 已限定为钉住的宿主 0.1.7-rc.1,并写明 DSH 源码树新版本改成内容 sha1、pin 上调时该触发器必须复核 |
reviewer 同时确认(作为本 PR 的正面证据):4 个单元用例在 606339f 上全部会红(逐条给出断言行号)、name::key 改写确实走到了部分回滚的释放路径、新 e2e 门在 CI 的 plugin-mount 里真实执行且绿(native-reload.e2e.ts:67:1 … 8 passed)、以及事故文档引用的日志行 desktop.frontdesk.log:2370 栈形状逐字吻合。
补充说明两处 reviewer 无法执行的部分(由我这边实测):npm 0.22.0 上 3/3 红、修复版 3/3 绿(scratch profile + 真实 dsh web + Chromium,见上一条评论),以及本机 pnpm typecheck / lint / test 全绿(122/1292/9)。
现在等 CI 重跑。
|
以维护者身份用 |
- 单测计数:`122 files / 1292 passed / 9 skipped` → **1293**。1292 是 #785 当时的数字, 本 commit 自己的 `make check` 两次都报 1293(`tests/` 与 main 逐字相同,122 个 spec + `it.each` 展开 = 1293)。涉及 CHANGELOG.md / CHANGELOG_EN.md / README.md / README_EN.md 与事故文档。 - 「3/3 红 / 3/3 绿」措辞改为「连续 3 次运行全红 / 全绿」并注明该 lane 里只有 1 个用例、 是重复跑三次——原措辞会被读成「3 个用例各红一次」。涉及上述四份发布文档 + 事故文档 + AGENTS.md §2(同一句话在那边也有)。 - 其余 review 结论均已满足:版本三处锁步 + 两个守护用例绿、无遗留 0.22.0 引用、 zh/en 段落一一对应、「最近更新」保持两条并把指针下移为 v0.21.1 → v0.12.3、 release.yml 的 tag 校验与 `latest` dist-tag 都能过、lockfile 无需改动。
- package.json / dsh.plugin.json / SIDEBAR_SERVICE_VERSION → 0.22.1(三者锁步由 tests/service.spec.ts 与 tests/manifest-consistency.spec.ts 断言) - README / README_EN:新增 v0.22.1 条目、同步三处徽章与 latest/支持线表格、 切回 npm 通道的示例版本号;「最近更新」保持两条(v0.22.1 + v0.22.0), 更早版本指针区间随之下移为 v0.21.1 → v0.12.3 - docs/external-plugin-guide.md:当前版本行 → v0.22.1 - CHANGELOG / CHANGELOG_EN:补 v0.22.1 条目(两个缺陷的根因链、真机证据、 修复要点与守卫清单) 支持线**不变**:仅 DSH 0.1.7-rc.1+(peer 下限 ^0.1.7-rc.1,CI 钉 0.1.7-rc.1); 0.21.1 / 0.22.0 用户直接升级;0.1.6-alpha.2 及更早仍固定 v0.19.1。 内容提要:① 修复客户端条目替换后 `dsh-better-sidebar:files` id 被孤儿化 (omdsh-dev#777 + omdsh-dev#785:清理循环跳过 FILES_KIND + 注册失败回滚已占用的 id 与槽位, 并新增部署级回归门 tests/e2e/native-reload.e2e.ts);② 修复 macOS 桌面版 窗口拖拽/双击缩放失效(omdsh-dev#773 + omdsh-dev#786:视口层用 initial !important 退出 app-region 计算,补齐放大视图,探针改用真实级联计算值)。 验证:make check 全绿(typecheck / lint / build / test 122 files 1293 passed 9 skipped / check:consumer-types);此外本地 pnpm test:mount 与 pnpm test:mount:aggregate 绿,npm 0.22.0 与修复版在 scratch profile 上的 红→绿对照见 PR omdsh-dev#785 / omdsh-dev#786。
背景
#777(已合并)修掉了
dsh-better-sidebar:filesid 被孤儿化的根因,但它带的两处断言在未修复的代码上同样会通过,等于没有钉住「失败后仍能重新注册」这条性质;同时registerSlots的「部分回滚」分支从未被执行。本 PR 只做守护与记录,不改运行时行为。改动
tests/native-registration.spec.tskey改为name::key。原判据下两个槽位共享同一个key,因此只可能失败第一个槽,registerSlots的部分回滚分支永远走不到;现在可以让某个描述符的第二个槽失败,并断言第一个槽的 disposer 确实被释放。reportFailure的 phase 断言(register git/register files/ …),把「可见诊断」契约一起钉住。src/client/native/index.ts换回 v0.22.0(606339f)的版本 → 4/4 红;换回修复后的版本 → 4/4 绿。src/client/native/index.ts:disposeSafely补一段注释,说明释放失败只写console.error、不走reportFailure(不弹诊断条)的理由——两个调用点都在 teardown 路径上,弹条是噪音;而被真正占住的 id 不会隐藏,它会在下一次注册尝试里经reportFailure显式报出。无行为改动。AGENTS.md:§3.4 第 9 条补「原生注册账本的两条不变量」(清理循环必须跳过FILES_KIND;注册成功一半是可达状态,因此失败必须回滚已占用的 id 与已建槽位),并附真机日志实证链接;§6 把native-registration.spec.ts纳入关键测试守护。docs/plans/2026-09-28-native-files-takeover-reload-leak.md:事故记录——现象、含确认栈的真机日志(desktop.frontdesk.log15:51:39.338)、完整根因链、否决 fix(native): absorb a duplicate tab-type id instead of reportingalready registered#766「吸收」方案的理由、以及仍存在的跨激活瞬时重复(有意不做)。验证
pnpm typecheckpnpm lintpnpm test