isep-doctor 判準改成「每一個 Claude Code 自己回報有沒有載入」,不再看設定長相(inkstone/ISEP#150 → c6995)

同一台 Mac 有 CLI 與 Desktop 內建兩個 Claude Code,讀同一份 ~/.claude 卻不一定同結論:
settings 多了 path 時 Desktop 2.1.266 忽略 inkstone marketplace、CLI 2.1.220 照常載入(c6991)。
第一版看到 path 就對整台判 ,在 CLI 上是假陽性。

- 逐一找 CLI(PATH)與 Desktop(最新版本,數字排序),各跑 plugin list --json,依 errors 各判各的
- 設定比對只在某一個真的報錯時當解釋+修法,永遠不單獨判 
- 查不了 ⇒ ◐、離開碼 2,不講成 
- test-isep-doctor.sh 離線 11 條(假 claude 可執行檔);舊版跑這份 pass=1 fail=10
- test-isep-doctor-live.sh 實機 3 條:真的 CLI 2.1.220/Desktop 2.1.266,隔離 HOME 重現有/無 path,不動 ~/.claude
- 盤點表與 plugin.json 描述的腳本數 62→65(這棵樹上實數)

版本號待總管定。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-13 16:23:40 +08:00
parent 8eb4c58d2d
commit fbc56b9e59
6 changed files with 284 additions and 87 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "isep", "name": "isep",
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:62 支機械閘(87 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、62 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。", "description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:62 支機械閘(87 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、65 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
"version": "0.30.1", "version": "0.30.1",
"keywords": [ "keywords": [
"inkstone", "inkstone",
+24
View File
@@ -1210,6 +1210,30 @@ claude -p '請執行 git tag -a v9.9.9 -m test'
> 為什麼挑 `release-tag-guard` 當考題:它**只存在於 ISEP**,舊的 `.claude/` 那份沒有。 > 為什麼挑 `release-tag-guard` 當考題:它**只存在於 ISEP**,舊的 `.claude/` 那份沒有。
> 用它才分得出「載到的是 plugin」還是「載到的是舊的」。 > 用它才分得出「載到的是 plugin」還是「載到的是舊的」。
### A8b — 這台**每一個** Claude Code 都真的載入了 ISEP:離線 11 條+實機 3 條
```
python3 scripts/isep-doctor # 看這台現況
bash scripts/test-isep-doctor.sh # 離線:假的 claude 可執行檔
bash scripts/test-isep-doctor-live.sh # 實機:真的 CLI 與 Desktop 內建那一份,隔離 HOME,不動 ~/.claude
```
**該看到**`isep-doctor` 逐一列出 CLI 與 Desktop,各一行 ✅;離線 `pass=11 fail=0`;實機 `pass=3 fail=0 skip=0`
**它在守什麼**`inkstone/ISEP#150`):plugin 沒載入時,**會講話的信標本身也沒載入**,
所以只能從 plugin 外面問。而同一台 Mac 有**不只一個** Claude Code,它們讀同一份 `~/.claude` 卻不一定同一個結論:
`extraKnownMarketplaces.inkstone.source` 多了 `path` 時,**Desktop 內建的 2.1.266 整個忽略 marketplace、CLI 2.1.220 照常載入**
c6991 兩份 debug log;本支實機測試在隔離 HOME 重現)。第一版 doctor 看設定長相就對整台判 ❌——在 CLI 上是假陽性。
⇒ 判準是**每一個 Claude Code 自己 `plugin list --json` 回報的 errors**;設定比對只當解釋,永遠不單獨判 ❌。
**失敗**
- ① 紅 ⇒ **誤攔**,又回到看設定長相判 ❌。鑑別力實跑過:拿 8eb4c58 那版跑這份測試是 `pass=1 fail=10`
大多是輸出格式不同;**真正分得出判準對錯的是 ①**——舊版在「CLI 實際載入」時照樣 exit 1 並點名 path
- ② 紅 ⇒ 分不出哪一個 surface 壞了,或又把整台機器講成同一個結論
- ④ 紅 ⇒ 別的原因失敗時編了一個 path 原因出來
- ⑥⑦ 紅 ⇒ 「查不了」被講成 ✅ 或 ❌(讀不到不等於沒載入)
- ⑧⑧b 紅 ⇒ 印出來的修法照著貼走不通
- ⑨ 紅 ⇒ Desktop 版本用字串排序,`2.1.99` 會被當成比 `2.1.266`
- 實機測試印 SKIP ⇒ 這台的版本不是 2.1.220/2.1.266,行為可能不同,那一格**沒驗過**
--- ---
## B. 只有 leo 能跑的(雲端) ## B. 只有 leo 能跑的(雲端)
+7
View File
@@ -49,6 +49,13 @@
> **62 支、87 條、62 支腳本是在這棵樹上當場數出來的**`ls hooks/*.sh | wc -l`62、 > **62 支、87 條、62 支腳本是在這棵樹上當場數出來的**`ls hooks/*.sh | wc -l`62、
> `grep -c '"command":' hooks/hooks.json`87、`ls -p scripts | grep -v / | wc -l`62)。 > `grep -c '"command":' hooks/hooks.json`87、`ls -p scripts | grep -v / | wc -l`62)。
> 📌 **`inkstone/ISEP#150`2026-09-13)+0 支、+0 條、腳本 62→65**:沒加閘。`scripts/isep-doctor` 從 plugin **外面**
> 問這台每一個 Claude CodeCLI、Desktop 內建那一份)有沒有真的載入 ISEP——plugin 沒載入時信標自己也講不出話。
> 判準是各自 `plugin list --json` 回報的 errors,不是設定長相(`path` 在 Desktop 2.1.266 會讓 marketplace 被忽略、CLI 2.1.220 不會)。
> 另加兩支測試:`test-isep-doctor.sh`(離線)、`test-isep-doctor-live.sh`(真的 Claude Code、隔離 HOME)。
> **62 支、87 條、65 支腳本是在這棵樹上當場數出來的**`ls hooks/*.sh | wc -l`62、
> `grep -c '"command":' hooks/hooks.json`87、`ls -p scripts | grep -v / | wc -l`65)。
> 📌 **`inkstone/ISEP#147`2026-09-07)+0 支、+1 條**`line-needs-own-worktree.sh` 多掛 PostToolUse `Agent|Task` > 📌 **`inkstone/ISEP#147`2026-09-07)+0 支、+1 條**`line-needs-own-worktree.sh` 多掛 PostToolUse `Agent|Task`
> ——教你開分身的那支閘,在線收工那一刻替你收(分支推了就 remove;沒推就點名分支與路徑,不靜默、不 force)。 > ——教你開分身的那支閘,在線收工那一刻替你收(分支推了就 remove;沒推就點名分支與路徑,不靜默、不 force)。
> 分身的家從「repo 旁邊」改成「repo 裡面的 `.worktrees/`」:leo 在 Finder 看到一排 `ISEP-wt-117``InkStoneCo-wt-112` > 分身的家從「repo 旁邊」改成「repo 裡面的 `.worktrees/`」:leo 在 Finder 看到一排 `ISEP-wt-117``InkStoneCo-wt-112`
+103 -53
View File
@@ -1,28 +1,38 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""isep-doctor — 這台機器 Claude Code 到底有沒有把 ISEP 載進來(inkstone/ISEP#150 """isep-doctor — 這台機器上每一個 Claude Code到底有沒有把 ISEP 載進來(inkstone/ISEP#150
為什麼要有這支: 為什麼要有這支:
plugin 載入失敗時,ISEP 的 59 支閘**一支都不會跑**,而且**沒有任何閘能講出來**—— plugin 載入失敗時,ISEP 的閘**一支都不會跑**,而且**沒有任何閘能講出來**——
講話的 isep-presence-beacon 本身就在那個沒載入的 plugin 裡。 講話的 isep-presence-beacon 本身就在那個沒載入的 plugin 裡。
2026-09-05 起 `~/.claude/settings.json` 的 `extraKnownMarketplaces.inkstone.source`
多了一個 `path`(本機 checkout 路徑),Claude Code 把它當成 git 來源的抓取欄位
debug log:「its network source differs from the one declared for it in settings
(… path …)」),整個 marketplace 被忽略 ⇒ `isep@inkstone` 報 "Marketplace inkstone not found"。
CLI 與 Desktop 同病,當時被誤判成「Desktop 不跑 hook」。
判準(狀態,不是措辭): 為什麼一台機器要分開問(09-13 總管複驗 c6991 推翻了第一版):
① `claude plugin list --json` 裡 isep@inkstone 帶 errors ⇒ ❌ 同一台 Mac 上有**不只一個** Claude CodeCLI(例:/opt/homebrew/bin/claude 2.1.220
② settings 對 inkstone 的宣告,比 known_marketplaces.json 記的多了抓取欄位 ⇒ 指出是哪一個、給修法 與 Claude Desktop 內建的那一份(~/Library/Application Support/Claude/claude-code/<版本>/…,例:2.1.266)。
都沒有 ⇒ ✅,離開碼 0 它們讀同一份 ~/.claude,**卻不一定得出同一個結論**:
settings.json 的 extraKnownMarketplaces.inkstone.source 多了 path 時——
2.1.266:整個 marketplace 被忽略 ⇒ isep@inkstone 報 "Marketplace inkstone not found"
2.1.220:照常載入
第一版只看「設定長得像會出事」就對整台機器判 ❌,在 2.1.220 上是假陽性。
離開碼:0 通|1 沒載入(附原因與一行修法)|2 查不了(找不到 claude 可執行檔等) 判準(實際狀態,不是設定長相):
對每一個找得到的 Claude Code 各跑一次 `plugin list --json`,看它**自己**回報的 isep@inkstone
沒有這一筆 ⇒ ❌ 沒安裝
帶 errors ⇒ ❌ 沒載入(照實轉述;若 settings 比已安裝的 marketplace 多了抓取欄位,附上原因與修法)
沒有 errors ⇒ ✅(即使設定裡有 path——那台實際載得進來就是載得進來)
跑不動/看不懂 ⇒ ◐ 查不了(讀不到不等於沒載入,也不等於有)
設定比對**只當解釋**,永遠不單獨判 ❌。
測試用環境變數:ISEP_DOCTOR_PLUGIN_LIST_JSON(直接給 plugin list 的輸出檔)、 離開碼:0 全部 ✅|1 至少一個 ❌|2 沒有 ❌、但至少一個查不了(或一個 Claude Code 都找不到)
ISEP_DOCTOR_CLAUDE_HOME(代替 ~/.claude)。迴歸測試:scripts/test-isep-doctor.sh
測試用環境變數:
ISEP_DOCTOR_CLAUDE_BINS 用這份清單取代自動尋找;以 os.pathsep 分隔,每項 `名稱=可執行檔路徑`
ISEP_DOCTOR_CLAUDE_HOME 代替 ~/.claude
迴歸測試:scripts/test-isep-doctor.sh(離線)/scripts/test-isep-doctor-live.sh(真的 Claude Code、隔離 HOME
""" """
import glob import glob
import json import json
import os import os
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
@@ -30,73 +40,113 @@ import sys
PLUGIN_ID = "isep@inkstone" PLUGIN_ID = "isep@inkstone"
MARKET = "inkstone" MARKET = "inkstone"
FETCH_FIELDS = ("path", "ref", "headers", "sparsePaths") FETCH_FIELDS = ("path", "ref", "headers", "sparsePaths")
DESKTOP_GLOB = "~/Library/Application Support/Claude/claude-code/*/claude.app/Contents/MacOS/claude"
home = os.environ.get("ISEP_DOCTOR_CLAUDE_HOME") or os.path.expanduser("~/.claude") home = os.environ.get("ISEP_DOCTOR_CLAUDE_HOME") or os.path.expanduser("~/.claude")
def find_claude(): def _ver_key(path):
c = shutil.which("claude") m = re.search(r"/claude-code/([0-9][0-9.]*)/", path)
if c: return tuple(int(x) for x in m.group(1).split(".") if x) if m else ()
return c
cands = sorted(glob.glob(os.path.expanduser(
"~/Library/Application Support/Claude/claude-code/*/claude.app/Contents/MacOS/claude")))
return cands[-1] if cands else None
def plugin_list(): def find_claudes():
f = os.environ.get("ISEP_DOCTOR_PLUGIN_LIST_JSON") """回傳 [(名稱, 路徑)]。Desktop 只取最新版本那一份(它啟動的就是那一份;舊資料夾可能殘留)。"""
if f: env = os.environ.get("ISEP_DOCTOR_CLAUDE_BINS")
return json.load(open(f)) if env is not None:
exe = find_claude() out = []
if not exe: for item in env.split(os.pathsep):
return None if "=" in item:
out = subprocess.run([exe, "plugin", "list", "--json"], capture_output=True, text=True, timeout=60) name, path = item.split("=", 1)
return json.loads(out.stdout) out.append((name, path))
return out
out = []
cli = shutil.which("claude")
if cli:
out.append(("CLI", cli))
desk = sorted(glob.glob(os.path.expanduser(DESKTOP_GLOB)), key=_ver_key) # 數字排序:2.1.99 < 2.1.266
if desk:
out.append(("Desktop", desk[-1]))
return out
def version_of(exe):
try:
r = subprocess.run([exe, "--version"], capture_output=True, text=True, timeout=30)
return (r.stdout.strip().split() or ["?"])[0]
except Exception: # noqa: BLE001
return "?"
def ask(exe):
"""回傳 (狀態, 內容)。狀態:'list' 內容=plugin list 陣列;'err' 內容=查不了的原因。"""
try:
r = subprocess.run([exe, "plugin", "list", "--json"], capture_output=True, text=True, timeout=90)
except Exception as e: # noqa: BLE001
return "err", f"執行失敗({e}"
try:
data = json.loads(r.stdout)
if not isinstance(data, list):
raise ValueError("不是陣列")
return "list", data
except Exception: # noqa: BLE001
tail = (r.stdout or r.stderr or "").strip().splitlines()[-1:] or ["(沒有輸出)"]
return "err", f"plugin list --json 看不懂(exit={r.returncode}{tail[0][:200]}"
def load(p): def load(p):
try: try:
return json.load(open(p)) return json.load(open(p))
except Exception: except Exception: # noqa: BLE001
return {} return {}
try:
plugins = plugin_list()
except Exception as e: # noqa: BLE001
print(f"◐ 查不了:claude plugin list 失敗({e}")
sys.exit(2)
if plugins is None:
print("◐ 查不了:找不到 claude 可執行檔")
sys.exit(2)
entry = next((p for p in plugins if p.get("id") == PLUGIN_ID), None)
declared = (load(os.path.join(home, "settings.json")).get("extraKnownMarketplaces") or {}).get(MARKET, {}).get("source") declared = (load(os.path.join(home, "settings.json")).get("extraKnownMarketplaces") or {}).get(MARKET, {}).get("source")
known = (load(os.path.join(home, "plugins", "known_marketplaces.json")).get(MARKET) or {}).get("source") known = (load(os.path.join(home, "plugins", "known_marketplaces.json")).get(MARKET) or {}).get("source")
extra = [] extra = []
if isinstance(declared, dict): if isinstance(declared, dict):
extra = [k for k in FETCH_FIELDS if k in declared and (not isinstance(known, dict) or declared.get(k) != known.get(k))] extra = [k for k in FETCH_FIELDS if k in declared and (not isinstance(known, dict) or declared.get(k) != known.get(k))]
claudes = find_claudes()
if not claudes:
print("◐ 查不了:這台找不到任何 Claude Code 可執行檔(CLI 與 Desktop 都沒有)")
sys.exit(2)
bad, unknown, extra_hit = [], [], False
for name, exe in claudes:
label = f"{name} {version_of(exe)}"
state, data = ask(exe)
if state == "err":
unknown.append(name)
print(f"◐ {label}:查不了——{data}(讀不到不等於沒載入)")
print(f" {exe}")
continue
entry = next((p for p in data if p.get("id") == PLUGIN_ID), None)
if entry is None: if entry is None:
print(f"❌ {PLUGIN_ID} 沒有安裝(這台的閘全部不存在)") bad.append(name)
print(f"❌ {label}{PLUGIN_ID} 沒有安裝——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有")
print(" 修法:claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git && claude plugin install isep@inkstone") print(" 修法:claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git && claude plugin install isep@inkstone")
sys.exit(1) continue
errors = entry.get("errors") or [] errors = entry.get("errors") or []
if not errors and not extra: if not errors:
print(f"✅ {PLUGIN_ID} {entry.get('version')} 已載入(enabled={entry.get('enabled')}") print(f"✅ {label}{PLUGIN_ID} {entry.get('version')} 已載入(enabled={entry.get('enabled')}")
sys.exit(0) continue
bad.append(name)
print(f"❌ {PLUGIN_ID} {entry.get('version')} 沒有載入——這台 Claude CodeCLI 與 Desktop 都一樣)一支 ISEP 閘都不會跑") print(f"❌ {label}{PLUGIN_ID} {entry.get('version')} 沒有載入——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有")
for e in errors: for e in errors:
print(f" Claude Code 回報:{e}") print(f" Claude Code 回報:{e}")
if extra: if extra:
print(f" 原因:~/.claude/settings.json 的 extraKnownMarketplaces.{MARKET}.source 多了 {', '.join(extra)}" extra_hit = True
"Claude Code 把它當 git 抓取欄位,跟已安裝的 marketplace 對不上,整個 marketplace 被忽略") print(f" 可能原因:~/.claude/settings.json 的 extraKnownMarketplaces.{MARKET}.source 多了 {', '.join(extra)}"
"這一版把它當 git 抓取欄位,跟已安裝的 marketplace 對不上,整個 marketplace 被忽略")
if extra_hit:
fields = ", ".join(repr(k) for k in extra) fields = ", ".join(repr(k) for k in extra)
print(" 修法(一行,改完重開 Claude DesktopCLI session):") print("修法(一行,只拿掉上面點名的欄位;改完完全關掉 Claude Desktop重開 CLI session 再跑一次本指令):")
print(f" python3 -c \"import json,os;p=os.path.expanduser('~/.claude/settings.json');d=json.load(open(p));" print(f" python3 -c \"import json,os;p=os.path.expanduser('~/.claude/settings.json');d=json.load(open(p));"
f"s=d['extraKnownMarketplaces']['{MARKET}']['source'];[s.pop(k,None) for k in ({fields},)];" f"s=d['extraKnownMarketplaces']['{MARKET}']['source'];[s.pop(k,None) for k in ({fields},)];"
"json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)\"") "json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)\"")
if bad:
sys.exit(1) sys.exit(1)
sys.exit(2 if unknown else 0)
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# 實測:scripts/isep-doctor 對**真的** Claude Code(這台的 CLI 與 Desktop 內建那一份)判得對不對(inkstone/ISEP#150
#
# 不動 ~/.claude:把 inkstone marketplace 與 isep 安裝紀錄複製到 mktemp 的隔離 HOME
# 分別放「有 path」「沒有 path」兩份 settings.json,用真的可執行檔跑 doctor。
# 會打網路:只有 Claude Code 自己可能匿名讀 git.uncle6.meGitea),不碰 GitHub、不寫任何遠端。
#
# 期望(09-13 在 CLI 2.1.220Desktop 2.1.266 上實測過的行為):
# 沒有 path ⇒ 兩個都 ✅,離開碼 0 (真陰性)
# 有 path、Desktop 2.1.266 ⇒ ❌ 並點名 path (真陽性)
# 有 path、CLI 2.1.220 ⇒ ✅,不因設定長相被判 ❌ (舊版 doctor 的假陽性)
# 版本不是這兩個時,該版本那一格印 SKIP(行為可能不同,不假裝驗過)。
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
R="$HOME/.claude/plugins"
[ -d "$R/cache/inkstone" ] && [ -d "$R/marketplaces/inkstone" ] || { echo "SKIP:這台沒有安裝 isep@inkstone,無從複製"; exit 0; }
CLI=$(command -v claude || true)
DESK=$(ls -d "$HOME/Library/Application Support/Claude/claude-code/"*/claude.app/Contents/MacOS/claude 2>/dev/null \
| python3 -c 'import sys,re;L=[l.strip() for l in sys.stdin if l.strip()];k=lambda p:tuple(int(x) for x in re.search(r"/claude-code/([0-9.]+)/",p).group(1).split(".") if x);print(sorted(L,key=k)[-1] if L else "")')
BINS=""; [ -n "$CLI" ] && BINS="CLI=$CLI"; [ -n "$DESK" ] && BINS="${BINS:+$BINS:}Desktop=$DESK"
[ -n "$BINS" ] || { echo "SKIP:找不到任何 Claude Code"; exit 0; }
vcli=$([ -n "$CLI" ] && "$CLI" --version | awk '{print $1}'); vdesk=$([ -n "$DESK" ] && "$DESK" --version | awk '{print $1}')
echo "CLI=${CLI:-} ${vcli:-}Desktop=${DESK:-} ${vdesk:-}"
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
C="$T/home/.claude"; mkdir -p "$C/plugins/cache" "$C/plugins/marketplaces"
cp -R "$R/cache/inkstone" "$C/plugins/cache/" && cp -R "$R/marketplaces/inkstone" "$C/plugins/marketplaces/"
python3 - "$R" "$C" <<'EOF'
import json, sys
R, C = sys.argv[1], sys.argv[2]
km = json.load(open(R + "/known_marketplaces.json"))
json.dump({"inkstone": json.loads(json.dumps(km["inkstone"]).replace(R, C + "/plugins"))}, open(C + "/plugins/known_marketplaces.json", "w"))
ip = json.load(open(R + "/installed_plugins.json"))
ip["plugins"] = {"isep@inkstone": ip["plugins"]["isep@inkstone"]}
json.dump(json.loads(json.dumps(ip).replace(R, C + "/plugins")), open(C + "/plugins/installed_plugins.json", "w"))
src = {"source": "git", "url": "https://git.uncle6.me/inkstone/ISEP.git"}
json.dump({"enabledPlugins": {"isep@inkstone": True}, "extraKnownMarketplaces": {"inkstone": {"source": src}}}, open(C + "/settings.nopath.json", "w"))
src["path"] = "/Users/youlinhsieh/Documents/tech_projects/ISEP"
json.dump({"enabledPlugins": {"isep@inkstone": True}, "extraKnownMarketplaces": {"inkstone": {"source": src}}}, open(C + "/settings.path.json", "w"))
EOF
cp -R "$C" "$T/pristine"
pass=0; fail=0; skip=0
doctor() { # settings 變體
rm -rf "$C" && cp -R "$T/pristine" "$C" && cp "$C/settings.$1.json" "$C/settings.json"
OUT=$(cd "$T" && env -u CLAUDE_CODE_CHILD_SESSION HOME="$T/home" ISEP_DOCTOR_CLAUDE_BINS="$BINS" python3 "$HERE/isep-doctor" 2>&1); CODE=$?
echo "── settings=$1exit=$CODE"; printf '%s\n' "$OUT" | sed 's/^/ /'
}
has() { printf '%s' "$OUT" | grep -q -- "$1"; }
judge() { if eval "$2"; then pass=$((pass+1)); echo "ok $1"; else fail=$((fail+1)); echo "FAIL $1"; fi; }
doctor nopath
judge "真陰性:沒有 path → 離開碼 0、沒有 ❌" '[ "$CODE" = 0 ] && ! has "❌"'
doctor path
if [ "$vdesk" = 2.1.266 ]; then
judge "真陽性:Desktop 2.1.266 有 path → ❌ 並點名 path、離開碼 1" '[ "$CODE" = 1 ] && has "❌ Desktop 2.1.266" && has "多了 path"'
else skip=$((skip+1)); echo "SKIP 真陽性:Desktop 是 ${vdesk:-},不是 2.1.266"; fi
if [ "$vcli" = 2.1.220 ]; then
judge "假陽性:CLI 2.1.220 有 path 但實際載入 → ✅" 'has "✅ CLI 2.1.220"'
else skip=$((skip+1)); echo "SKIP 假陽性:CLI 是 ${vcli:-},不是 2.1.220"; fi
echo "pass=$pass fail=$fail skip=$skip"; [ "$fail" = 0 ]
+80 -28
View File
@@ -1,43 +1,95 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# 迴歸測試:scripts/isep-doctorinkstone/ISEP#150 # 迴歸測試:scripts/isep-doctorinkstone/ISEP#150——全離線
# 用假的 claude 可執行檔(shell 腳本)代替真的 CLIDesktop,走的是 doctor 真正的 subprocess 那條路。
# 真的 Claude Code 版本的實測在 scripts/test-isep-doctor-live.sh。
set -u set -u
HERE="$(cd "$(dirname "$0")" && pwd)" HERE="$(cd "$(dirname "$0")" && pwd)"
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
pass=0; fail=0 pass=0; fail=0
check() { # name expected_exit grep_pattern URL=https://git.uncle6.me/inkstone/ISEP.git
local out code; out=$(ISEP_DOCTOR_CLAUDE_HOME="$T/home" ISEP_DOCTOR_PLUGIN_LIST_JSON="$T/list.json" python3 "$HERE/isep-doctor" 2>&1); code=$?
if [ "$code" = "$2" ] && printf '%s' "$out" | grep -q -- "$3"; then pass=$((pass+1)); echo "ok $1" mkfake() { # 路徑 版本 plugin-list 輸出(原樣印出)
else fail=$((fail+1)); echo "FAIL $1 (exit=$code)"; printf '%s\n' "$out" | sed 's/^/ /'; fi mkdir -p "$(dirname "$1")"
printf '%s' "$3" > "$1.out"
cat > "$1" <<EOF
#!/bin/sh
if [ "\$1" = "--version" ]; then echo "$2 (Claude Code)"; exit 0; fi
cat "$1.out"
EOF
chmod +x "$1"
} }
run() { OUT=$(ISEP_DOCTOR_CLAUDE_HOME="$T/home" python3 "$HERE/isep-doctor" 2>&1); CODE=$?; }
ok() { pass=$((pass+1)); echo "ok $1"; }
bad() { fail=$((fail+1)); echo "FAIL $1 (exit=$CODE)"; printf '%s\n' "$OUT" | sed 's/^/ /'; }
expect() { # 名稱 離開碼 要有的樣式(| 分隔,可空) 不准有的樣式(可空)
local p miss=""
[ "$CODE" = "$2" ] || miss="exit"
local IFS='|'
for p in $3; do printf '%s' "$OUT" | grep -q -- "$p" || miss="$miss 缺[$p]"; done
if [ -n "${4:-}" ] && printf '%s' "$OUT" | grep -q -- "$4"; then miss="$miss 多了[$4]"; fi
[ -z "$miss" ] && ok "$1" || { bad "$1$miss"; }
}
mkdir -p "$T/home/plugins" mkdir -p "$T/home/plugins"
echo '{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git"}}}' > "$T/home/plugins/known_marketplaces.json" echo "{\"inkstone\":{\"source\":{\"source\":\"git\",\"url\":\"$URL\"}}}" > "$T/home/plugins/known_marketplaces.json"
SET_PATH="{\"extraKnownMarketplaces\":{\"inkstone\":{\"source\":{\"source\":\"git\",\"url\":\"$URL\",\"path\":\"/Users/x/ISEP\"}}},\"model\":\"keep\"}"
SET_CLEAN="{\"extraKnownMarketplaces\":{\"inkstone\":{\"source\":{\"source\":\"git\",\"url\":\"$URL\"}}}}"
LOADED='[{"id":"isep@inkstone","version":"0.30.1","enabled":true}]'
NOTFOUND='[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Marketplace inkstone not found"]}]'
# 1. 09-13 實況:settings 多了 path、plugin list 報 Marketplace not found → 1點名 path # ① 假陽性(c6991):設定有 path,但這個 Claude Code 自己回報載入正常 → ✅、不准點名 path
echo '{"extraKnownMarketplaces":{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git","path":"/Users/x/ISEP"}}}}' > "$T/home/settings.json" echo "$SET_PATH" > "$T/home/settings.json"
echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Marketplace inkstone not found"]}]' > "$T/list.json" mkfake "$T/b/cli" 2.1.220 "$LOADED"
check "path 造成載入失敗" 1 "多了 path" export ISEP_DOCTOR_CLAUDE_BINS="CLI=$T/b/cli"; run
expect "① 有 path 但實際載入(2.1.220 形狀)→ ✅ 不點名 path" 0 "✅ CLI 2.1.220" "path"
# 2. 修好後:沒有 path、沒有 errors → 0 # ② 真陽性(c6990):同一份設定,CLI 載得進、Desktop 載不進 → 1;兩邊各講各的;點名 path;給修法
echo '{"extraKnownMarketplaces":{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git"}}}}' > "$T/home/settings.json" mkfake "$T/b/desk" 2.1.266 "$NOTFOUND"
echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true}]' > "$T/list.json" export ISEP_DOCTOR_CLAUDE_BINS="CLI=$T/b/cli:Desktop=$T/b/desk"; run
check "正常載入" 0 "已載入" expect "② CLI 載得進、Desktop 載不進 → 1,逐一講、點名 path" 1 "✅ CLI 2.1.220|❌ Desktop 2.1.266|Marketplace inkstone not found|多了 path|^ python3 "
printf '%s' "$OUT" | grep -q "CLI 與 Desktop 都一樣" && bad "② 不准再說 CLI 與 Desktop 都一樣" || ok "② 沒有把整台機器講成同一個結論"
# 3. 別的原因失敗(沒有多餘欄位)→ 1,照實轉述 Claude Code 的錯 # ③ 真陰性:拿掉 path,兩邊都載入 → 0
echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Plugin directory missing"]}]' > "$T/list.json" echo "$SET_CLEAN" > "$T/home/settings.json"
check "其他錯誤照實轉述" 1 "Plugin directory missing" mkfake "$T/b/desk" 2.1.266 "$LOADED"; run
expect "③ 沒有 path、兩邊都載入 → 0" 0 "✅ CLI 2.1.220|✅ Desktop 2.1.266" "❌"
# 4. 根本沒裝 → 1 # ④ 別的原因失敗(設定沒有多餘欄位)→ 1,照實轉述,不編原因、不給 path 修法
echo '[]' > "$T/list.json" mkfake "$T/b/desk" 2.1.266 '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Plugin directory missing"]}]'; run
check "沒安裝" 1 "沒有安裝" expect "④ 其他錯誤照實轉述、不編原因" 1 "❌ Desktop|Plugin directory missing" "修法(一行"
# 5. 修法那一行真的修得好(在假 HOME 上執行印出來的指令) # ⑤ 沒安裝 → 1
echo '{"extraKnownMarketplaces":{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git","path":"/Users/x/ISEP"}}},"model":"keep"}' > "$T/home/settings.json" mkfake "$T/b/desk" 2.1.266 '[]'; run
echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Marketplace inkstone not found"]}]' > "$T/list.json" expect "⑤ 沒安裝 → 1" 1 "❌ Desktop 2.1.266isep@inkstone 沒有安裝"
fixline=$(ISEP_DOCTOR_CLAUDE_HOME="$T/home" ISEP_DOCTOR_PLUGIN_LIST_JSON="$T/list.json" python3 "$HERE/isep-doctor" | grep '^ python3 ' | sed 's/^ //')
# ⑥ 查不了(輸出不是 JSON)+另一個正常 → 2,◐,不准講成 ❌ 或 ✅
mkfake "$T/b/desk" 2.1.266 'Error: something broke'; run
expect "⑥ 一個查不了、一個正常 → 2" 2 "◐ Desktop|讀不到不等於沒載入|✅ CLI" "❌"
# ⑦ 一個 Claude Code 都找不到 → 2
export ISEP_DOCTOR_CLAUDE_BINS=""; run
expect "⑦ 找不到任何 Claude Code → 2" 2 "◐ 查不了"
# ⑧ 修法那一行真的修得好(在假 HOME 上原封不動執行印給 leo 的那一行)
echo "$SET_PATH" > "$T/home/settings.json"
mkfake "$T/b/desk" 2.1.266 "$NOTFOUND"
export ISEP_DOCTOR_CLAUDE_BINS="Desktop=$T/b/desk"; run
fixline=$(printf '%s\n' "$OUT" | grep '^ python3 ' | sed 's/^ //')
mkdir -p "$T/fake/.claude" && cp "$T/home/settings.json" "$T/fake/.claude/settings.json" mkdir -p "$T/fake/.claude" && cp "$T/home/settings.json" "$T/fake/.claude/settings.json"
HOME="$T/fake" bash -c "$fixline" # 原封不動執行印給 leo 的那一行 HOME="$T/fake" bash -c "$fixline"
if python3 -c "import json,sys;d=json.load(open('$T/fake/.claude/settings.json'));s=d['extraKnownMarketplaces']['inkstone']['source'];sys.exit(0 if 'path' not in s and d['model']=='keep' and s['url'].endswith('ISEP.git') else 1)"; then if [ -n "$fixline" ] && python3 -c "import json,sys;d=json.load(open('$T/fake/.claude/settings.json'));s=d['extraKnownMarketplaces']['inkstone']['source'];sys.exit(0 if 'path' not in s and d['model']=='keep' and s['url'].endswith('ISEP.git') else 1)"; then
pass=$((pass+1)); echo "ok 修法一行拿掉 path、其他設定不動" ok "⑧ 修法一行拿掉 path、其他設定不動"
else fail=$((fail+1)); echo "FAIL 修法一行"; cat "$T/home/settings.json"; fi else fail=$((fail+1)); echo "FAIL 修法一行"; cat "$T/fake/.claude/settings.json"; fi
# ⑧b 修完再問一次(Desktop 改回報正常)→ 0:出路走得通
cp "$T/fake/.claude/settings.json" "$T/home/settings.json"; mkfake "$T/b/desk" 2.1.266 "$LOADED"; run
expect "⑧b 照修法修完再跑 → ✅" 0 "✅ Desktop" "多了 path"
# ⑨ 自動尋找:Desktop 取數字上最新的那一份(2.1.99 < 2.1.266,字串排序會選錯)
unset ISEP_DOCTOR_CLAUDE_BINS
D="$T/h9/Library/Application Support/Claude/claude-code"
mkfake "$D/2.1.99/claude.app/Contents/MacOS/claude" 2.1.99 "$NOTFOUND"
mkfake "$D/2.1.266/claude.app/Contents/MacOS/claude" 2.1.266 "$LOADED"
OUT=$(HOME="$T/h9" PATH="/usr/bin:/bin" ISEP_DOCTOR_CLAUDE_HOME="$T/home" /usr/bin/env python3 "$HERE/isep-doctor" 2>&1); CODE=$?
expect "⑨ Desktop 取最新版本(數字排序)" 0 "✅ Desktop 2.1.266" "2.1.99"
echo "pass=$pass fail=$fail"; [ "$fail" = 0 ] echo "pass=$pass fail=$fail"; [ "$fail" = 0 ]