ba444d1210
2026-08-27 實犯:`ticket where … >/dev/null 2>&1` 之後直接 `ticket new`—— 戳記寫成功了,命中的 72 張一眼沒看,於是開出 arcrun-rag#147, 而第一名 arcrun-rag#104 講的是同一件事,已經開了 13 天。 ⇒ 舊戳記證明的是「這個程序被執行過」,而「有沒有看」在 stdout 那一端, 閘本來完全碰不到。 做法:把 stdout 那一端變成機械事實,不是文字判斷。 - `fd_is_devnull(fd)`:fstat 問得出來的事實。刻意只認 /dev/null 這一種 (寫進檔案讀得回來、pipe 有下游,只有 /dev/null 物理上找不回來)。 - `where` 把 `shown` 記進戳記;順便替前 3 名補內文摘要——今天的實害正是 「光看標題看不出是同一條線」(#104 的標題完全沒提 ingest)。 - `new` 的閘一之二:命中 >0 且 shown 為假就擋,並把那份被丟掉的清單 交到眼前;只要 stderr 不是 /dev/null,這一次的擋就記成「看過了」, 重下一模一樣的指令就會過——成本落在「看」,不落在「寫」。 - 刻意排在閘二**之前**:否則第一次就帶 `--not-a-comment "理由"` 的人 永遠看不到候選清單,理由是閉著眼睛寫的。 - 舊格式戳記(沒有 shown 欄位)當成沒看過(fail-closed)。 不該擋的(測試覆蓋):沒命中就不吵、看過了就不吵、`--not-a-comment` 既有欄位照舊、不判斷理由寫得好不好(leo 2026-08-17 已證偽文字層判準)。 測試 17/17:scripts/test-ticket-where-seen-guard.sh(離線,不打真實 Gitea、 不留測試票;離開碼 2=擋、1=放行走到網路才炸)。docs/TESTING.md 新增 A13。 plugin.json 升到 0.8.0——版本沒動=沒有人吃得到。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
4.9 KiB
Bash
Executable File
117 lines
4.9 KiB
Bash
Executable File
#!/bin/bash
|
||
# 「戳記要證明看過,不是跑過」的測試(inkstone/ISEP#72 → comment 4873)
|
||
#
|
||
# 這支測的是 scripts/ticket 的閘一之二:搜尋跑了,但輸出被丟進 /dev/null 時,
|
||
# `ticket new` 要擋下來,並把那份被丟掉的候選清單交到眼前。
|
||
#
|
||
# 怎麼在「不真的開票」的前提下分辨擋/放行:
|
||
# TICKET_HOST 指到一個連不上的位址(127.0.0.1:9)
|
||
# 離開碼 2 = 閘擋下(die)
|
||
# 離開碼 1 = 閘全部放行,走到網路那一層才炸(Connection refused)
|
||
# 全程離線、不打真實 Gitea、不產生任何票。
|
||
cd "$(dirname "$0")/.." || exit 1
|
||
T=scripts/ticket
|
||
STAMP=/tmp/.ticket-where-ok
|
||
TMP=$(mktemp -d)
|
||
trap 'rm -rf "$TMP"' EXIT
|
||
PASS=0; FAIL=0
|
||
|
||
SAVED=""; [ -f "$STAMP" ] && SAVED=$(cat "$STAMP")
|
||
rm -f "$STAMP"
|
||
|
||
cat > "$TMP/body.md" <<'MD'
|
||
## 目標
|
||
測試用內文。
|
||
|
||
## 驗收條件
|
||
離開碼 2 = 被擋;離開碼 1 = 放行(走到網路才炸)。
|
||
|
||
## deliverable 類型
|
||
code
|
||
MD
|
||
TITLE="身為要開票的人,我要先看過命中的舊票,我才不會又開一張一樣的"
|
||
|
||
# stamp <n> <shown|none> <age_sec>
|
||
stamp() {
|
||
python3 - "$1" "$2" "$3" <<'PY'
|
||
import json, sys, time
|
||
n, shown, age = int(sys.argv[1]), sys.argv[2], float(sys.argv[3])
|
||
st = {"at": time.time() - age, "kws": ["ingest", "queue"], "n": n,
|
||
"top": ["inkstone/arcrun-rag#104"],
|
||
"top_detail": [{"ref": "inkstone/arcrun-rag#104", "score": 2,
|
||
"title": "它把我整個 repo 的一萬多個檔案排進佇列",
|
||
"labels": ["Human"],
|
||
"excerpt": "這些庫都早就萃好了,所以它要辨識這個庫已經有 wiki"}]}
|
||
if shown != "none":
|
||
st["shown"] = (shown == "true")
|
||
json.dump(st, open("/tmp/.ticket-where-ok", "w"))
|
||
PY
|
||
}
|
||
|
||
# run <want_rc> <說明> [額外參數...]
|
||
run() {
|
||
want=$1; what=$2; shift 2
|
||
out=$(TICKET_HOST=http://127.0.0.1:9 GITEA_TOKEN=x \
|
||
python3 "$T" new ISEP -F "$TMP/body.md" --title "$TITLE" "$@" 2>&1)
|
||
got=$?
|
||
LAST_OUT="$out"
|
||
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'want=%s got=%s %s\n' "$want" "$got" "$what"
|
||
}
|
||
|
||
has() { # has <字串> <說明>
|
||
if printf '%s' "$LAST_OUT" | grep -q "$1"; then PASS=$((PASS+1)); printf ' ✅ '
|
||
else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf '訊息裡有「%s」 %s\n' "$1" "$2"
|
||
}
|
||
|
||
echo "── 該擋 ──"
|
||
rm -f "$STAMP"
|
||
run 2 "完全沒搜過(既有閘一,回歸)"
|
||
stamp 3 true 3600
|
||
run 2 "戳記過期(既有閘一,回歸)"
|
||
stamp 72 false 10
|
||
run 2 "★ 今天的重演:where 的輸出被丟進 /dev/null,命中 72 張一眼沒看"
|
||
has "沒有到任何人眼前" "擋下的理由講的是「沒看」不是「沒搜」"
|
||
has "arcrun-rag#104" "擋下的訊息把被丟掉的候選清單交到眼前"
|
||
has "這些庫都早就萃好了" "連內文摘要都給了(光看標題看不出是同一條線)"
|
||
stamp 72 false 10
|
||
run 2 "★ 閉著眼睛先寫好理由也繞不過(--not-a-comment 在前面就被擋)" --not-a-comment "這是新的一條線"
|
||
stamp 72 none 10
|
||
run 2 "舊格式戳記(沒有 shown 欄位)→ fail-closed,當成沒看過" --not-a-comment "x"
|
||
stamp 72 true 10
|
||
run 2 "命中卻沒給理由(既有閘二,回歸)"
|
||
|
||
echo
|
||
echo "── 不該擋(誤攔比漏擋更該修)──"
|
||
stamp 72 true 10
|
||
run 1 "看過了、也給了理由 → 放行(走到網路才炸)" --not-a-comment "這是新的一條線"
|
||
stamp 0 false 10
|
||
run 1 "沒有命中任何票 → 沒東西可看,不要吵"
|
||
stamp 0 none 10
|
||
run 1 "沒命中 + 舊格式戳記 → 一樣不吵"
|
||
|
||
echo
|
||
echo "── 擋完之後:那一次的擋本身就是「把清單交到眼前」 ──"
|
||
stamp 72 false 10
|
||
run 2 "第一次:被擋" --not-a-comment "這是新的一條線"
|
||
run 1 "第二次下一模一樣的指令 → 放行(不必多打任何字)" --not-a-comment "這是新的一條線"
|
||
|
||
echo
|
||
echo "── fd_is_devnull:判準是 fstat 問出來的事實,不是猜文字 ──"
|
||
u() { # u <want> <說明> <shell 片段>
|
||
got=$(eval "$3")
|
||
if [ "$got" = "$2" ]; then PASS=$((PASS+1)); printf ' ✅ '; else FAIL=$((FAIL+1)); printf ' ❌ '; fi
|
||
printf 'want=%s got=%s %s\n' "$2" "$got" "$1"
|
||
}
|
||
PROBE="import importlib.util as u,sys; from importlib.machinery import SourceFileLoader as L; s=u.spec_from_loader('t',L('t','scripts/ticket')); m=u.module_from_spec(s); s.loader.exec_module(m); print(m.fd_is_devnull(1), file=sys.stderr)"
|
||
u "stdout 丟進 /dev/null → True" "True" "python3 -c \"\$PROBE\" >/dev/null 2>&1; python3 -c \"\$PROBE\" 2>&1 >/dev/null"
|
||
u "stdout 接到 pipe(Claude Code 就是這種)→ False" "False" "python3 -c \"\$PROBE\" 2>&1 | cat"
|
||
u "stdout 寫進一般檔案(讀得回來)→ False" "False" "python3 -c \"\$PROBE\" 2>&1 >\"$TMP/o.txt\""
|
||
|
||
rm -f "$STAMP"
|
||
[ -n "$SAVED" ] && printf '%s' "$SAVED" > "$STAMP"
|
||
echo
|
||
echo "通過 $PASS 條,失敗 $FAIL 條"
|
||
[ "$FAIL" = 0 ] || exit 1
|