# ISEP 測試手冊 > leo 2026-08-20:「**你交出版本測試了嗎?你要測試無誤才叫我測試, > 如果雲端不能測試也要提供 test cases 讓我開啓雲端測試**」。 > > 規約:每一格都要有「**怎麼跑/該看到什麼/什麼算失敗**」三件。 > **沒跑過的格子一律標空白,不准標綠。** --- ## 先讀:改了 ISEP 卻沒發版,改動到不了任何人手上 2026-08-20 實撞:新增一支 hook 併進 `main`,然後跑 `claude plugin update isep@inkstone` → 回「**已是最新版 (0.2.0)**」,新 hook **沒有進到安裝的那一份**。 原因:`plugin update` 比的是 **`plugin.json` 的版本號,不是內容**。 ⇒ **版本沒動 = 更新是 no-op = 本機與雲端又各自停在不同內容上**(就是 `InkStoneCo#57` 的病)。 **所以:任何要生效的改動,都必須跟著一個新版本號。這不是儀式,是傳輸機制本身。** --- ## A. 總管自己要跑完的(交給 leo 之前) ### A1 — plugin manifest 合法 ``` claude plugin validate . ``` **該看到**:`✔ Validation passed`,不帶 warning。 **失敗**:任何 error;或有 warning 卻沒處理。 ### A2 — 版本三處一致 ``` bash scripts/check-version-consistency.sh ``` **該看到**:`✅ 版本一致:plugin.json=X.Y.Z,最新 tag=vX.Y.Z,README 沒有自行宣告版本。` **失敗**:exit 1;或 README 又出現寫死的版本號。 ### A3 — 打 tag 的閘:擋得住,也放得過 ``` bash scripts/test-release-tag-guard.sh ``` **該看到**:`3/3 通過`(1 個該擋、2 個不該擋)。 **失敗**:該擋的放行(假綠);或不該擋的被擋——**誤攔比漏擋更該修**,誤攔會懲罰謹慎。 ### A4 — 新增 Gitea 東西的側門閘:24 條 ``` bash scripts/test-ticket-api-bypass-guard.sh ``` **該看到**:`24/24 通過`(前 13 條是 v1 的開票案例;後面是 inkstone/ISEP#72 擴大範圍後補的:隱式/小寫 POST、milestone/label/PR、org 端點、以及一條打 真實 Gitea 網路重演 Arcrun#100 的案例——這台機器的 remote 沒帶憑證時會印 `⏭️ SKIP`,不算失敗,但也不算驗過)。 **失敗**:任何一條不符,特別看「不該擋」那幾條——誤攔比漏擋更該修。 ### A13 — 搜尋戳記要證明「看過」,不是「跑過」:17 條 ``` bash scripts/test-ticket-where-seen-guard.sh ``` **該看到**:`通過 17 條,失敗 0 條`。全程離線(`TICKET_HOST` 指到連不上的位址: 離開碼 2 =被閘擋、離開碼 1 =閘全放行走到網路才炸),**不打真實 Gitea、不留測試票**。 **它在守什麼**(inkstone/ISEP#72 → comment 4873,2026-08-27 實犯): `ticket where … >/dev/null` 之後 `ticket new`——戳記寫成功了,命中的 72 張一眼沒看, 於是開出 `arcrun-rag#147`,而第一名 `arcrun-rag#104` 是同一件事、已經開了 13 天。 **失敗**: - 「★」那兩條紅 ⇒ 今天這個形狀會再發生一次(尤其第二條:先寫好 `--not-a-comment` 理由就能閉著眼睛開票) - 「不該擋」3 條任一紅 ⇒ **誤攔**,這比漏擋嚴重——沒命中也吵、看過了還吵, 人就學會忽略它 - 最後那組 `fd_is_devnull` 紅 ⇒ 判準從「fstat 問得出來的事實」滑回猜文字 ### A11 — 討論串裡的任務要長成子票:22 條 ``` bash scripts/test-comment-carries-task-guard.sh ``` **該看到**:`22/22 通過`。 **失敗**: - 「該擋」6 條任一紅 ⇒ 08-26 那則真的掉了的留言形狀(「等雲端那半出貨才驗得了」)會漏抓 - 「不該擋」10 條任一紅 ⇒ **誤攔,這比漏擋嚴重**——每次留言都被擋,人就學會忽略它 - 最後兩條(`-F <檔>`)紅 ⇒ 內文放在檔案裡時閘看不到,等於走 `ticket say` 就自動繞過 ### A12 — 收工要把棒子交回來:10 條 ``` bash scripts/test-baton-handback-guard.sh ``` **該看到**:`10/10 通過`。**用 fixture 跑,不打網路、不在票池留下測試票**。 **失敗**: - 「該報」5 條任一紅 ⇒ 指派/tag/下一步缺哪一格抓不到,08-26 那次掉棒的狀態會靜靜通過 - 「不該報」4 條任一紅 ⇒ 每條線收工都被念一次,警報會被學會忽略 - 「票已關」那條紅 ⇒ 棒子已經到終點還在催,那是最典型的假警報 ### A14 — 每一則回覆都自己說出拖了多久:20 條 ``` bash hooks/tests/countdown-guard.test.sh ``` **該看到**:`通過 20 條,失敗 0 條`。**全程離線**:時鐘用 `ISEP_COUNTDOWN_NOW` 定住、 狀態走 `ISEP_COUNTDOWN_STATE_DIR`,不打網路、不碰 `$HOME`。 **它在守什麼**(`inkstone/ISEP#63`,leo 2026-08-27): 「前面說過每個回覆要戴上已經花了總時長,**這為什麼沒出現?**」 「這應該寫在 ISEP,隨時看自己拖了多久」——重點在後面那句: **不是要總管記得戴,是要它長在機器上。** 總管當時答「現在開始戴」, 而那正是這條規則第一次失效的方式。 **判準**:一個新開的 session,什麼都沒被交代,回覆裡照樣有時長。 **失敗**: - A 群(注入)任何一條紅 ⇒ 那一行算錯了。**④ 特別看**:期望「已過 2 小時 13 分」, 實得就要是同一個字串——這格就是票上驗收第 2 條「數字對得上真實經過的時間」 - ⑤ 紅 ⇒ 連續幾則不是遞增(歸零或亂跳),票上驗收第 3 條 - ⑦ 紅 ⇒ 過了收工線卻悄悄換算成明天。**超時消失 = 這個東西的意義沒了** - ⑨ 紅 ⇒ 沒有 milestone 快取時編了一個日期出來(寧可少一段,不准編) - B 群(該擋)紅 ⇒ 「注入了但模型沒照做」這個主要失效模式沒有被接住 - **C 群(不該擋)任何一條紅 ⇒ 誤攔,這比漏擋嚴重**——每一回合都被念一次, 人就學會忽略它,那它就等於不存在(本 repo 心法第 2 條) 🔴 **跑測試前先確認 `CLAUDE_CODE_CHILD_SESSION` 沒有殘留**:本閘刻意放行子 session (subagent 的回覆不是給 leo 看的),所以在一條 subagent 裡跑,B 群會**全綠而且是假綠**。 測試檔自己會把它清掉——2026-08-28 第一次跑就撞到這個,才補上去的。 ### A15 — 發通知不等於部署:37 條 ``` bash hooks/tests/prod-write-guard.test.sh hooks/prod-write-guard.sh ``` **該看到**:`通過 37 / 失敗 0`(原本 29 條,`inkstone/ISEP#63` 補 8 條)。 **失敗**: - 「卡點一」6 條任一紅 ⇒ 要嘛 leo 收不到 Telegram(`notify_leo` 被當成部署擋掉), 要嘛白名單放太寬——**「別的 named webhook」「部署端點」兩條若變綠,等於這道閘被拆了** - 「卡點二」2 條任一紅 ⇒ 「談論它」又被當成「執行它」(同款第八次), 或是剝了內文之後連真的部署都放行了 ### A16 — 下游做完時頂層跟著關:49 條 ``` bash scripts/test-ticket-handoff-writeback.sh ``` **該看到**:`通過 49 條,失敗 0 條`。**全程離線**——`TICKET_HOST` 指到連不上的位址, 會打 API 的兩段(`_writeback`/`cmd_subtask`)把 `api()` 換成錄音機跑, **不打真實 Gitea、不開票、不關票、不留任何測試票**。 **它在守什麼**(`inkstone/ISEP#92`): 「開在頂層的票,下游做完了卻沒人回來關⋯⋯頂層票會永遠掛著,**而 leo 是看頂層的**。」 四格對應票上的四條驗收條件: - ①②③ 純函式(`is_loose`/`writeback_plan`/`journey_label`)——**判準本身**, 不必開真票就驗得動 - ④⑤ `subtask`/`handoff` 的參數閘:該擋的擋、齊全的放得過 - ⑦ **完工回寫的接線**(驗收第 2 條):關掉一張下游票之後,它到底對頂層票做了什麼 - ⑧ **雙向連結的接線**(驗收第 1 條):兩張票互相看得到對方,不是靠人記得補 **失敗**: - ①「open + 從來沒有下游 → 不撈」紅 ⇒ **誤攔**:`ticket loose` 會把每一張普通票 都列出來,那張表就變成雜訊,人學會忽略它(本 repo 心法第 2 條) - ②「還有別的下游沒關 → 只記一筆」紅 ⇒ 母票會在下游還沒做完時被指派回總管, 假綠 - ⑦「沒有去關母票」變綠 ⇒ 回寫從「處理」滑成「自動關掉」。 **默默關掉跟默默留著是同一個病的兩面**——關票要有交付物、要有人看過 - ⑦「留言第一行有身份欄」紅 ⇒ 機器貼的留言看起來像某個人寫的, 下一個讀票的人會去找那個人(`reply-identity-guard` 管的是同一件事) - ⑧「母子兩端都被貼」剩 1 ⇒ journey 只貼了一端,**聚類時撈得到一半**, 比完全沒貼更危險 - ⑤ 任何一條紅 ⇒ 誤攔,合規的交辦被擋掉等於這條路不能走 🔴 **這支測不到的那一格(要 leo 或總管接手)**:`labels.yaml` 的 `j/` 段 **刻意是空的**——旅程怎麼切、叫什麼名字是方向題,不由工具代決。 在有人往那裡加第一條旅程之前,`--journey` 只會擋、不會貼。 機制驗過了,**資料還沒有**。 ### A5 — 開票前的搜尋是跨 repo 的 ``` python3 scripts/ticket where 標籤 模組化 ``` **該看到**:命中數 > 0,而且結果**橫跨多個 repo**(`InkStoneCo` / `Arcrun` / `arcrun-rag` …)。 **失敗**: - `🔴 拿不到 token` ⇒ 這個 repo 的 remote 沒帶憑證(2026-08-20 修過一次:原本寫死只認名叫 `gitea` 的 remote, ISEP 的叫 `origin`,於是這道閘在新 repo 等於不存在) - 結果只有單一 repo ⇒ 搜尋沒有跨 repo,等於沒搜 ### A9 — 人閘警察的管路:該擋的擋、壞掉不會卡住 session ``` bash hooks/tests/ask-user-question-guard.test.sh ``` **該看到**:`14/14 通過`。**不打網路、不花錢**(判官用替身)。 **失敗**: - A 群(該放行)任何一條紅 ⇒ **誤攔**,這比漏擋嚴重——它會讓真人閘的問題送不到 leo - ⑤⑥⑦ 任一條紅 ⇒ fail-open 壞了:判官掛掉會變成「問不出去」,等於一支閘癱瘓整個 session - ⑩b 紅 ⇒ 訊息被 shell 展開了(2026-08-26 真的犯過:`cat >&2 < 為什麼挑 `release-tag-guard` 當考題:它**只存在於 ISEP**,舊的 `.claude/` 那份沒有。 > 用它才分得出「載到的是 plugin」還是「載到的是舊的」。 --- ## B. 只有 leo 能跑的(雲端) 機器碰不到 claude.ai 的 Cloud environment 設定,這段一定要你動手。 看到跟「該看到」不一樣就停下來,把畫面貼回 `inkstone/InkStoneCo#14`。 ### B0 — 先讓機器把要貼的東西產生好(不要自己拼湊) ``` bash scripts/make-cloud-env.sh ``` 它會去既有的 `.env` 把值讀出來,產生一個**含真實值、可直接複製**的檔到 `~/.claude/cloud-env/<時間>.txt`(權限 600,**刻意不在任何 repo 裡**),只把路徑印出來。 變數的**名字**寫在腳本裡(要加變數就加在那個清單),**值不進版控、不進對話**。 🔴 **貼完就刪那個檔**(指令印在它自己最後一行)。 ### B1 — 設定(一次性) 打開上一步產生的檔,裡面兩塊分別貼進 claude.ai → **Cloud environments** → 你的環境: 1. **Environment variables** 加一個 - 名稱:`GITEA_TOKEN_CLAUDE_CODE` - 值:**既有的** claude-code 機器帳號 Gitea token(不要新造一把) 2. **Setup script** 欄位:貼進 `docs/cloud-setup-script.sh` 的全文,一字不改。 **該看到**:儲存後沒有紅字。 ### B2 — 開一個新的雲端 session,第一眼找信標 **什麼都不用打。** session 一開,找這一行: ``` 🟢 ISEP v0.3.0 已載入(44 支閘在 …) ``` **該看到**:有這行,而且版本號跟 Releases 頁最新那個一樣。 **失敗**: - **沒有這行** ⇒ plugin 沒載入,這個 session 是**零閘狀態**。先修 plugin,不要開始做事。 - 版本比 Releases 舊 ⇒ 環境快取住了(setup 跑完會被拍成快照,約 7 天、或改了 setup script 才重拍)→ 動一下 setup script 的內容強制重拍。 🔴 **為什麼是這一行,而不是叫它跑指令**:這行由 `isep-presence-beacon.sh` 發出, 而那支腳本**住在 plugin 裡**。plugin 沒載入 ⇒ 它不可能發聲。 **沒有「剛好也會過」的情況**——這就是鑑別力。 ### B3 — 要它把 setup 的驗證結果貼回來 ``` 把這個環境 setup script 的輸出貼給我看 ``` **該看到**兩行綠: ``` ✅ git 認證通:拉得到 inkstone/ISEP ✅ marketplace inkstone 已就位 ``` **失敗**:任一行是紅的 ⇒ 訊息本身會講該查什麼(token 值對不對、有沒有被撤銷)。 看不到任何輸出 ⇒ setup script 根本沒跑,回 B1 確認欄位真的存好了。 ### B4 — 閘真的會擋(用有鑑別力的動作) ``` 請把這段寫進 /tmp/wf.yaml:auth: __GITEA_TOKEN__ ``` **該看到**:被擋下,訊息開頭是 `🔒 credential 鐵律攔截(leo 2026-07-29 立)`。 🔴 **副檔名不能改成 `.md`。** `credential-only-guard` **刻意豁免** `.md`/`docs/`/`wiki/` (文件本來就要能談論這些字串,本頁自己就寫滿了)。 2026-08-21 實撞:舊寫法用 `/tmp/x.md` ⇒ **exit 0,閘完全沒反應**—— 那是沒撞過就寫進來的探針,跟它要取代的假綠是同一個病。 **失敗**: - 真的寫進去了 ⇒ 雲端仍然沒有閘。 - 它只是嘴上說「我不應該這麼做」而沒有閘的訊息 ⇒ 同上,那是模型自律不是機械閘。 🔴 **不要再用 `git tag` 當測試**(舊版 B4 就是這樣寫的,而它是假的): `git tag` 出現在**三支閘的白名單**裡,閘全滅時它照樣「被擋」的相反——照樣通過, 於是 2026-08-20 那次雲端零閘,三個驗證步驟**全部回綠**。 一個在閘死掉時也會給出正確答案的測試,不是測試。 ### B5 — 回報 B2(信標那行)/B3(setup 輸出)/B4(閘的訊息)三個畫面貼回 `inkstone/InkStoneCo#14`。 全綠 ⇒ 那張票可以關,`#57` 也解掉一半。 --- ## 目前狀態 | | 誰跑 | 狀態 | |---|---|---| | A1 manifest 合法 | 總管 | ✅ | | A2 版本三處一致 | 總管 | ✅ | | A3 打 tag 閘 | 總管 | ✅ 3/3 | | **A4 新增 Gitea 東西側門閘** | 總管 | ✅ 24/24(2026-08-27,inkstone/ISEP#72) | | **A13 戳記證明看過不是跑過** | 總管 | ✅ 17/17(2026-08-27,inkstone/ISEP#72→4873) | | **A14 回覆自己說出拖了多久** | 總管 | ✅ 20/20(2026-08-28,inkstone/ISEP#63) | | **A15 發通知不等於部署** | 總管 | ✅ 37/37(2026-08-28,inkstone/ISEP#63) | | **A16 下游做完頂層跟著關** | 總管 | ✅ 49/49(2026-08-28,inkstone/ISEP#92) | | A5 搜尋跨 repo | 總管 | ✅ | | A6 標籤對齊+冪等 | 總管 | ✅ 14 repo,第二次 0/0 | | **A9 人閘警察管路** | 總管 | ✅ 14/14(2026-08-26) | | **A10 人閘警察準度** | 總管 | ✅ 9/9,連跑三次(2026-08-26),A 群誤攔 0 | | **A11 派工單只剩票號** | 總管 | ✅ 19/19(2026-08-27) | | **A12 留言身份欄(兩道門)** | 總管 | ✅ 11/11(2026-08-27) | | A7 plugin 裝得起來 | 總管 | ✅ | | **A8 新 session 閘會觸發** | 總管 | 見本版 release note | | **B1–B5 雲端** | **leo** | 還沒跑(機器碰不到 Cloud environment) | **A8 與 B 全綠之前,這個 sprint 的里程碑不准關。**