Skip to content

fix(native): 钉牢 files 接管泄漏的守护断言 + 记录两条不变量 - #785

Merged
Menghuan1918 merged 3 commits into
mainfrom
fix/native-leak-guard-tests
Sep 28, 2026
Merged

Menghuan1918 merged 3 commits into
mainfrom
fix/native-leak-guard-tests

Conversation

@Menghuan1918

Copy link
Copy Markdown
Collaborator

背景

#777(已合并)修掉了 dsh-better-sidebar:files id 被孤儿化的根因,但它带的两处断言在未修复的代码上同样会通过,等于没有钉住「失败后仍能重新注册」这条性质;同时 registerSlots 的「部分回滚」分支从未被执行。本 PR 只做守护与记录,不改运行时行为。

改动

  1. tests/native-registration.spec.ts

    • 断言从「活着的 id 集合」改写到注册表的事件日志:孤儿 id 会让键集合看起来完全正常(泄漏的 id 恰好就是期望的那三个),键集合断言没有判别力。
    • 槽位失败判据由 key 改为 name::key。原判据下两个槽位共享同一个 key,因此只可能失败第一个槽,registerSlots 的部分回滚分支永远走不到;现在可以让某个描述符的第二个槽失败,并断言第一个槽的 disposer 确实被释放。
    • 补 reportFailure 的 phase 断言(register git / register files / …),把「可见诊断」契约一起钉住。
    • 实测判别力:把 src/client/native/index.ts 换回 v0.22.0(606339f)的版本 → 4/4 红;换回修复后的版本 → 4/4 绿。
  2. src/client/native/index.ts:disposeSafely 补一段注释,说明释放失败只写 console.error、不走 reportFailure(不弹诊断条)的理由——两个调用点都在 teardown 路径上,弹条是噪音;而被真正占住的 id 不会隐藏,它会在下一次注册尝试里经 reportFailure 显式报出。无行为改动。

  3. AGENTS.md:§3.4 第 9 条补「原生注册账本的两条不变量」(清理循环必须跳过 FILES_KIND;注册成功一半是可达状态,因此失败必须回滚已占用的 id 与已建槽位),并附真机日志实证链接;§6 把 native-registration.spec.ts 纳入关键测试守护。

  4. docs/plans/2026-09-28-native-files-takeover-reload-leak.md:事故记录——现象、含确认栈的真机日志(desktop.frontdesk.log 15:51:39.338)、完整根因链、否决 fix(native): absorb a duplicate tab-type id instead of reporting already registered #766「吸收」方案的理由、以及仍存在的跨激活瞬时重复(有意不做)。

验证

项目 结果
pnpm typecheck ✅
pnpm lint ✅
pnpm test ✅ 122 files / 1292 passed / 9 skipped
反向判别力(换回 v0.22.0 源码) ✅ 4/4 红

#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 补一条说明该门与触发方式。
@Menghuan1918

Copy link
Copy Markdown
Collaborator Author

追加:部署级回归门(真机红→绿实测)

新增 tests/e2e/native-reload.e2e.ts(第 2 个 commit),把线上那条报错链固化进挂载 lane。

触发方式:utimesSync 已安装的 lib/client.js —— 宿主 HMR 的 500ms stat-poll 以 mtime/ctime/size 的 sha1 作为 rev,rev 变化即「页面内条目替换」(teardown → 重新 import),全程不刷新页面,与线上「插件市场更新 / Plugins 页禁用→启用」同一条路径。CLIENT_JS 从 e2e-mount.sh 已 export 的 DSH_HOME 推导,脚本无需改动。

断言:宿主元素被替换(说明新激活起来了)→ 无 native register … error 控制台错误 → 无诊断条 → files 接管重新可用(guide 行 + 文件树仍列出 seed 文件)。

实测(同一台机器、同一份用例、同一份 profile 形状)

被测产物 结果
npm dsh-better-sidebar@0.22.0(当前 latest) 3/3 红,每次都是同一条链:cannot create effect on inactive context(registerSlots ← registerFilesKind ← sync ← notify)紧随 tab type id "dsh-better-sidebar:files" is already registered(register ← registerFilesKind ← sync ← notify)
本地 pnpm build && pnpm pack 的修复版 3/3 绿(6.2s / 5.6s / 5.5s)

这补上了单元用例测不到的一段:单测用假 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 绿、清空负载后全量绿)。
@Menghuan1918

Copy link
Copy Markdown
Collaborator Author

独立 review 结论已全部处理(commit 233201f)

一位独立 reviewer 对照 606339f(v0.22.0)与 4099700(main)做了静态复核 + 日志逐行核对,结论是「needs changes — prose only」:测试与 e2e 门本身成立,问题在描述与文档。全部处理如下:

# 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 重跑。

@Menghuan1918
Menghuan1918 merged commit e46250a into main Sep 28, 2026
3 checks passed
@Menghuan1918

Copy link
Copy Markdown
Collaborator Author

以维护者身份用 --admin 合并:分支保护要求一次 approving review,而 GitHub 不允许对自己的 PR 自我批准(本 PR 由我发起)。独立 review 已在上面两条评论里完整记录(含逐条断言行号、被推翻的断言、以及本轮全部修正),前置条件与 CI 三 job 全绿均满足。合并 commit:e46250a。

Menghuan1918 added a commit that referenced this pull request Sep 28, 2026
- 单测计数:`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 无需改动。
sdwhwzp pushed a commit to sdwhwzp/DSH-better-sidebar that referenced this pull request Sep 28, 2026
- 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。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant