Files
ISEP/docs/TESTING.md
T
claude-code 7e58623cb3 PR 不再躺著:收工時清點沒有結論的 PR,並讓「給結論」是一個動作(inkstone/ISEP#81)
實查(2026-08-27):inkstone org 有 7 個 open PR,最久的三個躺了兩星期
(Arcrun#104 15 天、Arcrun#116 14 天、arcrun-rag#91 14 天),
而 v0.9.0 的 46 支閘一支都沒有在管 PR。票看起來「已交付」,
東西卻沒進 main ⇒ 沒進版本 ⇒ leo 手上永遠不會出現它。

新增 hooks/pr-verdict-guard.sh(Stop,F 組)
  open、沒有人被指派、也沒有 REQUEST_CHANGES review 的 PR ⇒ 擋一次並點名
  (含開了幾天與網址);merge 了但 head 分支還留著的一起點名
  ——票上寫死「merge → 併完當場刪掉那條 branch」,分支還在=結論只給了一半。

  🔴 票上點名要避開的錯路(ISEP v0.6.0 divergence §B4):
     「佇列非空即 block」會讓總管永遠停不下來。所以擋的是遺忘,不是等待——
     ① 有人被指派/掛 Human ⇒ 不點名(棒子在他手上,是 Gitea 原生欄位不是措辭)
     ② 有 REQUEST_CHANGES review ⇒ 結論給過了,球在對方腳下
     ③ 這一回合碰過它 ⇒ 不點名
     ④ 響過就退讓:每個 PR 各自 1→4→8→16

  「碰過」不讀任何一句話:① updated_at 跟上次收工比變了(外部系統的事實)
  ② 上次收工時還不存在(剛開出來,不在誕生的回合開罵)
  ③ 這回合的 tool call 出現它的識別碼(接住「結論寫在票上」這種合法結論)。
  ③ 不是關鍵字黑名單——黑名單拿措辭來「擋」,這裡拿識別碼來「放行」,
  比對錯了只會少擋一次。leo 2026-08-17 證偽的是前者。

  刻意只掛 Stop 不掛 SubagentStop:給 PR 結論是總管的權,subagent 連 main 都不准推。

新增 scripts/pr-verdict —— merge/reject/changes 各是一個動作
  merge   併 + 刪分支 + **當場複驗分支真的不見了**(下了刪除指令 ≠ 它被刪掉)
  reject  理由寫進票 + 關 PR + 刪分支
  changes 修改要求寫進票 + 留 REQUEST_CHANGES(讓「給過結論」變成閘查得到的事實)
  list / --dry-run(按下去之前先看會打哪幾通 API,一通都不送出)

測試 hooks/tests/pr-verdict-guard.test.sh:52 條全過,離線走 fixture,
不打 Gitea、不開測試 PR、不留測試票。A 群 8 條全在證「不該擋」。
另用真實 Gitea 唯讀重演過票上那三個真跡(一次點名 7 個)。

順手:plugin.json 的「27 支腳本」是過期數字,實數 34
(ls -p scripts | grep -v / | wc -l)——同一個病,只是換一欄。

版本 0.9.0 → 0.10.0。tag 待併入 main 後由總管打(release-tag-guard 管那道)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZBu4Sa1cGntKFRBYNZ6xs
2026-08-28 00:05:40 +00:00

335 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.jsonX.Y.Z,最新 tagvX.Y.ZREADME 沒有自行宣告版本。`
**失敗**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、milestonelabelPR、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 48732026-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 — PR 一定要有結論:52 條
```
bash hooks/tests/pr-verdict-guard.test.sh
```
**該看到**`52 通過 / 0 失敗`。全程離線(走 `PR_VERDICT_FIXTURE`),
**不打 Gitea、不開測試 PR、不留任何測試票**;狀態檔走 `PR_VERDICT_STATE_DIR`
**它在守什麼**`inkstone/ISEP#81`2026-08-27 實查):最久的三個 open PR 躺了兩星期
`inkstone/arcrun-rag#91` 14 天、`inkstone/Arcrun#116` 14 天、`inkstone/Arcrun#104` 15 天),
而 v0.9.0 的 46 支閘**一支都沒有在管 PR**。票看起來是「已交付」,
東西卻沒進 main ⇒ 沒進版本 ⇒ **leo 手上永遠不會出現它**
**失敗**
- B 群(⑨⑩⑪,票上那三個真跡)任一紅 ⇒ 今天這個形狀會再發生一次
- C 群 ⑭⑰ 紅 ⇒ 驗收條件 1/3 沒過:「放著不管」抓不到,或「merge 了分支還在」放行
- **A 群任何一條紅 ⇒ 誤攔,這比漏擋嚴重**。②③特別重要:
指派給人/掛 Human 之後還在點名,就是 ISEP v0.6.0 divergence §B4 記過的那個坑
(「`s/review` 佇列非空即 block」⇒ 佇列本來就不會空 ⇒ **總管永遠停不下來**
- E 群 ㉙ 紅 ⇒ 閘訊息裡承諾的出路(「把它指派給那個人,本閘立刻不再點名它」)是假的
- D 群 ㉓ 紅 ⇒ 判準從「識別碼比對」滑成「只要提到票號就放行」,閘會被任何一句話關掉
> 📌 **真跡不只在 fixture 裡跑過**:同一支閘打真實 Gitea 也驗過,
> 一次點名 7 個沒有結論的 PR,含票上那三個(15/14/14 天)。
> 指令:`printf '{"session_id":"x","transcript_path":"/nonexistent"}' | bash hooks/pr-verdict-guard.sh`
> ——**唯讀,不改動任何 PR**。
### A15 — 給結論的那支工具自己說得出要打哪幾通 API
```
python3 scripts/pr-verdict list
python3 scripts/pr-verdict merge inkstone/ISEP#71 --dry-run
```
**該看到**`list` 印出 open PR 與「有沒有人被指派」;`merge --dry-run` 印出
`[dry-run] POST …/pulls/71/merge``[dry-run] DELETE …/branches/<分支>`**一通都不真的送出**。
**失敗**`--dry-run` 底下出現真實的寫入結果 ⇒ 這支工具沒辦法在按下去之前被檢查。
### 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 <<EOF` 沒加引號,
訊息裡的反引號被當命令執行,**閘照擋,但它教人怎麼解的那兩行變成空白**)
### A10 — 人閘警察的準度:四題公式判得準不準
```
bash hooks/tests/ask-user-question-guard.live.test.sh
```
🔴 **這支真的會叫 haiku**(9 題、每題一次呼叫,整支約 2 分鐘)。
**該看到**`9/9 通過`,且結尾的「A 群誤攔」計數是 **0**
**失敗**
- **A 群紅(誤攔真人閘)=最嚴重**:等於讓總管替 leo 決定他的品味。看到就停下來改判準,不要放著
- B 群紅 = 漏擋,判官把純技術題當成人閘。改 `ask-user-question-guard.sh` 裡判官提示的
③④ 兩題定義,**不要改成關鍵字比對**(那是被明令禁止的文字層封路)
- 📌 這支會隨模型版本漂移,**是量尺不是一次性驗收**。改完判準要連跑三次都全綠才算數
2026-08-26 實測:第一版判準連兩次都在同一題漏擋,收緊 ③④ 定義後三次全綠)
### A11 — 派工單只剩票號:擋得住,也放得過,而且會注入共通規定
```
bash hooks/tests/dispatch-format-guard.test.sh
```
**該看到**`19/19 通過`。**離線、不打網路、不花錢**——這支閘是純結構判斷,沒有語意判官,
所以它不需要像 A10 那樣另開一支 live 測試量準度,**每次結果都一樣**。
**失敗**
- A 群任何一條紅 ⇒ **誤攔**。合規的派工只有一行票號,擋掉它等於整台機器派不了工
- ⑦ 紅 ⇒ **共通規定沒有被注入**。這是「派工單只剩票號」能成立的前提:
交件方式、不准 push main、org 是 `inkstone` 這些不必有人記得寫,機器每次都補。
它壞了不會有人立刻發現——派工照樣送出去,只是收工方**不知道要貼回原票**
- ⑨ 紅 ⇒ 真跡放行了。那份測資是**真的發生過的那一次派工**(見 `hooks/tests/fixtures/README.md`
- ⑰ 紅 ⇒ 訊息被 shell 展開了(同 A9 ⑩b 那個病:閘照擋,但它教人怎麼解的那兩行變成空白)
### A12 — 票上的每一則留言都認得出是誰寫的(兩道門)
```
bash hooks/tests/reply-identity.test.sh
```
**該看到**`11/11 通過`。離線,正門的案例全部在打 API 之前就結束,不會真的送出留言。
**失敗**
- ③ 紅 ⇒ 誤攔了「GET 撈留言」。那是最常做的動作,擋它比漏擋更糟
- ①⑧ 紅 ⇒ 有一道門沒守住。**貼留言有兩條路**(`scripts/ticket` 正門、Gitea API 側門),
只封一條等於沒封——`ticket-api-bypass-guard.sh` 是**刻意放行**對既有票留言的
### A6 — 標籤對齊且冪等
```
bash scripts/gitea-labels-sync.sh
bash scripts/gitea-labels-sync.sh
```
**該看到**:第二次全部 `0 created / 0 updated`
**失敗**:第二次還在改(不冪等);或任何既有標籤被刪除。
### A7 — plugin 裝得起來、內容對得上
```
claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git
claude plugin install isep@inkstone
claude plugin list
claude plugin details isep
```
**該看到**`isep@inkstone` `enabled`,版本=最新 release`details` 列出 9 skills、5 個 hook 事件。
**失敗**:版本落後(先發版,見開頭那段);或 `marketplace list``Source` 顯示**本機目錄**而非 Git URL
——本機目錄有未提交改動就會跟 main 分岔,那是一條漂移路徑。
### A8 — 閘在**新 session** 真的會觸發
前七格證明「腳本會擋」與「檔案就位」,**不是「harness 真的會去叫它」**。
plugin 的 hook 是 session 啟動時載入,所以這格一定要開**新**的 session。
```
claude -p '請執行 git tag -a v9.9.9 -m test'
```
**該看到**:回報被擋,訊息是 `release-tag-guard` 那段(提到 plugin.json 與版本對不上)。
**失敗**
- tag 真的被打出去 ⇒ **閘沒被載入**,這是最危險的假綠
- 訊息來自 `InkStoneCo/.claude/hooks/…` 而不是 plugin ⇒ 你驗到的是舊那份
> 為什麼挑 `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.yamlauth: __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/242026-08-27inkstone/ISEP#72 |
| **A13 戳記證明看過不是跑過** | 總管 | ✅ 17/172026-08-27inkstone/ISEP#72→4873 |
| **A14 PR 一定要有結論** | 總管 | ✅ 52/522026-08-28inkstone/ISEP#81)+真實 Gitea 唯讀重演 |
| **A15 pr-verdict --dry-run** | 總管 | ✅(2026-08-28 |
| A5 搜尋跨 repo | 總管 | ✅ |
| A6 標籤對齊+冪等 | 總管 | ✅ 14 repo,第二次 0/0 |
| **A9 人閘警察管路** | 總管 | ✅ 14/142026-08-26 |
| **A10 人閘警察準度** | 總管 | ✅ 9/9,連跑三次(2026-08-26),A 群誤攔 0 |
| **A11 派工單只剩票號** | 總管 | ✅ 19/192026-08-27 |
| **A12 留言身份欄(兩道門)** | 總管 | ✅ 11/112026-08-27 |
| A7 plugin 裝得起來 | 總管 | ✅ |
| **A8 新 session 閘會觸發** | 總管 | 見本版 release note |
| **B1B5 雲端** | **leo** | 還沒跑(機器碰不到 Cloud environment |
**A8 與 B 全綠之前,這個 sprint 的里程碑不准關。**