feat(retro): корпус хуков по сегментам и known, выгрузка блокировок за период (#29)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
60d31c25db
commit
4d52ef773d
@ -1,14 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Выгрузка блокировок хуков из транскриптов Claude Code в JSONL — сырьё для регрессионных тестов хуков (B2).
|
||||
|
||||
Запуск: python3 hook_blocks_export.py <каталог-транскриптов> [<каталог> ...] > blocks.jsonl
|
||||
Запуск: python3 hook_blocks_export.py [--since ГГГГ-ММ-ДД] [--id-prefix <дата>] <каталог-транскриптов> [...]
|
||||
Одна строка — одна блокировка: сессия, время, cwd и ветка на момент вызова, инструмент, команда или путь файла,
|
||||
первая строка ответа хука. Поле expected не заполняется: вердикт (block/allow) проставляется при разметке.
|
||||
Выгружаются только ошибки хуков (результат начинается с `PreToolUse:<инструмент> hook error`); вывод команд,
|
||||
в котором встретилось слово BLOCKED (файлы памяти, исходники хуков), не попадает.
|
||||
Похожие на секреты подстроки заменяются на <redacted>.
|
||||
--since — только сессии с первой меткой времени не раньше даты; --id-prefix (по умолчанию — дата --since) —
|
||||
поле id вида <префикс>-NNN (срез ретро, B12). Похожие на секреты подстроки заменяются на <redacted>.
|
||||
"""
|
||||
import json, sys, glob, os, re
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HOOK_ERROR = re.compile(r"\s*PreToolUse:\S+ hook error")
|
||||
|
||||
@ -42,38 +47,80 @@ def hook_of(txt, tool):
|
||||
return "other"
|
||||
|
||||
|
||||
for d in sys.argv[1:]:
|
||||
for f in sorted(glob.glob(os.path.join(os.path.expanduser(d), "*.jsonl"))):
|
||||
sid = os.path.basename(f)[:8]
|
||||
pending = {} # tool_use_id -> (name, input, cwd, branch, ts)
|
||||
for line in open(f, encoding="utf-8", errors="replace"):
|
||||
try:
|
||||
j = json.loads(line)
|
||||
except Exception:
|
||||
def _records(path):
|
||||
for line in open(path, encoding="utf-8", errors="replace"):
|
||||
try:
|
||||
j = json.loads(line)
|
||||
except ValueError:
|
||||
continue
|
||||
if isinstance(j, dict):
|
||||
yield j
|
||||
|
||||
|
||||
def first_ts(path):
|
||||
for j in _records(path):
|
||||
if isinstance(j.get("timestamp"), str) and j["timestamp"]:
|
||||
return j["timestamp"]
|
||||
return ""
|
||||
|
||||
|
||||
def export(dirs, since=None, prefix=None):
|
||||
"""Записи выгрузки по каталогам транскриптов, по порядку файлов и строк."""
|
||||
n = 0
|
||||
for d in dirs:
|
||||
for f in sorted(glob.glob(os.path.join(os.path.expanduser(d), "*.jsonl"))):
|
||||
if since and first_ts(f)[:10] < since:
|
||||
continue
|
||||
m = j.get("message") or {}
|
||||
cont = m.get("content")
|
||||
if j.get("type") == "assistant":
|
||||
for b in cont or []:
|
||||
if isinstance(b, dict) and b.get("type") == "tool_use":
|
||||
pending[b.get("id")] = (b.get("name"), b.get("input") or {}, j.get("cwd", ""),
|
||||
j.get("gitBranch", ""), j.get("timestamp", ""))
|
||||
elif j.get("type") == "user" and isinstance(cont, list):
|
||||
for b in cont:
|
||||
if not (isinstance(b, dict) and b.get("type") == "tool_result"):
|
||||
continue
|
||||
txt = b.get("content", "")
|
||||
if isinstance(txt, list):
|
||||
txt = "\n".join(x.get("text", "") for x in txt if isinstance(x, dict))
|
||||
if not HOOK_ERROR.match(txt):
|
||||
continue # обычный вывод команды, даже если в нём есть слово BLOCKED
|
||||
name, inp, cwd, branch, ts = pending.get(b.get("tool_use_id"), ("?", {}, "", "", ""))
|
||||
if name not in ("Bash", "Edit", "Write", "MultiEdit"):
|
||||
continue
|
||||
msg = next((l for l in txt.splitlines() if "BLOCKED" in l), "")
|
||||
print(json.dumps({
|
||||
"session": sid, "ts": ts, "cwd": cwd, "branch": branch, "tool": name,
|
||||
"command": redact(inp.get("command", "")) if name == "Bash" else None,
|
||||
"file_path": inp.get("file_path") if name != "Bash" else None,
|
||||
"hook": hook_of(txt, name), "message": redact(msg[:300]), "expected": None,
|
||||
}, ensure_ascii=False))
|
||||
sid = os.path.basename(f)[:8]
|
||||
pending = {} # tool_use_id -> (name, input, cwd, branch, ts)
|
||||
for j in _records(f):
|
||||
m = j.get("message") or {}
|
||||
cont = m.get("content")
|
||||
if j.get("type") == "assistant":
|
||||
for b in cont or []:
|
||||
if isinstance(b, dict) and b.get("type") == "tool_use":
|
||||
pending[b.get("id")] = (b.get("name"), b.get("input") or {}, j.get("cwd", ""),
|
||||
j.get("gitBranch", ""), j.get("timestamp", ""))
|
||||
elif j.get("type") == "user" and isinstance(cont, list):
|
||||
for b in cont:
|
||||
if not (isinstance(b, dict) and b.get("type") == "tool_result"):
|
||||
continue
|
||||
txt = b.get("content", "")
|
||||
if isinstance(txt, list):
|
||||
txt = "\n".join(x.get("text", "") for x in txt if isinstance(x, dict))
|
||||
if not isinstance(txt, str) or not HOOK_ERROR.match(txt):
|
||||
continue # обычный вывод команды, даже если в нём есть слово BLOCKED
|
||||
name, inp, cwd, branch, ts = pending.get(b.get("tool_use_id"), ("?", {}, "", "", ""))
|
||||
if name not in ("Bash", "Edit", "Write", "MultiEdit"):
|
||||
continue
|
||||
msg = next((l for l in txt.splitlines() if "BLOCKED" in l), "")
|
||||
rec = {
|
||||
"session": sid, "ts": ts, "cwd": cwd, "branch": branch, "tool": name,
|
||||
"command": redact(inp.get("command", "")) if name == "Bash" else None,
|
||||
"file_path": inp.get("file_path") if name != "Bash" else None,
|
||||
"hook": hook_of(txt, name), "message": redact(msg[:300]), "expected": None,
|
||||
}
|
||||
if prefix:
|
||||
n += 1
|
||||
rec = dict({"id": "%s-%03d" % (prefix, n)}, **rec)
|
||||
yield rec
|
||||
|
||||
|
||||
def main(argv):
|
||||
args, dirs, since, prefix = argv[1:], [], None, None
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--since" and i + 1 < len(args):
|
||||
since, i = args[i + 1], i + 2
|
||||
elif args[i] == "--id-prefix" and i + 1 < len(args):
|
||||
prefix, i = args[i + 1], i + 2
|
||||
else:
|
||||
dirs.append(args[i])
|
||||
i += 1
|
||||
for rec in export(dirs, since, prefix or since):
|
||||
print(json.dumps(rec, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
||||
@ -2,19 +2,22 @@
|
||||
"""Сборка регрессионного корпуса хуков из сырой выгрузки блокировок (B2, #6).
|
||||
|
||||
Запуск из корня репозитория:
|
||||
python3 docs/retro/scripts/hook_cases_build.py > skills/harness-doctor/fixtures/hook-cases.jsonl
|
||||
Вход: docs/retro/fixtures/hook-blocks.jsonl (hook_blocks_export.py, 12.10.2026, 126 записей).
|
||||
Выход: первая строка — {"home": …, "layout": […]} (раскладка песочницы), дальше по записи на случай:
|
||||
поля выгрузки + id, expected (allow|block), why (класс). Вердикт размечен вручную по спеке
|
||||
python3 docs/retro/scripts/hook_cases_build.py [--fixtures-dir <каталог>] > skills/harness-doctor/fixtures/hook-cases.jsonl
|
||||
Вход: <fixtures>/hook-blocks.jsonl (hook_blocks_export.py, 12.10.2026, 126 записей) и пары ретро
|
||||
hook-blocks-<дата>.jsonl + hook-labels-<дата>.json (B12, #29).
|
||||
Выход — сегменты: строка {"home": …, "layout": […]} (раскладка песочницы), дальше по записи на случай:
|
||||
поля выгрузки + id, expected (allow|block), why (класс)[, known]. Первый сегмент — пилот 12.10 и живые случаи,
|
||||
дальше по сегменту на пару ретро; записи со skip не входят. Вердикт размечен вручную по спеке
|
||||
openspec/specs/hook-guards/spec.md, а не прогоном хука.
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
RAW = os.path.join(HERE, "..", "fixtures", "hook-blocks.jsonl")
|
||||
FIX = os.path.join(HERE, "..", "fixtures")
|
||||
HOME = "/Users/andrey.epifancev"
|
||||
AW, CH = "projects/awp-ca", "projects/cc-skills/claude-harness"
|
||||
|
||||
@ -77,9 +80,35 @@ def why_of(cid, r):
|
||||
return "devnull" if DEVNULL.search(r.get("command") or "") else "read-only"
|
||||
|
||||
|
||||
def main():
|
||||
def label_segments(fix):
|
||||
"""Сегменты ретро: пары hook-blocks-<дата>.jsonl + hook-labels-<дата>.json. Разметка должна совпадать с выгрузкой."""
|
||||
for lp in sorted(glob.glob(os.path.join(fix, "hook-labels-*.json"))):
|
||||
date = os.path.basename(lp)[len("hook-labels-"):-len(".json")]
|
||||
lab = json.load(open(lp, encoding="utf-8"))
|
||||
cases = lab.get("cases", {})
|
||||
recs = [json.loads(l) for l in open(os.path.join(fix, "hook-blocks-%s.jsonl" % date), encoding="utf-8")
|
||||
if l.strip()]
|
||||
ids = [r.get("id") for r in recs]
|
||||
missing, extra = [i for i in ids if i not in cases], sorted(set(cases) - set(ids))
|
||||
if missing or extra:
|
||||
raise SystemExit("hook-labels-%s.json: нет разметки для %s; лишняя разметка %s"
|
||||
% (date, ", ".join(missing) or "—", ", ".join(extra) or "—"))
|
||||
yield {"home": lab["home"], "layout": lab["layout"]}
|
||||
for r in recs:
|
||||
c = cases[r["id"]]
|
||||
if "skip" in c:
|
||||
continue
|
||||
r.update({"expected": c["expected"], "why": c["why"]})
|
||||
if c.get("known"):
|
||||
r["known"] = c["known"]
|
||||
yield r
|
||||
|
||||
|
||||
def main(argv):
|
||||
fix = argv[argv.index("--fixtures-dir") + 1] if "--fixtures-dir" in argv else FIX
|
||||
print(json.dumps({"home": HOME, "layout": LAYOUT}, ensure_ascii=False))
|
||||
for n, line in enumerate(l for l in open(RAW, encoding="utf-8") if l.strip()):
|
||||
raw = os.path.join(fix, "hook-blocks.jsonl")
|
||||
for n, line in enumerate(l for l in open(raw, encoding="utf-8") if l.strip()):
|
||||
r = json.loads(line)
|
||||
cid = "c%03d" % (n + 1)
|
||||
r.update({"id": cid, "expected": "block" if cid in BLOCK else "allow", "why": why_of(cid, r)})
|
||||
@ -88,7 +117,9 @@ def main():
|
||||
print(json.dumps({"id": cid, "session": "live-1015", "cwd": HOME + "/" + CH, "branch": "main",
|
||||
"tool": "Bash", "command": cmd, "file_path": None, "hook": "guard-main(Bash)",
|
||||
"message": "", "expected": "allow", "why": why}, ensure_ascii=False))
|
||||
for obj in label_segments(fix):
|
||||
print(json.dumps(obj, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main(sys.argv)
|
||||
|
||||
@ -115,7 +115,7 @@ rm -rf "$T"
|
||||
HC="$CC/skills/harness-doctor/scripts/hook_corpus.py"
|
||||
if [ -f "$HC" ] && [ -f "$CC/hooks/harness_target.py" ]; then
|
||||
out=$(python3 "$HC" "$CC/hooks" --summary 2>&1); rc=$?
|
||||
printf '%s\n' "$out" | grep '^MISMATCH' | sed 's/^MISMATCH / /'
|
||||
printf '%s\n' "$out" | grep -E '^(MISMATCH|KNOWN-FIXED) ' | sed 's/^/ /'
|
||||
[ "$rc" = 0 ] && ok "$(printf '%s\n' "$out" | tail -1)" || warn "$(printf '%s\n' "$out" | tail -1) — расхождения выше"
|
||||
else miss "корпус хуков (skills/harness-doctor/scripts/hook_corpus.py, hooks/harness_target.py)"; fi
|
||||
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
"""Регрессионный корпус хуков (B2): прогон fixtures/hook-cases.jsonl через guard-main и require-plan в песочнице.
|
||||
|
||||
Запуск: python3 hook_corpus.py <каталог хуков> [--summary] [--fixtures <jsonl>]
|
||||
Песочница строится во временном каталоге по раскладке из первой строки корпуса; префикс домашнего каталога записей
|
||||
заменяется на её home, HOME хуков подменяется туда же. Bash идёт через guard-main, Edit/Write — через guard-main и
|
||||
require-plan (блок, если блокирует любой).
|
||||
Вывод: строка на запись «id<TAB>why<TAB>ожидалось<TAB>получено<TAB>команда» и итог «total N/M»;
|
||||
с --summary — только расхождения и «корпус хуков: N/M». Код выхода 0 — всё совпало, 1 — нет.
|
||||
Корпус — сегменты: строка с ключом layout ({home, layout}) открывает сегмент, следующие строки — его записи.
|
||||
Для каждого сегмента строится своя песочница; префикс home записей заменяется на её home, HOME хуков — туда же.
|
||||
Bash идёт через guard-main, Edit/Write — через guard-main и require-plan (блок, если блокирует любой).
|
||||
Запись с known — известная ошибка хуков: расхождение — KNOWN (не ошибка прогона), совпадение — KNOWN-FIXED (ошибка:
|
||||
снять known). Вывод: строка «id<TAB>why<TAB>ожидалось<TAB>получено<TAB>статус<TAB>команда» и итог «total N/M»;
|
||||
с --summary — только MISMATCH и KNOWN-FIXED и «корпус хуков: N/M[, известных ошибок K]». Код 0 — нет ошибок.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
@ -52,6 +53,23 @@ def verdict(hooks, case, prefix, home, env):
|
||||
return "allow"
|
||||
|
||||
|
||||
def segments(lines):
|
||||
"""[(шапка, [записи])]: строка с ключом layout открывает сегмент."""
|
||||
segs = []
|
||||
for obj in lines:
|
||||
if "layout" in obj:
|
||||
segs.append((obj, []))
|
||||
elif segs:
|
||||
segs[-1][1].append(obj)
|
||||
return segs
|
||||
|
||||
|
||||
def status(case, got):
|
||||
if got == case["expected"]:
|
||||
return "KNOWN-FIXED" if case.get("known") else "ok"
|
||||
return "KNOWN" if case.get("known") else "MISMATCH"
|
||||
|
||||
|
||||
def main(argv):
|
||||
args = argv[1:]
|
||||
summary = "--summary" in args
|
||||
@ -64,29 +82,33 @@ def main(argv):
|
||||
return 1
|
||||
hooks = os.path.abspath(pos[0])
|
||||
lines = [json.loads(l) for l in open(fx, encoding="utf-8") if l.strip()]
|
||||
head, cases = lines[0], lines[1:]
|
||||
env0 = {k: v for k, v in os.environ.items() if k not in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE")}
|
||||
tmp = tempfile.mkdtemp(prefix="hook-corpus-")
|
||||
try:
|
||||
home = os.path.join(tmp, "home")
|
||||
try:
|
||||
build(home, head["layout"])
|
||||
except Exception as e:
|
||||
print("корпус хуков: песочница не собрана (%s)" % e)
|
||||
return 1
|
||||
env = {k: v for k, v in os.environ.items() if k not in ("GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE")}
|
||||
env["HOME"] = home
|
||||
ok = 0
|
||||
for c in cases:
|
||||
got = verdict(hooks, c, head["home"], home, env)
|
||||
ok += got == c["expected"]
|
||||
body = (c.get("command") or c.get("file_path") or "").replace(head["home"], "~")
|
||||
row = "\t".join([c["id"], c["why"], c["expected"], got, " ".join(body.split())[:70]])
|
||||
if not summary:
|
||||
print(row)
|
||||
elif got != c["expected"]:
|
||||
print("MISMATCH " + row)
|
||||
print(("корпус хуков: %d/%d" if summary else "total %d/%d") % (ok, len(cases)))
|
||||
return 0 if ok == len(cases) else 1
|
||||
total = good = known = 0
|
||||
for n, (head, cases) in enumerate(segments(lines)):
|
||||
home = os.path.join(tmp, "seg-%d" % n, "home")
|
||||
try:
|
||||
build(home, head["layout"])
|
||||
except Exception as e:
|
||||
print("корпус хуков: песочница сегмента %d не собрана (%s)" % (n + 1, e))
|
||||
return 1
|
||||
env = dict(env0, HOME=home)
|
||||
for c in cases:
|
||||
got = verdict(hooks, c, head["home"], home, env)
|
||||
st = status(c, got)
|
||||
total += 1
|
||||
good += st in ("ok", "KNOWN")
|
||||
known += st == "KNOWN"
|
||||
body = (c.get("command") or c.get("file_path") or "").replace(head["home"], "~")
|
||||
row = "\t".join([c["id"], c["why"], c["expected"], got, st, " ".join(body.split())[:70]])
|
||||
if not summary:
|
||||
print(row)
|
||||
elif st in ("MISMATCH", "KNOWN-FIXED"):
|
||||
print(st + " " + row)
|
||||
tail = ", известных ошибок %d" % known if known else ""
|
||||
print(("корпус хуков: %d/%d%s" if summary else "total %d/%d%s") % (good, total, tail))
|
||||
return 0 if good == total else 1
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
@ -4,7 +4,55 @@ F="$ROOT/skills/harness-doctor/fixtures/hook-cases.jsonl"
|
||||
assert_file "hook_corpus.py существует" "$R"
|
||||
assert_file "корпус hook-cases.jsonl" "$F"
|
||||
assert_eq "корпус совпадает со сборкой из выгрузки" "$(python3 "$ROOT/docs/retro/scripts/hook_cases_build.py" | cmp -s - "$F" && echo same)" "same"
|
||||
while IFS=$'\t' read -r id why exp got cmd; do
|
||||
while IFS=$'\t' read -r id why exp got st cmd; do
|
||||
case "$id" in total*) continue ;; esac
|
||||
assert_eq "корпус $id ($why): $cmd" "$got" "$exp"
|
||||
case "$st" in
|
||||
ok|KNOWN) ok "корпус $id ($why)" ;;
|
||||
*) fail "корпус $id ($why): $cmd" "ожидалось $exp, получено $got ($st)" ;;
|
||||
esac
|
||||
done < <(python3 "$R" "$ROOT/hooks")
|
||||
# сегменты и known (#29): синтетический корпус из двух сегментов с разными home
|
||||
S="$TMP/seg"; mkdir -p "$S"
|
||||
cat > "$S/c.jsonl" <<'EOF'
|
||||
{"home": "/h1", "layout": [{"path": "m", "branch": "main"}]}
|
||||
{"id": "a1", "why": "main", "tool": "Write", "file_path": "/h1/m/a.md", "cwd": "/h1/m", "expected": "block"}
|
||||
{"id": "a2", "why": "known-x", "tool": "Write", "file_path": "/h1/m/b.md", "cwd": "/h1/m", "expected": "allow", "known": "RT-21"}
|
||||
{"home": "/h2", "layout": [{"path": "r", "branch": "feat/x"}]}
|
||||
{"id": "b1", "why": "branch", "tool": "Write", "file_path": "/h2/r/a.md", "cwd": "/h2/r", "expected": "allow"}
|
||||
EOF
|
||||
out=$(python3 "$R" "$ROOT/hooks" --summary --fixtures "$S/c.jsonl"); rc=$?
|
||||
assert_eq "два сегмента и известная ошибка: exit 0" "$rc" "0"
|
||||
assert_eq "итог с известными ошибками" "$out" "корпус хуков: 3/3, известных ошибок 1"
|
||||
sed 's/"expected": "allow", "known"/"expected": "block", "known"/' "$S/c.jsonl" > "$S/f.jsonl"
|
||||
out=$(python3 "$R" "$ROOT/hooks" --summary --fixtures "$S/f.jsonl"); rc=$?
|
||||
assert_eq "known совпал с вердиктом: exit 1" "$rc" "1"
|
||||
assert_grep "KNOWN-FIXED с id записи" '^KNOWN-FIXED a2' <(printf '%s' "$out")
|
||||
# разметка ретро (#29): hook-labels-<дата>.json + hook-blocks-<дата>.jsonl → новый сегмент корпуса
|
||||
B="$ROOT/docs/retro/scripts/hook_cases_build.py"
|
||||
FX="$TMP/fx"; mkdir -p "$FX"; cp "$ROOT/docs/retro/fixtures/hook-blocks.jsonl" "$FX/"
|
||||
cat > "$FX/hook-blocks-2026-11-01.jsonl" <<'EOF'
|
||||
{"id": "2026-11-01-001", "session": "s", "ts": "2026-11-02T10:00:00Z", "cwd": "/h3/r", "branch": "main", "tool": "Bash", "command": "ls x 2>/dev/null", "file_path": null, "hook": "guard-main(Bash)", "message": "BLOCKED", "expected": null}
|
||||
{"id": "2026-11-01-002", "session": "s", "ts": "2026-11-02T10:01:00Z", "cwd": "/h3/r", "branch": "main", "tool": "Bash", "command": "echo x > f", "file_path": null, "hook": "guard-main(Bash)", "message": "BLOCKED", "expected": null}
|
||||
{"id": "2026-11-01-003", "session": "s", "ts": "2026-11-02T10:02:00Z", "cwd": "/h3/r", "branch": "main", "tool": "Bash", "command": "cat $X", "file_path": null, "hook": "guard-main(Bash)", "message": "BLOCKED", "expected": null}
|
||||
EOF
|
||||
cat > "$FX/hook-labels-2026-11-01.json" <<'EOF'
|
||||
{"home": "/h3", "layout": [{"path": "r", "branch": "main"}],
|
||||
"cases": {"2026-11-01-001": {"expected": "allow", "why": "devnull"},
|
||||
"2026-11-01-002": {"expected": "block", "why": "main-redirect"},
|
||||
"2026-11-01-003": {"skip": "цель в переменной, состояние неизвестно"}}}
|
||||
EOF
|
||||
out=$(python3 "$B" --fixtures-dir "$FX"); rc=$?
|
||||
assert_eq "сборка с разметкой ретро: exit 0" "$rc" "0"
|
||||
assert_eq "в корпусе два сегмента" "$(printf '%s\n' "$out" | grep -c '"layout"')" "2"
|
||||
assert_eq "сегмент ретро: две записи" "$(printf '%s\n' "$out" | grep -c '"2026-11-01-00[12]"')" "2"
|
||||
assert_eq "запись со skip не в корпусе" "$(printf '%s\n' "$out" | grep -c '2026-11-01-003')" "0"
|
||||
printf '%s\n' "$out" > "$FX/cases.jsonl"
|
||||
assert_eq "корпус с сегментом ретро проходит хуки" "$(python3 "$R" "$ROOT/hooks" --summary --fixtures "$FX/cases.jsonl" | tail -1)" "корпус хуков: 131/131"
|
||||
python3 - "$FX/hook-labels-2026-11-01.json" <<'PY'
|
||||
import json, sys
|
||||
p = sys.argv[1]; d = json.load(open(p)); del d["cases"]["2026-11-01-002"]; d["cases"]["2026-11-01-009"] = {"skip": "x"}
|
||||
json.dump(d, open(p, "w"))
|
||||
PY
|
||||
out=$(python3 "$B" --fixtures-dir "$FX" 2>&1 >/dev/null); rc=$?
|
||||
assert_eq "разметка не совпадает с выгрузкой: exit 1" "$rc" "1"
|
||||
assert_grep "сообщение называет недостающий и лишний id" '2026-11-01-002.*2026-11-01-009' <(printf '%s' "$out")
|
||||
|
||||
@ -13,3 +13,13 @@ out=$(python3 "$X" "$T")
|
||||
assert_eq "две записи: вывод cat с BLOCKED не выгружен" "$(printf '%s\n' "$out" | grep -c .)" "2"
|
||||
assert_eq "новый префикс → guard-main(Bash)" "$(printf '%s\n' "$out" | sed -n 1p | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["hook"], r["command"])')" "guard-main(Bash) echo x > f"
|
||||
assert_eq "старая фраза → guard-main(Edit/Write)" "$(printf '%s\n' "$out" | sed -n 2p | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["hook"], r["file_path"])')" "guard-main(Edit/Write) /r/a.md"
|
||||
# --since и --id-prefix (B12, #29): только сессии не раньше даты, id <дата>-NNN; модуль импортируется без выгрузки
|
||||
T2="$TMP/tr2"; mkdir -p "$T2"
|
||||
blk='{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"u1","is_error":true,"content":"PreToolUse:Bash hook error: BLOCKED [guard-main]: запись в f"}]}}'
|
||||
printf '%s\n%s\n' '{"type":"assistant","cwd":"/r","gitBranch":"main","timestamp":"2026-10-01T10:00:00Z","message":{"content":[{"type":"tool_use","id":"u1","name":"Bash","input":{"command":"echo a > f"}}]}}' "$blk" > "$T2/old.jsonl"
|
||||
printf '%s\n%s\n' '{"type":"assistant","cwd":"/r","gitBranch":"main","timestamp":"2026-11-02T10:00:00Z","message":{"content":[{"type":"tool_use","id":"u1","name":"Bash","input":{"command":"echo b > f"}}]}}' "$blk" > "$T2/new.jsonl"
|
||||
out=$(python3 "$X" --since 2026-11-01 --id-prefix 2026-11-05 "$T2")
|
||||
assert_eq "--since: только сессия не раньше даты" "$(printf '%s\n' "$out" | grep -c .)" "1"
|
||||
assert_eq "--id-prefix: id <дата>-NNN" "$(printf '%s\n' "$out" | python3 -c 'import json,sys; r=json.loads(sys.stdin.readline()); print(r["id"], r["command"])')" "2026-11-05-001 echo b > f"
|
||||
assert_eq "без флагов поля id нет (выгрузка 12.10 не меняется)" "$(python3 "$X" "$T2" | python3 -c 'import json,sys; print(sum("id" in json.loads(l) for l in sys.stdin))')" "0"
|
||||
assert_eq "импорт модуля не выгружает" "$(python3 -c "import sys; sys.argv=['x', '$T2']; sys.path.insert(0, '$ROOT/docs/retro/scripts'); import hook_blocks_export" 2>&1)" ""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user