#!/usr/bin/env bash # scripts/milestone-account 的迴歸測試(inkstone/ISEP#85) # # 驗收 1:關掉一個 milestone → 三個數字要自己算出來,不用手填 # 驗收 2:差超過 25% → 要逼我挑一個代號,挑不出來不准結案 # 驗收 3:累積幾次之後 → 看得出哪一種原因最常發生 # 驗收 4:拿已經逾期的 milestone 進去算 → 要算得出數字 # # 🔴 全程離線(MILESTONE_ACCOUNT_FIXTURE =一份假的 Gitea 回應表), # 時鐘用 MILESTONE_ACCOUNT_NOW 定住,帳本寫在 mktemp 目錄。 # **不打 Gitea、不關任何真的 milestone、不碰 repo 裡的 system-dev/estimates/。** set -u TOOL="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/milestone-account}" TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT PASS=0; FAIL=0; N=0 NOW="2026-08-28T00:00:00Z" FX="$TMP/fx.json" python3 - "$FX" <<'PY' import json, sys, urllib.parse api = {} def iso(d, h=0): return "2026-08-%02dT%02d:00:00Z" % (d, h) def issue(n, created, closed=None, assignee=True, labels=(), ms=None): return {"number": n, "created_at": created, "closed_at": closed, "updated_at": closed or created, "assignees": [{"login": "cc"}] if assignee else [], "labels": [{"name": l} for l in labels], "milestone": ({"id": ms} if ms else None)} def comment(n, at): return {"issue_url": "https://x/api/v1/repos/o/r/issues/%d" % n, "created_at": at} def repo(full, mid, title, created, due, closed, issues, comments, open_ms=True): o, r = full.split("/") ms = {"id": mid, "title": title, "created_at": created, "due_on": due, "closed_at": closed, "state": "open" if closed is None else "closed"} api["/repos/%s/%s/milestones/%d" % (o, r, mid)] = ms api["/repos/%s/%s/issues?state=all&type=issues&limit=50&page=1" % (o, r)] = issues api["/repos/%s/%s/issues/comments?limit=100&since=%s" % (o, r, urllib.parse.quote(created))] = comments api["/repos/%s/%s/milestones?state=open&limit=50" % (o, r)] = [ms] if open_ms else [] # ① A#12 磨洋工:08-02 收掉一張之後,時間軸整整空了 24 天 repo("inkstone/A", 12, "sprint-磨洋工", iso(1), "2026-08-08T23:59:59Z", None, [issue(1, iso(1, 1), iso(2), ms=12), issue(2, iso(1, 2), ms=12), issue(3, iso(1, 3), ms=12), issue(9, iso(10), ms=None)], [comment(1, iso(26))]) # ② B#13 準時收掉:偏差只有 -6%,不必挑代號 repo("inkstone/B", 13, "sprint-準時", iso(20), "2026-08-27T23:59:59Z", "2026-08-27T12:00:00Z", [issue(1, iso(20, 1), iso(25), ms=13), issue(2, iso(20, 2), iso(26), ms=13)], [comment(1, iso(22)), comment(2, iso(24))], open_ms=False) # ③ C#14 過程沒異常、就是估錯:每兩天都有動作,其他六個訊號全不成立 repo("inkstone/C", 14, "sprint-估錯", iso(1), "2026-08-10T23:59:59Z", "2026-08-25T00:00:00Z", [issue(1, iso(1, 1), iso(24), ms=14), issue(2, iso(1, 2), iso(23), ms=14)], [comment(1, iso(d)) for d in range(3, 25, 2)], open_ms=False) # ④ D#15 範圍變大:開跑之後又補進來三張票 repo("inkstone/D", 15, "sprint-範圍變大", iso(1), "2026-08-10T23:59:59Z", "2026-08-25T00:00:00Z", [issue(1, iso(1, 1), iso(24), ms=15), issue(2, iso(1, 2), iso(23), ms=15), issue(3, iso(15), ms=15), issue(4, iso(15, 1), ms=15), issue(5, iso(15, 2), ms=15)], [comment(1, iso(d)) for d in range(3, 25, 2)], open_ms=False) api["/orgs/inkstone/repos?limit=100"] = [ {"full_name": "inkstone/%s" % x} for x in "ABCD"] json.dump({"api": api}, open(sys.argv[1], "w"), ensure_ascii=False) PY run() { MILESTONE_ACCOUNT_FIXTURE="$FX" MILESTONE_ACCOUNT_DIR="$TMP/led" \ MILESTONE_ACCOUNT_NOW="$NOW" python3 "$TOOL" "$@" 2>&1; } t() { want="$1"; desc="$2"; must="$3"; shift 4 N=$((N+1)); out=$(run "$@"); rc=$? ok=1; [ "$rc" -eq "$want" ] || ok=0 if [ -n "$must" ] && ! printf '%s' "$out" | grep -qF "$must"; then ok=0; fi if [ "$ok" -eq 1 ]; then printf ' ✅ %s\n' "$desc"; PASS=$((PASS+1)) else printf ' ❌ %s —— 期望 exit=%s%s,實得 exit=%s\n' "$desc" "$want" \ "${must:+ 且輸出含「$must」}" "$rc" printf '%s\n' "$out" | sed -n '1,12p' | sed 's/^/ /'; FAIL=$((FAIL+1)) fi } echo "── 驗收 1:三個數字自己算出來 ─────────────────────────────" t 0 "① audit 印出「當初訂的期限」" "當初訂的期限" -- audit inkstone/A#12 t 0 "② audit 印出「實際花掉的」" "實際花掉的" -- audit inkstone/A#12 t 0 "③ 偏差算得出來(A#12 逾期 +238%)" "+238%" -- audit inkstone/A#12 t 0 "④ 期限是 08-08、實際算到今天 08-28 ⇒ 27 天" "27.0天" -- audit inkstone/A#12 echo "── 驗收 2:差超過 25% 逼你挑代號 ──────────────────────────" t 2 "⑤ A#12 差 +237% 卻沒給 --reason ⇒ 不准結案" "挑不出代號不准結案" -- \ close inkstone/A#12 t 2 "⑥ 承上:訊息要直接列出撐得住的代號" "時間軸撐得住的代號:idle" -- close inkstone/A#12 t 0 "⑦ B#13 只差 -6% ⇒ 不必挑代號,直接結得掉" "已記帳" -- close inkstone/B#13 t 2 "⑧ 造一個不存在的代號 ⇒ 擋(封閉集合)" "不是那七個代號之一" -- \ close inkstone/A#12 --reason 拖延症 t 2 "⑨ 挑一個時間軸撐不住的代號 ⇒ 擋(不採信我自己說的)" "時間軸撐不住" -- \ close inkstone/C#14 --reason idle t 0 "⑩ C#14 六個訊號都不成立 ⇒ 唯一合法的是 misestimate" "misestimate" -- \ audit inkstone/C#14 t 0 "⑪ 承上:--reason misestimate 收得掉" "原因=misestimate" -- \ close inkstone/C#14 --reason misestimate t 0 "⑫ A#12 挑 idle(時間軸真的有 24 天空白)⇒ 收得掉" "原因=idle" -- \ close inkstone/A#12 --reason idle t 0 "⑬ D#15 開跑後補三張票 ⇒ scope-creep 撐得住" "原因=scope-creep" -- \ close inkstone/D#15 --reason scope-creep echo "── 驗收 3:看得出哪一種原因最常發生 ───────────────────────" t 0 "⑭ report 數得出每個代號各幾次" "磨洋工" -- report t 0 "⑮ report 算得出累計結案幾個" "累計結案 4 個里程碑" -- report N=$((N+1)) if grep -q "哪一種原因最常發生" "$TMP/led/REPORT.md" 2>/dev/null \ && grep -q "時間軸證據" "$TMP/led/REPORT.md"; then printf ' ✅ ⑯ REPORT.md 寫出來了,而且每一筆都帶時間軸證據\n'; PASS=$((PASS+1)) else printf ' ❌ ⑯ REPORT.md 內容不對\n'; FAIL=$((FAIL+1)) fi N=$((N+1)) if [ "$(wc -l < "$TMP/led/ledger.jsonl")" = "4" ]; then printf ' ✅ ⑰ 帳本剛好四筆(沒有被擋下的那幾次也記進去)\n'; PASS=$((PASS+1)) else printf ' ❌ ⑰ 帳本筆數不對:%s\n' "$(wc -l < "$TMP/led/ledger.jsonl" 2>/dev/null)" FAIL=$((FAIL+1)) fi echo "── 驗收 4:拿已經逾期的里程碑進去算 ───────────────────────" t 0 "⑱ list --overdue 撈得到逾期的那一個,而且帶數字" "inkstone/A#12" -- \ list --org inkstone --overdue t 0 "⑲ 承上:逾期的要被標成「差超過 ±25%」那一類" "🔴" -- \ list --org inkstone --overdue echo "── 其他 ───────────────────────────────────────────────────" t 0 "⑳ codes 印得出七個代號,一個不多一個不少" "misestimate" -- codes N=$((N+1)) if [ "$(run codes | grep -cE '^ [a-z-]+ ')" = "7" ]; then printf ' ✅ ㉑ 真的是七個(封閉集合)\n'; PASS=$((PASS+1)) else printf ' ❌ ㉑ 代號數量不是 7:%s\n' "$(run codes | grep -cE '^ [a-z-]+ ')"; FAIL=$((FAIL+1)) fi t 0 "㉒ --dry-run 印出要打的那通 API 但不送出" "一筆都沒有真的寫出去" -- \ close inkstone/A#12 --reason idle --dry-run t 2 "㉓ 里程碑編號寫錯 ⇒ 講清楚寫法" "寫法是 owner/repo#里程碑編號" -- audit ISEP-12 echo echo "通過 $PASS 條,失敗 $FAIL 條(共 $N 條)" [ "$FAIL" -eq 0 ]