Files
ISEP/scripts/test-debt-worklist.sh
claude-code 45adc10a52 併進 main(v0.12.0)並接上「主線唯一答案」(inkstone/ISEP#83)
衝突只有 docs/TESTING.md 一處,兩邊都留:
  main 的 A16(gate-ok)/A17(未推警察)/A18(信標)原樣保留,
  我那節改編號成 A19——main 上 A16、A17 已經被 #90#99 各佔一次,不動別人的。

接上 inkstone/ISEP#82 帶進來的 hooks/lib/mainline.py(不自己養第二套判斷):
  · 有標主線 ⇒ 只排除「屬於主線那一條」的票;掛在**別條** milestone 的仍算債
    (那條線現在沒有人在推,正是它躺著的原因)
  · 沒標主線 ⇒ 退回保守做法:掛 open milestone 的一律不領,並在清單上照實說沒標
  · 別條 milestone 已逾期 ⇒ +15「答應過人家的日期已經過了」

測試 40 → 47 條(+7 條全給主線這段),並把 ISEP_COUNTDOWN_STATE_DIR 也隔離:
不然這支測試的結果會取決於「這台機器現在標了哪條主線」。

盤點數字在合併後的樹上重數:
  hooks/*.sh = 54、hooks.json "command": = 71(本 PR 沒新增 hook,兩個都沒變)
  scripts 一層檔案 = 39(本 PR +2)⇒ plugin.json description 37→39
  版本號沒動(0.12.0,總管統一定版)
2026-08-28 00:51:06 +00:00

223 lines
13 KiB
Bash
Executable File
Raw Permalink 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.
#!/bin/bash
# 還債清單(scripts/debt-worklist)的測試——inkstone/ISEP#83 的四條驗收條件,一條一組。
#
# 全程離線:資料走 ISEP_DEBT_FIXTURE、時鐘走 ISEP_DEBT_NOW、狀態走 ISEP_DEBT_STATE_DIR。
# **不打 Gitea、不留任何測試票、不碰 $HOME**。
#
# 為什麼要能離線測:這支工具的判斷(誰排前面、誰算緊急線、領走的還在不在)
# 全都是純函式,只有「撈」那一步要網路。把撈的那一步換掉,其餘每一條都測得動——
# 否則就只剩「今天跑起來看起來對」,那不是測試(本 repo 心法:跑過 ≠ 驗過)。
cd "$(dirname "$0")/.." || exit 1
W=scripts/debt-worklist
PASS=0; FAIL=0
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
NOW=2026-08-28T00:00:00Z
ok(){ # $1=條件 $2=說明
if eval "$1"; then PASS=$((PASS+1)); printf ' ✅ %s\n' "$2"
else FAIL=$((FAIL+1)); printf ' ❌ %s\n 判準:%s\n' "$2" "$1"; fi
}
run(){ # 跑 list(或任何子命令),輸出留在 $OUT,離開碼留在 $RC
# 🔴 ISEP_COUNTDOWN_STATE_DIR 也要指到暫存目錄:主線(hooks/lib/mainline.py)住在那裡,
# 不隔離的話這支測試的結果會取決於「這台機器現在標了哪條主線」——那就不是測試了。
OUT=$(ISEP_DEBT_FIXTURE="$FX" ISEP_DEBT_STATE_DIR="$T/state" ISEP_DEBT_NOW="$NOW" \
ISEP_COUNTDOWN_STATE_DIR="$ML_DIR" python3 "$W" "$@" 2>&1); RC=$?
}
ML_DIR="$T/mainline"; mkdir -p "$ML_DIR"
set_mainline(){ # $1=主線 milestone 標題(空=沒有主線)
if [ -z "$1" ]; then rm -f "$ML_DIR/mainline.json"; return; fi
python3 - "$ML_DIR/mainline.json" "$1" <<'PY'
import json, sys
path, title = sys.argv[1], sys.argv[2]
json.dump({"ref": "inkstone/A#1", "owner": "inkstone", "repo": "A", "id": 1,
"title": title, "members": [], "due_on": ""}, open(path, "w"))
PY
}
# ── 造資料:一份長得像 Gitea 回應的 fixture ─────────────────────────────────
mkfx(){ FX="$T/$1.json"; python3 - "$FX" "$1" <<'PY'
import json, sys
path, which = sys.argv[1], sys.argv[2]
def it(repo, n, title, labels=(), ms=None, ms_state="open", assignees=(),
updated="2026-08-01T00:00:00Z", body="", pr=False):
d = {"number": n, "title": title, "body": body, "updated_at": updated,
"created_at": "2026-07-01T00:00:00Z", "state": "open",
"labels": [{"name": l} for l in labels],
"assignees": [{"login": a} for a in assignees] or None,
"repository": {"full_name": repo},
"html_url": f"https://git.uncle6.me/{repo}/issues/{n}"}
if ms:
d["milestone"] = {"title": ms, "state": ms_state}
if ms == "上個 sprint":
d["milestone"]["due_on"] = "2026-08-10T23:59:59Z" # 已經過了 18 天
if pr:
d["pull_request"] = {"merged": False}
return d
repos = [{"name": "inkstone/A", "issues": 0, "prs": 0},
{"name": "inkstone/B", "issues": 0, "prs": 0}]
deps, items = {}, []
if which == "full":
items = [
# 事故+擋住別人的 → 該排第一
it("inkstone/A", 1, "壞掉了", ["type/bug"], updated="2026-08-20T00:00:00Z",
body="x" * 2000),
it("inkstone/A", 2, "我等 A#1", updated="2026-08-27T00:00:00Z",
body="被 inkstone/A#1 擋住" + "y" * 2000),
it("inkstone/B", 9, "我也等 A#1", updated="2026-08-27T00:00:00Z",
body="要先解 inkstone/A#1" + "y" * 2000),
# 掛在 open milestone 上=緊急線在管,不該出現
it("inkstone/A", 3, "緊急線的票", ["p/high", "type/bug"], ms="這週出貨"),
# 掛在**別條**(非主線)milestone 上,而且期限已經過了
it("inkstone/A", 8, "別條線的票", ms="上個 sprint", body="q" * 2000),
# milestone 已關但票還開著=孤兒,該出現
it("inkstone/A", 4, "孤兒票", ms="上週", ms_state="closed", body="z" * 2000),
# 等 leo 的三種,都不該出現在可領清單
it("inkstone/A", 5, "等 leo 批", ["Human", "p/high", "type/bug"]),
it("inkstone/A", 6, "等 leo 動手", ["human/exec"]),
it("inkstone/A", 7, "等 leo 驗", ["s/stage", "p/high"]),
# 放很久沒人動 vs 昨天才動過:同條件下前者要排前面
it("inkstone/B", 10, "放了很久", updated="2026-06-01T00:00:00Z", body="w" * 2000),
it("inkstone/B", 11, "昨天才動過", updated="2026-08-27T00:00:00Z", body="w" * 2000),
# 躺著的 PR 也是債
it("inkstone/B", 12, "躺著的 PR", pr=True, updated="2026-08-10T00:00:00Z",
body="v" * 2000),
# p/low:往後排
it("inkstone/B", 13, "晚點沒關係", ["p/low"], body="u" * 2000),
# 互相指著=必然同包
it("inkstone/B", 20, "雙胞胎甲", body="跟 inkstone/B#21 是同一件事" + "t" * 2000,
updated="2026-08-05T00:00:00Z"),
it("inkstone/B", 21, "雙胞胎乙", body="見 inkstone/B#20" + "t" * 2000,
updated="2026-08-05T00:00:00Z"),
# 母票/子票:只有原生相依看得出來(內文沒有互指)
it("inkstone/B", 30, "母票", body="s" * 2000, updated="2026-08-05T00:00:00Z"),
it("inkstone/B", 31, "子票", body="s" * 2000, updated="2026-08-05T00:00:00Z"),
]
deps = {"inkstone/B#30": ["inkstone/B#31"]}
elif which == "empty":
items = [it("inkstone/A", 3, "緊急線的票", ["p/high"], ms="這週出貨"),
it("inkstone/A", 5, "等 leo 批", ["Human"])]
elif which == "onedone":
# 「昨天領走的今天關掉了」——它從 open 清單消失
items = [it("inkstone/A", 2, "我等 A#1", body="y" * 2000)]
json.dump({"repos": repos, "items": items, "deps": deps}, open(path, "w"))
PY
}
mkfx full; FULL="$T/full.json"
mkfx empty
mkfx onedone
echo "── 驗收 1:跑一次要吐出排好序的清單+「我查了哪些 repo」的證明 ──"
FX="$FULL"; run list --top 20
ok '[ "$RC" = 0 ]' '離開碼 0'
ok 'grep -q "查過的 repo2 個" <<<"$OUT"' '證明段落列出查過的 repo 數'
ok 'grep -q "inkstone/A" <<<"$OUT" && grep -q "inkstone/B" <<<"$OUT"' '每個 repo 都列名(不是只給總數)'
ok 'grep -q "1\. \[.*\] inkstone/A#1" <<<"$OUT"' '事故+擋住兩張 ⇒ 排第一名'
ok 'grep -q "事故+40" <<<"$OUT"' '每張票旁邊看得到「為什麼排這裡」的分項'
ok 'grep -q "擋住2張" <<<"$OUT"' '「擋住別人的」算得出來(兩張指著 A#1)'
echo "── 驗收 1b:排序的五個條件真的有作用 ──"
pos(){ grep -n "$1" <<<"$OUT" | head -1 | cut -d: -f1; }
ok '[ "$(pos "inkstone/B#10")" -lt "$(pos "inkstone/B#11")" ]' '放最久的排在昨天才動過的前面'
ok '[ "$(pos "inkstone/B#12")" -lt "$(pos "inkstone/B#13")" ]' '躺著的 PR 排在 p/low 前面'
ok 'grep -q "PR躺著+25" <<<"$OUT"' 'PR 被認出來(不是只算 issue'
ok '! grep -q "inkstone/A#3" <<<"$OUT"' '掛 open milestone 的不進清單(那歸緊急線)'
ok 'grep -q "inkstone/A#4" <<<"$OUT"' 'milestone 已關但票還開著 ⇒ 是債,要進清單'
ok '! grep -q "inkstone/A#5" <<<"$OUT" && ! grep -q "inkstone/A#6" <<<"$OUT"' \
'Humanhuman-exec 不進可領清單(沒有人能替 leo 領)'
ok '! grep -q "inkstone/A#7" <<<"$OUT"' 's/stage=等 leo 驗,要催不是要領'
ok 'grep -q "等 leo 的 3 張" <<<"$OUT"' '被扣掉的有講數字,不是靜靜消失'
echo "── 驗收 1c:成包——相關的要一起領,但不准黏成一坨 ──"
ok 'grep -A6 "inkstone/B#20(票)" <<<"$OUT" | grep -q "inkstone/B#21"' '互相指著的成同一包'
ok 'grep -A6 "inkstone/B#30(票)" <<<"$OUT" | grep -q "inkstone/B#31"' '原生相依(母票→子票)成同一包'
ok '! (grep -A6 "inkstone/A#1(票)" <<<"$OUT" | grep -q "同一包")' \
'單向引用不成包(A#2、B#9 都指著 A#1,但 A#1 沒指回來 ⇒ 不能黏成一坨)'
echo "── 接上「現在的主線是哪一個」(inkstone/ISEP#82 的唯一答案)──"
set_mainline ""; FX="$FULL"; run list --top 20
ok 'grep -q "現在沒有標主線" <<<"$OUT"' '沒標主線 ⇒ 照實說,不編一條出來'
ok '! grep -q "inkstone/A#3" <<<"$OUT" && ! grep -q "inkstone/A#8" <<<"$OUT"' \
'沒標主線 ⇒ 保守:掛 open milestone 的一律不領(不跟緊急線搶同一張)'
set_mainline "這週出貨"; run list --top 20
ok 'grep -q "現在的主線" <<<"$OUT" && grep -q "這週出貨" <<<"$OUT"' \
'標了主線 ⇒ 清單上講得出用的是哪一條(判斷來自 scripts/mainline,不是自己算的)'
ok '! grep -q "inkstone/A#3" <<<"$OUT"' '在主線上的票仍然不領(那是緊急線在管的)'
ok 'grep -q "inkstone/A#8" <<<"$OUT"' '掛在別條 milestone 的是債(那條線現在沒人推)'
ok 'grep -q "逾期17天的承諾+15" <<<"$OUT"' '別條 milestone 已逾期 ⇒ 算「答應過人家的」加分'
ok 'grep -q "別條\*\* milestone 上的 1 張仍算債" <<<"$OUT"' '被改判成債的有幾張,講出來'
set_mainline "" # 收回去,後面幾組回到「沒有主線」的世界
echo "── 驗收 2:清單是空的要說話,不能默默結束 ──"
FX="$T/empty.json"; run list
ok '[ "$RC" = 0 ]' '離開碼 0(沒東西可領不是錯誤)'
ok 'grep -q "查詢跑過了,結果是零" <<<"$OUT"' '明講「查過了,結果是零」'
ok 'grep -q "查過的 repo2 個" <<<"$OUT"' '空清單一樣附查詢證明(分得出「沒東西」與「沒查」)'
echo "── 驗收 3:今天領走的,明天不在清單上 ──"
rm -rf "$T/state"; FX="$FULL"
run claim inkstone/B#20 inkstone/B#21
ok '[ "$RC" = 0 ]' '領走兩張成功'
run list --top 20
PICK=$(sed -n '/今天可以領的/,$p' <<<"$OUT")
ok '! grep -q "inkstone/B#21" <<<"$PICK"' '領走的不再出現在可領清單(只看清單那一段)'
ok 'grep -q "之前領走的 2 張" <<<"$OUT"' '第二天看得到「之前領走的」那一段'
ok 'grep -q "還開著 2" <<<"$OUT"' '還開著的照實說(沒關就是還沒做完)'
ok 'grep -q "已經領走的 2 張" <<<"$OUT"' '扣除數字對得上'
echo "── 驗收 3b:領走的票關掉之後,狀態要跟著變 ──"
FX="$T/onedone.json"; run list
ok 'grep -q "已關 2" <<<"$OUT"' '票已經不在 open 清單 ⇒ 認定它做完了'
echo "── 驗收 3c:放回去 ──"
FX="$FULL"; run forget inkstone/B#20 inkstone/B#21
ok '[ "$RC" = 0 ]' 'forget 成功'
run list --top 20
ok 'grep -q "inkstone/B#20" <<<"$OUT"' '放回去之後又回到可領清單'
echo "── 誤用:擋得住,而且說得出怎麼改 ──"
run claim B#20
ok '[ "$RC" = 2 ] && grep -q "owner/repo#N" <<<"$OUT"' '票號寫錯 ⇒ 擋下並示範正確寫法'
run claim inkstone/B#999
ok '[ "$RC" = 2 ]' '領一張不存在/已關的票 ⇒ 擋下'
run claim
ok '[ "$RC" = 2 ] && grep -q "用法" <<<"$OUT"' '沒給票號 ⇒ 印用法'
echo "── 機器可讀(要接進別的東西時不用 parse 中文)──"
run list --json
ok 'python3 -c "import json,sys;d=json.load(sys.stdin);sys.exit(0 if d[\"bundles\"] and d[\"repos\"] else 1)" <<<"$OUT"' \
'--json 吐得出合法 JSON,且有 bundlesrepos'
echo "── 紅線:這支腳本自己不會醒(不准做成輪詢/fan-out)──"
ok '! grep -nE "time\\.sleep|crontab|sched|urlopen.*hooks" '"$W" \
'原始碼裡沒有等待/排程/裝 webhook 的動作(只有註解在談紅線,那不算)'
echo "── SessionStart 那一句:不用有人記得下指令,也不准偷偷打網路 ──"
# 把 session-start-recall.sh 裡「還債線」那一段切出來單獨跑(其餘段落會打雲端,這裡不要)。
FRAG="$T/frag.sh"
sed -n '/push 6:還債線/,/^echo ""$/p' hooks/session-start-recall.sh > "$FRAG"
ok '[ -s "$FRAG" ]' 'session-start-recall.sh 裡真的有那一段(被刪掉就會紅)'
rm -rf "$T/nudge"; mkdir -p "$T/nudge"
OUT=$(set -euo pipefail; ISEP_DEBT_STATE_DIR="$T/nudge" bash "$FRAG" 2>&1); RC=$?
ok '[ "$RC" = 0 ]' '沒有狀態檔也跑得過(第一次開 session)'
ok 'grep -q "今天還沒撈過" <<<"$OUT"' '今天沒撈過 ⇒ 開 session 就會說'
ok 'grep -q "debt-worklist list" <<<"$OUT"' '順手把要跑的那一行給出來'
printf '{"claims":{"a/b#1":{}},"last_run":"%sT00:00:00Z"}' "$(date -u +%Y-%m-%d)" > "$T/nudge/claimed.json"
OUT=$(ISEP_DEBT_STATE_DIR="$T/nudge" bash "$FRAG" 2>&1)
ok 'grep -q "今天撈過了" <<<"$OUT"' '今天撈過了 ⇒ 不再重複催'
ok 'grep -q "手上領著 1 張" <<<"$OUT"' '手上還領著幾張講出來'
ok '! grep -qE "curl|urlopen|http" <<<"$(cat "$FRAG")"' \
'SessionStart 那一段完全不打網路(撈是人決定的動作,不是開機自動去撈)'
echo
echo "通過 $PASS 條,失敗 $FAIL 條"
[ "$FAIL" = 0 ]