Skip to content

[#796][FIX] worktree-link-deps:补链 .bin + 入参规范化为绝对路径 + 末尾自检 - #802

Merged
jinjunnn merged 1 commit into
alphafrom
fix/796-worktree-link-deps
Aug 3, 2026
Merged

[#796][FIX] worktree-link-deps:补链 .bin + 入参规范化为绝对路径 + 末尾自检#802
jinjunnn merged 1 commit into
alphafrom
fix/796-worktree-link-deps

Conversation

@jinjunnn

@jinjunnn jinjunnn commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Fixes #796

大白话

scripts/worktree-link-deps.sh 有两个洞,都不报错,只是链少了 / 链坏了 ——
后果要到「几百条测试少跑一半」时才显形,而那时看着像主线红了

  1. .bin 整个漏链。 for entry in "$src"/* 的 glob 不匹配点开头的条目,.bin 正是点开头。
    ⇒ 新 worktree 里 electron-vite / electron-builder / tsgo 全部 command not found
  2. 传相对路径静默生成坏软链。 WT/MAIN 从未规范化,ln -sfn "$WT/…" 于是写出一个
    相对目标,而相对目标是相对软链自己所在的目录解析的。

改了什么(三处,scripts/worktree-link-deps.sh 一个文件)

1. .bin 补链 —— 点条目走白名单,不是 dotglob 全收

for entry in "$src"/* "$src"/.bin; do

主 checkout 里点开头的条目实测枚举(ls -a1 … | grep -a ^.,逐 dir 走了 41 个 node_modules):
.bin / .bun / .cache / .vite / .vite-temp / .ts-dist。逐个判:

条目 链不链 理由
.bin 票面 AC1。不链 = 任何 bin 脚本都跑不了
.bun 不链 bun 的隔离 store。顶层每个包的软链都从 $MAIN/node_modules 相对指进去,逐条链完即可解析 —— 实测本仓 worktree 一直如此跑通(3756 条测试全绿),链它是多余的
.cache .vite .vite-temp .ts-dist 不链 可写缓存。链过去 = 让 worktree 的写落进主 checkout,正是本文件抬头写的坑 ②

为什么是白名单而不是黑名单:黑名单对「将来新冒出来的点条目」默认放行 ⇒ 悄悄往主
checkout 写;白名单默认拒绝 ⇒ 最坏是「少了、末尾自检当场把条数打出来」。fail-closed。

实测这个决定是对的:在一次性 worktree 里跑了 bun run typechecknpm run build 之后,
packages/app/node_modules/.ts-distworktree 里新建(mtime Aug 2 22:05),
主 checkout 的那份没被碰(mtime 仍是 Jul 26 04:18)。链了它就不是这个结果。

2. 相对路径 —— 规范化成绝对物理路径(而不是响亮拒绝)

WT="$(cd "$WT" && pwd -P)"
…
MAIN="$(cd "$MAIN" && pwd -P)"

为什么选规范化而不是拒绝:①「拒绝」让所有习惯敲相对路径的人多一步,而修法只是一行
cd+pwd -P;②pwd -P 对本来就绝对的入参是恒等变换(实测两条真实路径均不变),
不改变任何既有调用的行为;③它顺手修掉了第二个更隐蔽的后果 —— $MAIN 若相对,
[[ "$target" == "$MAIN/packages/"* ]] 拿它跟 realpath 的输出比,后者恒为绝对物理路径
判定恒假,工作区自有包被静默指回主 checkout(那正是本文件存在的理由)。

顺带把 [ -d "$WT" ] 挪到 git -C "$WT" 之前:原顺序下传一个不存在的路径会先炸在 git 上,
拿不到脚本自己那句 worktree not found

3. 末尾自检(AC4)

打印 node_modules dirs: N, entries: M,并在两种情况下判红退出:
.bin 没链上、或任何新建的软链是坏的(ln 对坏链不报错,建完立刻 stat 一次)。

验证:每条判据都先证明它能测出已知的坏

一次性 worktree 建在仓内 .worktrees/,用完 git worktree remove,末尾 git worktree list 已确认干净。
全程未用 git stash。变异实验只发生在一次性 worktree 与 scratchpad 的脚本副本上,
主 checkout 与本特性分支的工作树全程未被改坏(避免污染别的 lane 的闸门)。

⚠️ 先说一个把人骗过去的发现:AC3 点名的探针本身是瞎的

.bin 完全缺失的树上实测:

$ bunx tsgo --version              → Version 7.0.0-dev.20251207.1   exit=0   ← 假绿
$ bun run typecheck   (= tsgo -b)  → exit=0                                  ← 假绿
$ npm exec --no -- tsgo --version  → Version 7.0.0-dev.20251207.1   exit=0   ← 假绿

bun / npm 都能从依赖的 package.json 的 bin 字段找到可执行文件,不只看 .bin
(对照:在一个完全没有 node_modules 的目录里 bunx tsgo 会去 registry 找并 404 ——
所以上面的绿确实来自本地解析,不是网络兜底。)

bunx tsgo --version 当判据会得到假绿。 真正能测出这个缺陷的探针见下。

正反两向:.bin(绝对路径,同一个一次性 worktree,先坏后好)

探针 修复前(HEAD 的脚本) 修复后
./node_modules/.bin/tsgo --version No such file or directory,exit 127 Version 7.0.0-dev.20251207.1,exit 0
./packages/ui-mac/node_modules/.bin/electron-builder --version No such file or directory,exit 127 26.15.2,exit 0
command -v electron-vite electron-builder tsgo(按包脚本 runner 的 PATH) 三个全找不到,exit 1 三条绝对路径全部命中,exit 0
npm run --ignore-scripts build(#783 的真实路径) sh: electron-vite: command not found / exit 127 electron-vite 真的跑起来了,不再有 command-not-found

修复前脚本的输出(注意它exit 0,自称成功):

linked: /Users/tide/app/alpha-code/.worktrees/tmp796-abs
  @opencode-ai/app → /Users/tide/app/alpha-code/.worktrees/tmp796-abs/packages/app
baseline exit=0
$ ls -a1 node_modules | grep ^.
(no dot entries at all)

修复后:

linked: /Users/tide/app/alpha-code/.worktrees/tmp796-abs
  node_modules dirs: 41, entries: 653
  @opencode-ai/app → /Users/tide/app/alpha-code/.worktrees/tmp796-abs/packages/app

npm run build(不跳 prebuild)在这台机器上会挂在 prebuildhttps://models.dev/api.json
UNKNOWN_CERTIFICATE_VERIFICATION_ERROR —— 是本机代理,与本 PR 无关,故用 --ignore-scripts 对照。

正反两向:相对路径(同一个一次性 worktree,先坏后好)

修复前,bash scripts/worktree-link-deps.sh .worktrees/tmp796-rel:

linked: .worktrees/tmp796-rel
  @opencode-ai/app → …/tmp796-rel/packages/ui-mac/node_modules/@opencode-ai/.worktrees/tmp796-rel/packages/app
script exit=0                       ← 自称成功

readlink packages/ui-mac/node_modules/@opencode-ai/app
  -> .worktrees/tmp796-rel/packages/app   [*** RELATIVE ***, *** BROKEN ***]

独立枚举: entries: 615  DANGLING: 80

修复后,同一条相对路径命令:

linked: /Users/tide/app/alpha-code/.worktrees/tmp796-rel
  node_modules dirs: 41, entries: 653

readlink packages/ui-mac/node_modules/@opencode-ai/app
  -> /Users/tide/app/alpha-code/.worktrees/tmp796-rel/packages/app          [ABS, EXISTS]
readlink packages/ui-mac/node_modules/@opencode-ai/session-ui
  -> /Users/tide/app/alpha-code/.worktrees/tmp796-rel/packages/session-ui   [ABS, EXISTS]

用户可观察的后果,同一棵 base 树(b11be8ba)、同一条命令 bun test src(packages/ui-mac):

修复前(相对路径):  2154 pass / 102 fail  → Ran 2256 tests across 255 files.  exit=1
修复后(相对路径):  3756 pass /   0 fail  → Ran 3756 tests across 255 files.  exit=0

——「测试跑得少 + 一堆莫名其妙的红」,而脚本 exit 0。这就是票里说的「看着像主线红了」。

自检的条数不是好看的数字,是跟独立枚举对上的数字

脚本自报 node_modules dirs: 41, entries: 653用另一套代码(python 直接走 MAIN 的目录树,
不复用脚本任何输出)独立数:

independent: node_modules dirs = 41  linkable entries = 653      ← 逐字对上
independent: entries present in worktree = 655  dangling = 0
集合差 (worktree − MAIN) = 空                    ← 没有多链任何东西
集合差 (MAIN − worktree) = 恰好那 7 个刻意跳过的可写缓存
   node_modules/.bun, node_modules/.cache,
   packages/app/node_modules/.vite, packages/app/node_modules/.vite-temp,
   packages/desktop/node_modules/.ts-dist,
   packages/ui-mac/node_modules/.cache, packages/ui-mac/node_modules/.vite

自检本身不是空闸门 —— 两个分支各实施了一次绕过

A. 坏链分支:在一次性 worktree 里把 packages/session-ui 改名藏起来 → 重跑新脚本:

  ✗ 坏软链 …/packages/app/node_modules/@opencode-ai/session-ui -> …/packages/session-ui
  ✗ 坏软链 …/packages/enterprise/node_modules/@opencode-ai/session-ui -> …/packages/session-ui
  node_modules dirs: 41, entries: 653
  ✗ 2 条坏软链(见上)—— 依赖解析不完整,不要拿它跑测试
### script exit=1        ← 判红

B. .bin 分支:把修复那一行改回 for entry in "$src"/*(改的是 scratchpad 的副本,不动仓内文件),
先确认变异真的落地(grep73: for entry in "$src"/*; do)再跑:

  node_modules dirs: 41, entries: 615
  ✗ node_modules/.bin 没链上 —— worktree 里跑不了任何 bin 脚本(tsgo / electron-vite / electron-builder)
### mutant exit=1        ← 判红

653 − 615 = 38,恰等于独立数出来的「带 .bin 的 node_modules 目录数 = 38」。

本地门(真实输出,scripts/alpha-check.sh,跑在本分支的 worktree 上)

    ✓ zero upstream package edits
✓ 7343 个版本控制文件零字面 NUL 字节 (跳过 1147 个已登记二进制,18 个非文本条目)
    ✓ no literal NUL bytes
    ✓ typecheck
 54 pass / 0 fail   Ran 54 tests across 5 files.
 132 pass / 0 fail  Ran 132 tests across 10 files.
 3756 pass / 0 fail Ran 3756 tests across 255 files.
    ✓ tests
✅ all local gates green — safe to push
=== alpha-check exit=0 ===

pre-push 钩子在 git push 时也真的跑了同一套并放行(没有 --no-verify)。

与 base 的 fail-set 差 = 空:base(b11be8ba,干净一次性 worktree,正确链好)实测
3756 pass / 0 fail / 255 files,本分支同样 3756 / 0 / 255。北极星 diff 只有
scripts/worktree-link-deps.sh 一个文件,不碰任何 UPSTREAM_PATHS

同类扫描:scripts/ 下还有没有别的地方犯同一个 glob 漏点条目的错

两条独立检索轴,全仓(排除 node_modules/.git/.worktrees):

axis 1  for <var> in <path>/*        → 只有 worktree-link-deps.sh:36 与 :44
axis 2  dotglob / .[!.]* 的使用       → 全仓零命中(没有任何脚本处理过点条目)

:44(for scoped in "$entry"/*,遍历 @opencode-ai / @alpha-code 作用域目录内部)是同一类
的潜在成员
,但实测这两个作用域目录里当前没有任何点开头的条目,所以今天不是缺陷 ——
按票面「有就点名、不顺手改」,这里只点名。

另外点名一个不在本票范围内的既有缺口(同样不改):.bin 里唯一一条指回工作区自有包的链是
packages/web/node_modules/.bin/opencode → packages/opencode/bin/opencode,它会解析到主 checkout
packages/opencode。但这不是本 PR 引入的 —— 现有脚本对 @opencode-ai/@alpha-code
命名的自有包(如 packages/web/node_modules/opencode)本来就是整条指向主 checkout 的。
要不要收口是另一张票的事。

我主动没做的事

  • 没有为它新建测试框架。仓内的 scripts/gate-files.tsv 登记的是 bun 测试文件,
    这是个纯 dev 脚本、不在任何生产路径上;新建一个会真建 worktree 的测试是给这张窄票加一套设施。
    代替品是脚本自带的自检(每次跑都执行、fail-closed,且上面两个绕过实验证明它不是空闸门)。
  • 没有链 .bun,理由见上表(多余,且实测不链也全绿)。
  • 没有改 :44 那处同类 glob(今天不是缺陷,只点名)。
  • 没有收口 packages/web/node_modules/opencode 指向主 checkout 的既有缺口(超范围)。
  • 没有改 docs/verification/2026-08-02-req128-phase3-packaged-l2.md(它是当时的观测记录,
    是历史证据,不该被后来的修复回写)。
  • 没有合并

两个洞都**不报错**,只是链少了/链坏了 —— 后果要到几百条测试少跑一半时才显形。

1. `for entry in "$src"/*` 的 glob 不匹配点开头的条目,`.bin` 正是点开头 ⇒ 整个
   `.bin` 漏链,新 worktree 里 electron-vite / electron-builder / tsgo 全部
   command not found(#783 打包真机 L2 为此手工补链)。跑 bun 的门不需要 `.bin`,
   所以日常 typecheck / unit 全绿,洞一直藏着。
   修法:点条目按**白名单**补(只补 `.bin`)。`.bun` 无需(顶层逐条链完即可解析);
   `.cache`/`.vite`/`.vite-temp`/`.ts-dist` 是**可写缓存**,链过去等于让 worktree 的
   写落进主 checkout —— 正是本文件抬头说的坑 ②。白名单也是 fail-closed 的。

2. `WT`/`MAIN` 从未规范化 ⇒ 传相对路径时 `ln -sfn "$WT/…"` 写出**相对**软链目标,
   而相对目标相对**软链自己所在目录**解析,得到 `…/@opencode-ai/.worktrees/foo/…`
   这种鬼路径;`$MAIN` 若相对,自有包的前缀判定(与 realpath 的绝对输出比)还会恒假。
   修法:`cd … && pwd -P`,对本来就绝对的入参是恒等变换。

3. 末尾自检:打印「node_modules dirs / entries」,并对①`.bin` 没链上、②任何新建的
   坏软链 判红退出。两个分支都实测过能对已知的坏判红。

Fixes #796
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.

[BUG][P1] worktree-link-deps.sh 不链 .bin,且相对路径会静默生成坏软链 —— 后者让测试「跑得少」看着像红

1 participant