7cacfdcf11
leo 2026-09-07:「一個 repo 只看得到一個資料夾」「禁止這樣產出一大堆資料夾」。 根因:line-needs-own-worktree.sh 教工人開在 repo 旁邊,「收工 remove」只寫在閘訊息裡、沒機制驗, 每條線留一份 ⇒ tech_projects/ 長出 ISEP-wt-117/InkStoneCo-wt-112…一排。規則在文字裡不在機器上。 - scripts/worktree(新):open/close/list/sweep 四個動詞。分身住 <repo>/.worktrees/<票 repo>-<N> (走 .git/info/exclude,不動被追蹤的檔);「推了沒」只有這一份判準(三態:有/沒有/問不到, 問不到不當成沒推也不收);sweep 預設只列不動,--apply 只收「推了且乾淨」,--skip 是操作者點名。 - hooks/line-needs-own-worktree.sh:擋下時印 `scripts/worktree open`(絕對路徑,貼了就跑); 同一支多掛 PostToolUse Agent|Task——從【工單】認出分身,推了收、沒推點名分支+路徑(exit 2), 不 force、不刪。+0 支、+1 條(61 支/86 條,當場數的)。 - docs/governance/worktree-placement.md(新)+ principles 一行:禁令與閘互指。 - 測試:line-needs-own-worktree.test.sh 77→110(G 群:閘印的 open 那行原樣跑、旁邊不多任何東西; H 群:PostToolUse 推了收/沒推點名/安靜的方向);scripts/test-worktree.sh 42 條(七個分身的 fixture, 名字照 leo 看到的那排)。舊閘跑新測試 90/110,紅的 20 條正是這次改到的行為。 - permissions-allow.json 加 worktree 四種形狀(出路在雲端被分類器擋就走不通)。 - README/plugin.json/hooks-inventory 的數字在這棵樹上實數:61 支、86 條、62 支腳本。 待總管定版(改了會被載入的東西)。沒碰 hooks/lib/mainline.py。 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0178ef1fGw3XeZtpN7LaZrm4
387 lines
25 KiB
Bash
Executable File
387 lines
25 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# line-needs-own-worktree.sh 的迴歸測試(inkstone/ISEP#109 → comment 5391)。
|
||
#
|
||
# **離線、不打網路、不花錢、每次結果一樣**——判準只有兩個,兩個都是 git 自己回答的:
|
||
# ①這條命令會不會移動 HEAD ②那個目錄是不是主工作目錄。所以不需要語意判官。
|
||
#
|
||
# 每個案例都自己開一顆真的 git repo(外加一份真的 linked worktree)。
|
||
#
|
||
# 三群,兩個方向都要有證據:
|
||
# A 群「不該擋」——總管、唯讀動作、還原檔案、已經在自己的 worktree 裡、逃生門、
|
||
# worktree add 本身(**這群紅了比漏擋嚴重**:擋掉它等於線動不了工)
|
||
# B 群「該擋」 ——線在共用目錄裡 checkout/switch/-b,含 -C 與複合命令
|
||
# C 群「訊息與副作用」——講得出目錄與指令、不要開在 /tmp、逃生門、
|
||
# 以及**真的把說謊的登記清掉**(驗收條件 7)
|
||
#
|
||
# 用法:hooks/tests/line-needs-own-worktree.test.sh [hook 路徑]
|
||
|
||
set -u
|
||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
HOOK="${1:-$(cd "$HERE/.." && pwd)/line-needs-own-worktree.sh}"
|
||
|
||
PASS=0; FAIL=0; N=0
|
||
ROOT=$(mktemp -d "${TMPDIR:-/tmp}/isep-wt.XXXXXX")
|
||
clean() { rm -rf "$ROOT"; }
|
||
trap clean EXIT
|
||
|
||
# 🔴 macOS 的 $TMPDIR 是 /var/folders/… 而 git 回的是解析過的 /private/var/…
|
||
# 測試比對前先把路徑解析掉,不然會拿「同一個目錄的兩種寫法」互比而假紅。
|
||
ROOT=$(cd "$ROOT" && pwd -P)
|
||
REPO="$ROOT/shared-repo"
|
||
mkdir -p "$REPO"
|
||
git -C "$REPO" init -q -b main
|
||
git -C "$REPO" config user.email t@t; git -C "$REPO" config user.name t
|
||
echo hi > "$REPO/a.txt"; git -C "$REPO" add -A; git -C "$REPO" commit -qm init
|
||
# G/H 群要驗「推了沒」——給共用 repo 一顆真的(本機 bare)遠端。離線,不打網路。
|
||
git init -q --bare "$ROOT/remote.git"
|
||
git -C "$REPO" remote add origin "$ROOT/remote.git"
|
||
git -C "$REPO" push -q origin main 2>/dev/null
|
||
git -C "$REPO" worktree add -q "$ROOT/own-wt" -b mine
|
||
OWN="$ROOT/own-wt"
|
||
PLAIN="$ROOT/not-a-repo"; mkdir -p "$PLAIN"
|
||
|
||
# 第二顆 repo:D 群要驗「認出的是哪一個」,一顆 repo 分不出對錯
|
||
REPO2="$ROOT/other-repo"
|
||
mkdir -p "$REPO2"
|
||
git -C "$REPO2" init -q -b main
|
||
git -C "$REPO2" config user.email t@t; git -C "$REPO2" config user.name t
|
||
echo hi > "$REPO2/b.txt"; git -C "$REPO2" add -A; git -C "$REPO2" commit -qm init
|
||
|
||
# payload <command> [cwd]
|
||
payload() {
|
||
python3 - "$1" "${2:-$REPO}" <<'PY'
|
||
import json, sys
|
||
print(json.dumps({"session_id": "S-WT", "hook_event_name": "PreToolUse", "cwd": sys.argv[2],
|
||
"tool_name": "Bash", "tool_input": {"command": sys.argv[1]}},
|
||
ensure_ascii=False))
|
||
PY
|
||
}
|
||
|
||
# t <期望 exit> <說明> <command> [cwd] [env 前綴]
|
||
t() {
|
||
local want="$1" desc="$2" cmd="$3" cwd="${4:-$REPO}" envs="${5:-CLAUDE_CODE_CHILD_SESSION=1}"
|
||
N=$((N+1))
|
||
local err rc
|
||
err=$(printf '%s' "$(payload "$cmd" "$cwd")" | env $envs bash "$HOOK" 2>&1 >/dev/null); rc=$?
|
||
LAST_ERR="$err"
|
||
if [ "$rc" -eq "$want" ]; then
|
||
printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ %s —— 期望 exit=%s,實得 exit=%s\n' "$desc" "$want" "$rc"
|
||
printf '%s\n' "$err" | sed -n '1,8p' | sed 's/^/ /'
|
||
FAIL=$((FAIL+1))
|
||
fi
|
||
}
|
||
has() { N=$((N+1)); if printf '%s' "$LAST_ERR" | grep -qF -- "$1"; then printf ' ✅ %s\n' "$2"; PASS=$((PASS+1)); else printf ' ❌ %s —— 找不到「%s」\n' "$2" "$1"; FAIL=$((FAIL+1)); fi; }
|
||
hasnt() { N=$((N+1)); if printf '%s' "$LAST_ERR" | grep -qF -- "$1"; then printf ' ❌ %s —— 出現了「%s」\n' "$2" "$1"; FAIL=$((FAIL+1)); else printf ' ✅ %s\n' "$2"; PASS=$((PASS+1)); fi; }
|
||
|
||
echo
|
||
echo "── A 群:不該擋(誤攔比漏擋嚴重——擋錯了線就動不了工)──"
|
||
|
||
t 0 "① 總管(不是線)在共用目錄切分支 ⇒ 不管(那是他自己的目錄)" \
|
||
"git checkout -b feat/x" "$REPO" "CLAUDE_CODE_CHILD_SESSION="
|
||
t 0 "② git status ⇒ 唯讀,不管" "git status"
|
||
t 0 "③ git log --oneline ⇒ 唯讀,不管" "git log --oneline -5"
|
||
t 0 "④ git branch -a ⇒ 唯讀,不管" "git branch -a"
|
||
t 0 "⑤ git checkout -- a.txt(還原檔案,不動 HEAD)⇒ 放行" "git checkout -- a.txt"
|
||
t 0 "⑥ git checkout HEAD -- a.txt ⇒ 放行" "git checkout HEAD -- a.txt"
|
||
t 0 "⑦ git restore a.txt ⇒ 放行(根本不是 checkout)" "git restore a.txt"
|
||
t 0 "⑧ git checkout -p ⇒ 互動式挑 hunk,不動 HEAD,放行" "git checkout -p"
|
||
t 0 "⑨ **已經在自己的 worktree 裡**切分支 ⇒ 放行(這是我們要它做的事)" \
|
||
"git checkout -b feat/inside" "$OWN"
|
||
t 0 "⑩ git worktree add ⇒ 放行(那是解法本身,不是病)" \
|
||
"git worktree add $ROOT/wt2 -b feat/y"
|
||
t 0 "⑪ 逃生門 WORKTREE_OK=1 ⇒ 放行" \
|
||
"git checkout -b feat/z" "$REPO" "CLAUDE_CODE_CHILD_SESSION=1 WORKTREE_OK=1"
|
||
t 0 "⑫ 不是 git 命令 ⇒ 不管" "npm test"
|
||
t 0 "⑬ 目錄根本不是 git repo ⇒ 不管" "git checkout -b feat/x" "$PLAIN"
|
||
t 0 "⑭ 命令裡有 checkout 這個字但不是 git 的(不看措辭,看動作)" \
|
||
"echo '別忘了 git checkout 回 main'"
|
||
t 0 "⑮ payload 壞掉 ⇒ 放行(不擋在不確定上)" ""
|
||
|
||
N=$((N+1))
|
||
brc=$(printf 'not json' | CLAUDE_CODE_CHILD_SESSION=1 bash "$HOOK" >/dev/null 2>&1; echo $?)
|
||
if [ "$brc" -eq 0 ]; then printf ' ✅ ⑯ 完全不是 JSON 的 payload ⇒ 放行\n'; PASS=$((PASS+1));
|
||
else printf ' ❌ ⑯ payload 壞掉卻擋下來了(exit=%s)\n' "$brc"; FAIL=$((FAIL+1)); fi
|
||
|
||
echo
|
||
echo "── B 群:該擋 ──"
|
||
|
||
t 2 "⑰ 線在共用目錄 git checkout -b ⇒ 擋" "git checkout -b feat/new"
|
||
t 2 "⑱ 線在共用目錄 git switch -c ⇒ 擋" "git switch -c feat/new2"
|
||
t 2 "⑲ 線在共用目錄 git checkout main ⇒ 擋(切回去也是切)" "git checkout main"
|
||
t 2 "⑳ git -C <共用目錄> checkout(人在別的地方)⇒ 擋" "git -C $REPO checkout -b feat/c" "$PLAIN"
|
||
t 2 "㉑ 複合命令 cd … && git checkout ⇒ 擋(一段一段拆)" "cd $REPO && git checkout -b feat/d"
|
||
t 2 "㉒ 前面掛 env 的 git checkout ⇒ 擋" "FOO=1 git checkout -b feat/e"
|
||
t 2 "㉓ git switch main ⇒ 擋" "git switch main"
|
||
|
||
echo
|
||
echo "── C 群:訊息與副作用 ──"
|
||
|
||
t 2 "㉔ 擋下來(下面幾條檢查這一次的訊息)" "git checkout -b feat/msg"
|
||
has "$REPO" "㉕ 說得出是哪個目錄"
|
||
has "scripts/worktree open" "㉖ 給得出開自己 worktree 的指令(scripts/worktree open,inkstone/ISEP#147)"
|
||
has "不要開在 /tmp" "㉗ 講得出不要開在 /tmp(08-29 實測:那些全 prunable)"
|
||
has "WORKTREE_OK=1" "㉘ 講得出逃生門"
|
||
has "scripts/worktree close" "㉙ 講得出收工要怎麼收(scripts/worktree close)"
|
||
hasnt '$(' "㉚ 訊息沒有被 shell 展開"
|
||
hasnt "worktree add $ROOT/shared-repo-wt-" "㉚b 不再教人開在 repo 旁邊(09-07 那排分身就是這行教出來的)"
|
||
has "$REPO/.worktrees/" "㉚c 教的是 repo 裡面的 .worktrees/"
|
||
|
||
# 驗收條件 7:說謊的登記真的被清掉
|
||
git -C "$REPO" worktree add -q "$ROOT/ghost" -b ghosted
|
||
rm -rf "$ROOT/ghost"
|
||
N=$((N+1))
|
||
if git -C "$REPO" worktree list 2>/dev/null | grep -q prunable; then
|
||
printf ' ✅ ㉛ 造出一筆「指向空氣」的登記(目錄砍掉、登記還在)\n'; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ ㉛ 造不出 prunable 的登記,下一條測不到東西\n'; FAIL=$((FAIL+1))
|
||
fi
|
||
t 2 "㉜ 這時候擋下來" "git checkout -b feat/prune"
|
||
has "指向空氣" "㉝ 訊息說得出清掉了幾筆說謊的登記"
|
||
N=$((N+1))
|
||
if git -C "$REPO" worktree list 2>/dev/null | grep -q prunable; then
|
||
printf ' ❌ ㉞ prune 沒有真的執行——登記簿還在說謊\n'; FAIL=$((FAIL+1))
|
||
else
|
||
printf ' ✅ ㉞ 說謊的登記真的被清掉了(prune 是收工動作,不是偶爾整理)\n'; PASS=$((PASS+1))
|
||
fi
|
||
N=$((N+1))
|
||
if [ -d "$OWN" ] && git -C "$REPO" worktree list | grep -q "own-wt"; then
|
||
printf ' ✅ ㉟ 而且還在的那份 worktree 一根寒毛都沒動\n'; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ ㉟ prune 把還在的 worktree 也弄掉了——那是災難\n'; FAIL=$((FAIL+1))
|
||
fi
|
||
|
||
echo
|
||
echo "── D 群:認得出是**哪一個** repo(inkstone/ISEP#109 → comment 5398)──"
|
||
# 為什麼這群非有不可:舊版一律拿 payload 的 cwd 當答案,於是
|
||
# `cd <別的 repo> && git checkout` 擋是擋對了,訊息卻指著 cwd 那個 repo。
|
||
# 🔴 照著那個訊息做的人,會在錯的 repo 開一份用不到的 worktree,
|
||
# 真正要隔離的那個沒開到——而他以為自己隔離好了。給錯下一步比不擋更糟。
|
||
|
||
t 2 "㊱ cd <另一個 repo> && git checkout ⇒ 擋(人站在 REPO)" \
|
||
"cd $REPO2 && git checkout -b feat/x" "$REPO"
|
||
has "$REPO2" "㊲ 認出的是 cd 進去的那個 repo"
|
||
hasnt "$REPO/" "㊳ **不是** cwd 那個 repo(舊版就是在這裡指錯)"
|
||
|
||
t 2 "㊴ git -C <另一個 repo> checkout ⇒ 擋" \
|
||
"git -C $REPO2 checkout -b feat/y" "$REPO"
|
||
has "$REPO2" "㊵ 認出的是 -C 指的那個 repo"
|
||
hasnt "$REPO/" "㊶ 不是 cwd 那個"
|
||
|
||
t 2 "㊷ cd A && git -C B checkout ⇒ 擋(兩個都在,誰贏?)" \
|
||
"cd $REPO2 && git -C $REPO checkout -b feat/z" "$PLAIN"
|
||
has "$REPO" "㊸ **-C 贏**——git 自己就是這個優先序(-C 蓋掉當下的 cwd)"
|
||
hasnt "$REPO2" "㊹ cd 的目標沒有贏"
|
||
|
||
t 2 "㊺ 相對路徑的 cd ⇒ 擋,而且接得對" \
|
||
"cd shared-repo && git checkout main" "$ROOT"
|
||
has "$REPO" "㊻ 相對 cd 接在 cwd 後面算出正確的 repo"
|
||
|
||
t 2 "㊼ cd A && git -C <相對> checkout ⇒ 相對的 -C 接在 cd **之後**算" \
|
||
"cd $ROOT && git -C shared-repo checkout main" "$PLAIN"
|
||
has "$REPO" "㊽ 不是接在 cwd 後面(接錯會指到 not-a-repo/shared-repo)"
|
||
|
||
t 2 "㊾ 從自己的 worktree cd 回共用目錄再切 ⇒ 擋" \
|
||
"cd ../shared-repo && git checkout main" "$OWN"
|
||
has "$REPO" "㊿ 認出的是走回去的那個共用目錄"
|
||
|
||
t 2 "(51) 子殼的 cd 不外洩:(cd 別處 && true) ; git checkout ⇒ 擋在 cwd 這個 repo" \
|
||
"(cd $REPO2 && true) ; git checkout -b feat/sub" "$REPO"
|
||
has "$REPO" "(52) 子殼裡的 cd 出來就不算數了(沿用 push_target_dir 已驗過的範圍化)"
|
||
|
||
# 🔴 已知邊界,寫成測試而不是寫成註解——註解會過期,測試紅了才知道它變了。
|
||
# `);` 中間沒空白時,shlex 的 punctuation_chars 會把它併成一顆 token,
|
||
# 整段就斷在那裡 ⇒ **漏擋**(不是指錯 repo,是完全沒看到)。
|
||
# 這個邊界是 push_target_dir 的 tokenize 層帶來的,兩道閘一起有;
|
||
# 要修得動那層,會連帶改到推 main 那道閘的偵測範圍 ⇒ 不在本票裡動(另報)。
|
||
t 0 "(53) 已知邊界:右括號與分號黏在一起 ⇒ 這道閘看不到(漏擋,不是誤攔)" \
|
||
"(cd $REPO2 && true); git checkout -b feat/glued" "$REPO"
|
||
|
||
echo
|
||
echo "── D 群反面:認不出來的時候**不准猜**(誤攔+指錯路,兩個一起犯)──"
|
||
|
||
t 0 "(54) cd 到不是 repo 的地方 ⇒ 放行(舊版會擋,還會指著 cwd 那個 repo)" \
|
||
"cd $PLAIN && git checkout -b feat/x" "$REPO"
|
||
t 0 "(55) cd 進**自己的 worktree** 再切 ⇒ 放行(那正是我們要它做的事)" \
|
||
"cd $OWN && git checkout -b feat/inside2" "$REPO"
|
||
t 0 "(56) cd 的目標是沒展開的變數 ⇒ 放行(猜錯會叫人去錯的 repo 開 worktree)" \
|
||
'cd $SOMEREPO && git checkout -b feat/x' "$REPO"
|
||
t 0 "(57) cd - ⇒ 放行(上一個目錄是什麼,tokenize 看不出來)" \
|
||
"cd - && git checkout -b feat/x" "$REPO"
|
||
t 0 "(58) cd 到別的 repo 但只是還原檔案 ⇒ 放行(不動 HEAD 的形狀照樣認得)" \
|
||
"cd $REPO2 && git checkout -- b.txt" "$REPO"
|
||
|
||
N=$((N+1))
|
||
if [ -f "$(cd "$HERE/.." && pwd)/lib/checkout_target_dir.py" ]; then
|
||
printf ' ✅ (59) 解析器跟閘住在一起(少了它,閘只會印出錯的下一步)\n'; PASS=$((PASS+1))
|
||
else
|
||
printf ' ❌ (59) lib/checkout_target_dir.py 不見了\n'; FAIL=$((FAIL+1))
|
||
fi
|
||
|
||
echo
|
||
echo "── E 群:出路要真的走得通(inkstone/ISEP#109 → comment 6587,2026-09-07 實撞)──"
|
||
# 為什麼這群非有不可:閘訊息教的出路是 `WORKTREE_OK=1 git -C … checkout …`,
|
||
# 而舊版讀的是 **hook 自己的環境變數**。PreToolUse hook 在指令執行之前跑,
|
||
# 指令字串裡的 `WORKTREE_OK=1` 是給 git 的前綴賦值,hook 的行程裡根本沒有它
|
||
# ⇒ 那條出路在 Claude Code 這個 harness 下永遠走不通(總管當天三次照貼三次被擋)。
|
||
# 🔴 這群的測資是「閘自己印出來的那一行」,原樣餵回去——驗的是出路跑不跑得動,
|
||
# 不是閘擋不擋(mistakes.md「閘擋對了,但它印出來的那條出路跑不動」)。
|
||
# 判準是**位置**不是字:那個賦值必須是「會移動 HEAD 的那條 git 指令」自己的前綴。
|
||
|
||
t 0 "(60) 指令字串裡帶 WORKTREE_OK=1 前綴、hook 的環境裡**沒有**它 ⇒ 放行" \
|
||
"WORKTREE_OK=1 git checkout -b feat/esc" "$REPO"
|
||
t 2 "(61) 先擋一次,拿它印出來的出路當下一條的測資" "git checkout main" "$REPO"
|
||
ESC_LINE=$(printf '%s' "$LAST_ERR" | grep -F 'WORKTREE_OK=1 git -C' | head -1 | sed 's/^[[:space:]]*//; s/<分支>/main/')
|
||
N=$((N+1))
|
||
if [ -n "$ESC_LINE" ]; then printf ' ✅ (62) 抽得出那一行:%s\n' "$ESC_LINE"; PASS=$((PASS+1))
|
||
else printf ' ❌ (62) 訊息裡抽不出 WORKTREE_OK=1 git -C … 那一行\n'; FAIL=$((FAIL+1)); ESC_LINE="WORKTREE_OK=1 git -C $REPO checkout main"; fi
|
||
t 0 "(63) 閘印出來的那一行**原樣餵回去**(只填分支、人站在別處)⇒ 放行——6587 那天走不通的就是這條" \
|
||
"$ESC_LINE" "$PLAIN"
|
||
t 2 "(64) 只是印了那個字:echo WORKTREE_OK=1; git checkout main ⇒ 照擋(看位置不看字)" \
|
||
"echo WORKTREE_OK=1; git checkout main"
|
||
t 2 "(65) 前綴掛在別的指令上:WORKTREE_OK=1 echo hi && git checkout main ⇒ 照擋" \
|
||
"WORKTREE_OK=1 echo hi && git checkout main"
|
||
t 2 "(66) 值不是 1:WORKTREE_OK=0 git checkout main ⇒ 照擋" \
|
||
"WORKTREE_OK=0 git checkout main"
|
||
t 2 "(67) 寫在 commit 訊息裡再切 ⇒ 照擋(引號裡的字不是前綴)" \
|
||
"git commit -qm 'WORKTREE_OK=1 git checkout main' ; git checkout main"
|
||
t 0 "(68) cd 進去再掛前綴:cd <repo> && WORKTREE_OK=1 git checkout main ⇒ 放行" \
|
||
"cd $REPO && WORKTREE_OK=1 git checkout main" "$PLAIN"
|
||
t 0 "(69) 在自己的 worktree 裡也掛了前綴 ⇒ 本來就放行,前綴不礙事" \
|
||
"WORKTREE_OK=1 git checkout -b feat/x2" "$OWN"
|
||
|
||
echo
|
||
echo "── F 群:inkstone/ISEP#125 驗收表 A–F(票上原文的六條,期望值照票)──"
|
||
# A 逃生門原文照打→0 B 環境變數裡真的有→0 C 文件裡只寫 'git checkout X'→0
|
||
# D 文件裡寫 'cd <目錄> && git checkout X'→0 E 同 D 角括號拿掉→0 F 真的要切→2(本體,退化成 0=閘廢了)
|
||
t 0 "(70) #125-A 逃生門原文照打:WORKTREE_OK=1 git -C <TOP> checkout main ⇒ 0" \
|
||
"WORKTREE_OK=1 git -C $REPO checkout main" "$PLAIN"
|
||
t 0 "(71) #125-B 同一句、WORKTREE_OK 真的在 hook 的環境裡 ⇒ 0(本來就是,不要弄壞)" \
|
||
"git -C $REPO checkout main" "$PLAIN" "CLAUDE_CODE_CHILD_SESSION=1 WORKTREE_OK=1"
|
||
DOC_C="cat > $ROOT/doc-c.md <<'EOF'
|
||
把 repo 複製一份,然後 git checkout X
|
||
EOF
|
||
echo written"
|
||
t 0 "(72) #125-C 文件裡只寫 'git checkout X' ⇒ 0" "$DOC_C"
|
||
DOC_D="cat > $ROOT/doc-d.md <<'EOF'
|
||
示範:cd <目錄> && git checkout X
|
||
EOF
|
||
echo written"
|
||
t 0 "(73) #125-D 文件裡寫 'cd <目錄> && git checkout X' ⇒ 0(寫文件不是執行;09-02 兩次實地命中)" "$DOC_D"
|
||
DOC_E="cat > $ROOT/doc-e.md <<'EOF'
|
||
示範:cd $REPO && git checkout feat/x
|
||
EOF
|
||
echo written"
|
||
t 0 "(74) #125-E 同 D、角括號拿掉、目錄是真的共用目錄 ⇒ 0" "$DOC_E"
|
||
t 2 "(75) #125-F 真的要切(對照組)⇒ 2 🔴 本體,退化成 0=閘廢了" "cd $REPO && git checkout feat/x" "$PLAIN"
|
||
# 🔴 已知邊界,寫成測試而不是註解(跟 (53) 同一層、同一個病):tokenize 層把**換行**當空白吞掉,
|
||
# 多行指令被併成一句 ⇒ `echo hi⏎git checkout main` 從第一版起就看不到(舊閘實測 exit=0,
|
||
# 同一句改成 `echo hi; git checkout main` 就擋)。剝 heredoc 沒有讓它變好也沒有變壞——
|
||
# body 剝掉之後留下的仍是「換行分隔的兩句」。要修得動那層,會連帶改到推 main 那道閘的偵測範圍
|
||
# ⇒ 不在本票裡動(另報)。這條紅了=那層被改了,回頭看 (53) 與推 main 的 10+19 條。
|
||
t 0 "(76) 已知邊界:heredoc 之後**換行**再真的切 ⇒ 這道閘看不到(漏擋,不是誤攔;(53) 同層)" \
|
||
"cat > $ROOT/doc-f.md <<'EOF'
|
||
只是文件
|
||
EOF
|
||
git checkout main"
|
||
t 2 "(77) 同一句換成分號:cat <<'EOF' … EOF; git checkout main ⇒ 照擋(剝掉的只有 body)" \
|
||
"cat > $ROOT/doc-g.md <<'EOF'
|
||
只是文件
|
||
EOF
|
||
; git checkout main"
|
||
|
||
echo
|
||
echo "── G 群:分身開在 repo 裡面,旁邊不多任何東西(inkstone/ISEP#147)──"
|
||
# 為什麼這群非有不可:閘訊息教的「開在 repo 旁邊」一份都沒掉、也一份都沒收,
|
||
# leo 在 Finder 看到 ISEP-wt-117/ISEP-wt115/InkStoneCo-wt-112 一排。
|
||
# 這群的測資是**閘自己印出來的那一行**(跟 E 群同一個原則):抽出來、只填票號與分支、真的跑,
|
||
# 然後看三件事實:分身在 .worktrees/ 底下、repo 旁邊的目錄清單一個都沒多、共用目錄 git status 乾淨。
|
||
BESIDE_BEFORE=$(ls -1 "$ROOT" | sort)
|
||
t 2 "(78) 先擋一次,拿它印出來的 open 那一行當測資" "git checkout -b feat/g" "$REPO"
|
||
OPEN_LINE=$(printf '%s' "$LAST_ERR" | grep -F 'scripts/worktree open' | head -1 | sed 's/^[[:space:]]*//; s|<owner/repo#票號>|inkstone/shared-repo#901|; s|<你的分支>|feat/901|')
|
||
N=$((N+1))
|
||
if [ -n "$OPEN_LINE" ]; then printf ' ✅ (79) 抽得出那一行:%s\n' "$OPEN_LINE"; PASS=$((PASS+1))
|
||
else printf ' ❌ (79) 訊息裡抽不出 scripts/worktree open 那一行\n'; FAIL=$((FAIL+1)); fi
|
||
t 0 "(80) 那一行原樣跑(線、人站在別處)⇒ 這道閘不擋它(open 不是 checkout)" "$OPEN_LINE" "$PLAIN"
|
||
N=$((N+1))
|
||
if (cd "$PLAIN" && eval "$OPEN_LINE" >/dev/null 2>&1) && [ -d "$REPO/.worktrees/shared-repo-901" ]; then
|
||
printf ' ✅ (80b) 真的跑:分身長在 %s/.worktrees/shared-repo-901\n' "$REPO"; PASS=$((PASS+1))
|
||
else printf ' ❌ (80b) 閘印的 open 那一行跑不動、或分身不在 .worktrees/ 底下\n'; FAIL=$((FAIL+1)); fi
|
||
N=$((N+1))
|
||
if [ "$(ls -1 "$ROOT" | sort)" = "$BESIDE_BEFORE" ]; then
|
||
printf ' ✅ (81) repo 旁邊的目錄清單一個都沒多(leo 在 Finder 只看得到一個資料夾)\n'; PASS=$((PASS+1))
|
||
else printf ' ❌ (81) repo 旁邊多了東西:%s\n' "$(comm -13 <(printf '%s\n' "$BESIDE_BEFORE") <(ls -1 "$ROOT" | sort) | tr '\n' ' ')"; FAIL=$((FAIL+1)); fi
|
||
N=$((N+1))
|
||
if [ -z "$(git -C "$REPO" status --porcelain 2>/dev/null)" ]; then
|
||
printf ' ✅ (82) 共用目錄 git status 乾淨(.worktrees/ 進了 .git/info/exclude,沒動被追蹤的檔)\n'; PASS=$((PASS+1))
|
||
else printf ' ❌ (82) 共用目錄髒了:%s\n' "$(git -C "$REPO" status --porcelain | head -3 | tr '\n' ' ')"; FAIL=$((FAIL+1)); fi
|
||
t 0 "(83) 在那份分身裡切分支 ⇒ 放行(它是自己的 worktree)" "git checkout -b feat/g2" "$REPO/.worktrees/shared-repo-901"
|
||
t 0 "(84) cd 進分身再切 ⇒ 放行" "cd $REPO/.worktrees/shared-repo-901 && git checkout feat/901" "$PLAIN"
|
||
# 同一行再跑一次要冪等(重派同一張票的線不該撞牆)
|
||
N=$((N+1))
|
||
if (cd "$PLAIN" && eval "$OPEN_LINE" >/dev/null 2>&1); then printf ' ✅ (85) open 冪等:同一張票再開一次不炸\n'; PASS=$((PASS+1))
|
||
else printf ' ❌ (85) 同一張票第二次 open 失敗\n'; FAIL=$((FAIL+1)); fi
|
||
|
||
echo
|
||
echo "── H 群:收工那一半(PostToolUse Agent|Task)——推了收掉、沒推點名、不靜默(inkstone/ISEP#147)──"
|
||
# 這裡跑的是**總管的 session**(CLAUDE_CODE_CHILD_SESSION 沒設),掃的範圍由 CLAUDE_PROJECT_DIR 給。
|
||
post() { # post <prompt> [tool_name]
|
||
python3 - "$1" "${2:-Agent}" "$ROOT" <<'PY'
|
||
import json, sys
|
||
print(json.dumps({"session_id": "S-WT", "hook_event_name": "PostToolUse", "cwd": sys.argv[3],
|
||
"tool_name": sys.argv[2], "tool_input": {"prompt": sys.argv[1]}, "tool_response": {}},
|
||
ensure_ascii=False))
|
||
PY
|
||
}
|
||
tp() { # tp <期望 exit> <說明> <prompt> [tool_name] [env]
|
||
local want="$1" desc="$2" prompt="$3" tool="${4:-Agent}" envs="${5:-CLAUDE_CODE_CHILD_SESSION= CLAUDE_PROJECT_DIR=$ROOT}"
|
||
N=$((N+1)); local err rc
|
||
err=$(printf '%s' "$(post "$prompt" "$tool")" | env $envs bash "$HOOK" 2>&1 >/dev/null); rc=$?
|
||
LAST_ERR="$err"
|
||
if [ "$rc" -eq "$want" ]; then printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1))
|
||
else printf ' ❌ %s —— 期望 exit=%s,實得 exit=%s\n' "$desc" "$want" "$rc"; printf '%s\n' "$err" | sed -n '1,8p' | sed 's/^/ /'; FAIL=$((FAIL+1)); fi
|
||
}
|
||
WT901="$REPO/.worktrees/shared-repo-901"
|
||
# (a) 沒推:分身裡多一筆 commit、沒 push
|
||
echo g > "$WT901/g.txt"; git -C "$WT901" add -A; git -C "$WT901" -c user.email=t@t -c user.name=t commit -qm g
|
||
tp 2 "(86) 線收工、分支沒推 ⇒ exit 2(點名,不靜默)" "【工單】inkstone/shared-repo#901"
|
||
has "feat/901" "(87) 點得出分支名"
|
||
has "$WT901" "(88) 點得出路徑"
|
||
has "push origin feat/901" "(89) 印得出推的指令"
|
||
has "worktree-placement.md" "(90) 指到規約"
|
||
N=$((N+1)); if [ -d "$WT901" ]; then printf ' ✅ (91) 沒推的分身一根寒毛都沒動(不刪沒推的 commit)\n'; PASS=$((PASS+1)); else printf ' ❌ (91) 沒推的分身被刪了——那是災難\n'; FAIL=$((FAIL+1)); fi
|
||
# (b) 推了但工作樹髒:有未追蹤檔
|
||
git -C "$WT901" push -q origin feat/901 2>/dev/null
|
||
echo tmp > "$WT901/untracked.txt"
|
||
tp 2 "(92) 推了、但分身裡有未追蹤檔 ⇒ exit 2 點名(git 自己也拒絕 remove)" "【工單】inkstone/shared-repo#901"
|
||
has "未 commit" "(93) 講得出是有東西沒交代"
|
||
N=$((N+1)); if [ -d "$WT901" ] && [ -f "$WT901/untracked.txt" ]; then printf ' ✅ (94) 髒的分身沒被動\n'; PASS=$((PASS+1)); else printf ' ❌ (94) 髒的分身被動了\n'; FAIL=$((FAIL+1)); fi
|
||
# (c) 推了且乾淨 ⇒ 收掉、安靜
|
||
rm -f "$WT901/untracked.txt"
|
||
tp 0 "(95) 推了且乾淨 ⇒ exit 0(收掉)" "【工單】inkstone/shared-repo#901"
|
||
N=$((N+1)); if [ ! -e "$WT901" ] && ! git -C "$REPO" worktree list | grep -q shared-repo-901; then printf ' ✅ (96) 分身真的收掉了(目錄與登記都沒了)\n'; PASS=$((PASS+1)); else printf ' ❌ (96) 分身還在\n'; FAIL=$((FAIL+1)); fi
|
||
N=$((N+1)); if git -C "$REPO" rev-parse --verify -q refs/heads/feat/901 >/dev/null; then printf ' ✅ (97) 分支本身還在(收的是分身,不是分支)\n'; PASS=$((PASS+1)); else printf ' ❌ (97) 分支被刪了\n'; FAIL=$((FAIL+1)); fi
|
||
N=$((N+1)); if [ -d "$OWN" ] && [ -d "$REPO2" ]; then printf ' ✅ (98) 別人的 worktree 與別的 repo 一根寒毛都沒動\n'; PASS=$((PASS+1)); else printf ' ❌ (98) 收錯東西了\n'; FAIL=$((FAIL+1)); fi
|
||
# (d) 安靜的方向(誤攔比漏擋嚴重)
|
||
tp 0 "(99) 同一張票再收一次(分身已不在)⇒ exit 0 安靜" "【工單】inkstone/shared-repo#901"
|
||
hasnt "沒有找到" "(100) 沒分身時不吵(--quiet)"
|
||
tp 0 "(101) 派工單沒有【工單】⇒ 不管" "幫我看一下 shared-repo#901 的狀況"
|
||
tp 0 "(102) 票的 repo 沒有任何分身 ⇒ 安靜" "【工單】inkstone/other-repo#5"
|
||
tp 0 "(103) PostToolUse 但工具不是 Agent/Task ⇒ 不管" "【工單】inkstone/shared-repo#901" "Bash"
|
||
# (e) 一張派工單兩張票:兩份分身各自收
|
||
(cd "$PLAIN" && python3 "$(cd "$HERE/../.." && pwd)/scripts/worktree" open inkstone/shared-repo#903 --repo "$REPO" >/dev/null 2>&1)
|
||
(cd "$PLAIN" && python3 "$(cd "$HERE/../.." && pwd)/scripts/worktree" open inkstone/shared-repo#904 --repo "$REPO" >/dev/null 2>&1)
|
||
echo k > "$REPO/.worktrees/shared-repo-904/k"; git -C "$REPO/.worktrees/shared-repo-904" add -A; git -C "$REPO/.worktrees/shared-repo-904" -c user.email=t@t -c user.name=t commit -qm k
|
||
tp 2 "(104) 兩張工單:903 推了(HEAD=main 已在遠端)收掉、904 沒推點名 ⇒ exit 2" "【工單】inkstone/shared-repo#903
|
||
【工單】inkstone/shared-repo#904"
|
||
N=$((N+1)); if [ ! -e "$REPO/.worktrees/shared-repo-903" ] && [ -d "$REPO/.worktrees/shared-repo-904" ]; then printf ' ✅ (105) 903 收了、904 留著\n'; PASS=$((PASS+1)); else printf ' ❌ (105) 903/904 處理錯:903=%s 904=%s\n' "$([ -e "$REPO/.worktrees/shared-repo-903" ] && echo 在 || echo 沒了)" "$([ -e "$REPO/.worktrees/shared-repo-904" ] && echo 在 || echo 沒了)"; FAIL=$((FAIL+1)); fi
|
||
has "shared-repo-904" "(106) 點名的是 904"
|
||
# (f) 總管 session 的環境變數不影響收工那一半
|
||
tp 2 "(107) 環境裡有 WORKTREE_OK=1 也照樣收工檢查(那是切分支的逃生門,不是收工的)" "【工單】inkstone/shared-repo#904" "Agent" "CLAUDE_CODE_CHILD_SESSION= CLAUDE_PROJECT_DIR=$ROOT WORKTREE_OK=1"
|
||
|
||
clean
|
||
echo
|
||
echo "══ $PASS/$N 通過${FAIL:+,$FAIL 個失敗} ══"
|
||
[ "$FAIL" -eq 0 ]
|