diff --git a/docs/TESTING.md b/docs/TESTING.md index 677e70c..a1f06e2 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -165,11 +165,11 @@ bash hooks/tests/prod-write-guard.test.sh hooks/prod-write-guard.sh - 「卡點二」2 條任一紅 ⇒ 「談論它」又被當成「執行它」(同款第八次), 或是剝了內文之後連真的部署都放行了 -### A16 — 沒人會叫的事會自己叫:35 條 +### A16 — 沒人會叫的事會自己叫:36 條 ``` bash hooks/tests/overdue-nag.test.sh ``` -**該看到**:`通過 35 條,失敗 0 條`。**全程離線**——資料走 `--fixture`、時鐘走 `--now`、 +**該看到**:`通過 36 條,失敗 0 條`。**全程離線**——資料走 `--fixture`、時鐘走 `--now`、 網路用 `ISEP_NOTIFY_OFFLINE=1` 關掉。**不打 Gitea、不打實例、不留任何測試票。** **它在守什麼**(`inkstone/ISEP#93`,2026-08-27 實查):一張票掛著等 leo 三天了、 @@ -182,7 +182,9 @@ bash hooks/tests/overdue-nag.test.sh - ⑪ 紅 ⇒ 撈不到資料時把「我沒查到」講成「沒有事情逾期」——那是最貴的一種假情報 - **C 群任何一條紅 ⇒ 誤報**,這比漏報嚴重:每次開場都被念一串不相干的東西, 人就學會跳過它,那時真的有事也叫不動他 -- D 群紅 ⇒ **發不出去卻靜默**。「送出成功」跟「送到了」是兩件事 +- D 群紅 ⇒ **發不出去卻靜默**。「送出成功」跟「送到了」是兩件事。 + ㉒b 特別看:退路留言若只帶閘的判定、不帶**為什麼送不出去**, + 下一個人會去修閘——而斷點可能根本不在那裡(2026-08-28 第一版真的漏了這一格) - ㉖㉗㉘㉙ 紅 ⇒ 這個 session 的閘是哪一版變回「假設」。㉘ 特別重要: **閘說會擋就不准從 python 這條路繞過去**——繞得過的閘等於不存在 - ㉛ 紅 ⇒ 探針把 `/tmp/.prod-write-ok` 燒掉了(那是總管單次用完即丟的授權) @@ -421,7 +423,7 @@ B2(信標那行)/B3(setup 輸出)/B4(閘的訊息)三個畫面 | **A15 pr-verdict --dry-run** | 總管 | ✅(2026-08-28) | | **A14 回覆自己說出拖了多久** | 總管 | ✅ 20/20(2026-08-28,inkstone/ISEP#63) | | **A15 發通知不等於部署** | 總管 | ✅ 37/37(2026-08-28,inkstone/ISEP#63) | -| **A16 沒人會叫的事會自己叫** | 總管 | ✅ 35/35(2026-08-28,inkstone/ISEP#93) | +| **A16 沒人會叫的事會自己叫** | 總管 | ✅ 36/36(2026-08-28,inkstone/ISEP#93) | | **A16b Telegram 真的送達** | **leo** | ⛔ **驗不了——通道本身斷了**(見下) | | **A17 壓 wiki 不准弄丟東西** | 總管 | ✅ 25/25+真跡實壓(2026-08-28,inkstone/ISEP#89) | | A5 搜尋跨 repo | 總管 | ✅ | diff --git a/hooks/tests/overdue-nag.test.sh b/hooks/tests/overdue-nag.test.sh index 6ba03be..24678c5 100755 --- a/hooks/tests/overdue-nag.test.sh +++ b/hooks/tests/overdue-nag.test.sh @@ -19,6 +19,7 @@ set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$HERE/../.." && pwd)" NAG="$ROOT/scripts/isep-nag" +export NOTIFY_PATH="$ROOT/scripts/isep-notify" NOTIFY="$ROOT/scripts/isep-notify" HOOK="$ROOT/hooks/overdue-nag-guard.sh" @@ -128,6 +129,26 @@ has "$OUT" "測試訊息 ABC" && ok "㉑ 原文照抄在輸出裡(看到的人 || no "㉑ 原文照抄" "實得:$OUT" has "$OUT" "leo 的手機上沒有出現這則" && ok "㉒ 明著說 leo 沒收到(不是「送出成功」)" \ || no "㉒ 明著說沒收到" "實得:$OUT" +# 🔴 「為什麼發不出去」要跟著訊息一起留下來。2026-08-28 第一版漏了這一格: +# 閘明明 pass,退路留言裡卻只看得到 pass,真正的斷點(實例上沒有那支工作流) +# 一個字都沒進票 ⇒ 下一個人會去修閘,而斷點根本不在那裡。 +FB=$(python3 - <<'PYX' +import importlib.machinery, importlib.util, os +ld = importlib.machinery.SourceFileLoader("n", os.environ["NOTIFY_PATH"]) +sp = importlib.util.spec_from_loader("n", ld); m = importlib.util.module_from_spec(sp) +ld.exec_module(m) +fact = {"verdict": "pass", "version": "0.11.0", "guard": "/x/prod-write-guard.sh", + "confidence": "high", "source": "測試", "reason": "閘放行"} +body = m.fallback_body("要發的原文 XYZ", fact, + "通道本身斷了:實例上找不到 notify_leo 工作流") +ok = ("找不到 notify_leo" in body # 真正的斷點有進票 + and "要發的原文 XYZ" in body # 原文有進票 + and body.splitlines()[0].startswith("【身份】")) # 身份欄在第一行 +print("HAS_WHY" if ok else "NO_WHY:" + body[:120]) +PYX +) +[ "$FB" = "HAS_WHY" ] && ok "㉒b 退路留言帶著「為什麼發不出去」,不是只帶閘的判定" \ + || no "㉒b 退路帶原因" "實得:$FB" echo "── E 群:閘的版本是查得到的事實,不是假設 ────────────────────────" G=$(python3 "$NOTIFY" --gate 2>&1) diff --git a/scripts/isep-notify b/scripts/isep-notify index cc83b8e..6d50f33 100755 --- a/scripts/isep-notify +++ b/scripts/isep-notify @@ -273,7 +273,30 @@ def gitea_token(): return None -def comment_on_ticket(ref, text, gate_fact): +def fallback_body(text, gate_fact, why=None): + """退路留言的內文。**獨立成一支就是為了測得到**——這段的內容才是這條退路的價值。 + + 🔴 **為什麼發不出去**一定要在裡面,不是只寫閘的判定。 + 2026-08-28 第一版漏了這一格:閘明明 pass,票上卻只看得到 pass, + 而真正的斷點(實例上找不到 `notify_leo` 工作流)**一個字都沒進票** + ⇒ 下一個人會去修閘,而斷點根本不在那裡。 + """ + return "\n".join([ + IDENTITY, + "", + "🔴 **這則本來要用 Telegram 發給 leo,但這個 session 發不出去**——所以貼在這裡。", + "", + "**送不出去的原因**:%s" % (why or "(沒有記錄,這本身就是個 bug)"), + "", + gate_report(gate_fact), + "", + "---", + "", + text, + ]) + + +def comment_on_ticket(ref, text, gate_fact, why=None): """把那則發不出去的訊息貼回票上。回 (ok, 說明)。 🔴 **同一則不重複貼**:內容雜湊存在狀態目錄,24 小時內一樣就跳過。 @@ -284,7 +307,7 @@ def comment_on_ticket(ref, text, gate_fact): return False, "票的寫法是 owner/repo#N,你給的是:%s" % ref owner, repo, num = m.group(1), m.group(2), m.group(3) - key = hashlib.sha256(("%s|%s" % (ref, text)).encode()).hexdigest()[:16] + key = hashlib.sha256(("%s|%s|%s" % (ref, text, why or "")).encode()).hexdigest()[:16] seen = os.path.join(STATE_DIR, "commented-%s" % key) if os.path.exists(seen) and time.time() - os.path.getmtime(seen) < 24 * 3600: return True, "24 小時內已經貼過一模一樣的內容,跳過(不洗版)" @@ -295,17 +318,7 @@ def comment_on_ticket(ref, text, gate_fact): if not tok: return False, "拿不到 Gitea token(GITEA_TOKEN_CLAUDE_CODE 沒設,remote 也沒帶憑證)" - body = "\n".join([ - IDENTITY, - "", - "🔴 **這則本來要用 Telegram 發給 leo,但這個 session 發不出去**——所以貼在這裡。", - "", - gate_report(gate_fact), - "", - "---", - "", - text, - ]) + body = fallback_body(text, gate_fact, why) url = "%s/api/v1/repos/%s/%s/issues/%s/comments" % (GITEA_HOST, owner, repo, num) req = urllib.request.Request( url, data=json.dumps({"body": body}).encode(), method="POST", @@ -340,7 +353,7 @@ def notify(text, fallback_ticket=None, dry_run=False): if fact["verdict"] == "block": out.append("⛔ 閘說會擋 ⇒ **本支不繞路**,直接走退路。") - sent, why = False, "沒有嘗試(閘會擋,繞過去等於拆了那道閘)" + sent, why = False, "閘會擋,而繞過去等於拆了那道閘 ⇒ 沒有嘗試送出" else: sent, why = send_telegram(text) out.append(("✅ Telegram:%s" if sent else "❌ Telegram:%s") % why) @@ -350,7 +363,7 @@ def notify(text, fallback_ticket=None, dry_run=False): # ── 退路 ────────────────────────────────────────────────────────── if fallback_ticket: - ok, detail = comment_on_ticket(fallback_ticket, text, fact) + ok, detail = comment_on_ticket(fallback_ticket, text, fact, why) out.append(("✅ 退路(貼回票上):%s" if ok else "❌ 退路(貼回票上):%s") % detail) else: ok, detail = False, "沒有給 --fallback-ticket,沒有票可以貼"