claude-harness/tests/test-hook-guard-main.sh
andrey.epifantsev 2f96a13861 fix(hooks): финальное ревью — checkout main, префиксы с опциями, ссылки, литеральные пути, push текущей ветки (#6)
I-1, I-2, M-3, M-4, M-5 (переоценены в Important), M-8; спека и design.md синхронизированы (M-7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 02:24:07 +04:00

89 lines
8.0 KiB
Bash
Raw 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.

# guard-main.sh: решение по цели записи (спека hook-guards) в песочнице из настоящих git-репозиториев.
G="$ROOT/hooks/guard-main.sh"
S="$TMP/sb"; mkdir -p "$S/ws" "$HOME/.claude/projects/p/memory"
mk(){ git init -q "$1" && git -C "$1" symbolic-ref HEAD "refs/heads/${2:-main}" && git -C "$1" commit -q --allow-empty -m init; }
mk "$S/m" # репозиторий на main
mk "$S/f"; git -C "$S/f" worktree add -q "$S/wt" -b feat/6-x # основной checkout на main + worktree на ветке
mk "$S/ms" master # на master
mk "$S/d"; git -C "$S/d" checkout -q --detach # detached HEAD
j(){ python3 -c 'import json,sys; k="command" if sys.argv[2]=="Bash" else "file_path"; print(json.dumps({"cwd":sys.argv[1],"tool_name":sys.argv[2],"tool_input":{k:sys.argv[3]}}))' "$@"; }
rc(){ j "$@" | bash "$G" 2>/dev/null; echo $?; }
err(){ j "$@" | bash "$G" 2>&1 >/dev/null; }
# решение по цели, а не по cwd
assert_eq "чтение с 2>/dev/null на main" "$(rc "$S/m" Bash 'grep -rn "npm i\|npm install" . 2>/dev/null | head')" 0
assert_eq "fetch 2>&1 | tail на main" "$(rc "$S/m" Bash 'git fetch origin --prune 2>&1 | tail -3')" 0
assert_eq "Write вне репозитория при cwd на main" "$(rc "$S/m" Write "$HOME/.claude/projects/p/memory/x.md")" 0
assert_eq "Edit в worktree при cwd на main" "$(rc "$S/f" Edit "$S/wt/a.md")" 0
assert_eq "Edit в репозитории на main при cwd в worktree" "$(rc "$S/wt" Edit "$S/m/a.md")" 2
assert_eq "Write на master" "$(rc "$S/ws" Write "$S/ms/a.md")" 2
assert_eq "Write при detached HEAD" "$(rc "$S/ws" Write "$S/d/a.md")" 2
# файловые цели Bash
assert_eq "echo > в main" "$(rc "$S/m" Bash 'echo x > notes.md')" 2
assert_eq "make 2>build.log в main" "$(rc "$S/m" Bash 'make 2>build.log')" 2
assert_eq "sed -i по файлу main" "$(rc "$S/ws" Bash "sed -i 's/a/b/' $S/m/docs/HARNESS.md")" 2
assert_eq "sed -i '' по файлу памяти" "$(rc "$S/m" Bash "sed -i '' 's/a/b/' $HOME/.claude/projects/p/memory/x.md")" 0
assert_eq "docker … sh -c > /tmp" "$(rc "$S/m" Bash "docker compose exec -T db sh -c 'psql -c \"select 1\" > /tmp/x'")" 0
assert_eq "pip install на main" "$(rc "$S/m" Bash 'pip install requests')" 0
assert_eq "npm install на main" "$(rc "$S/m" Bash 'npm install')" 2
assert_eq "цель в переменной" "$(rc "$S/m" Bash 'echo x > "$OUT"')" 0
assert_eq "цель через \$HOME в main" "$(HOME="$S" rc "$S/ws" Bash 'echo x > $HOME/m/f')" 2
# каталог внутри команды
assert_eq "cd в worktree и запись" "$(rc "$S/f" Bash "cd $S/wt && echo x > f")" 0
assert_eq "cd в main и touch" "$(rc "$S/wt" Bash "cd $S/m && touch f")" 2
assert_eq "mkdir -p .wt/ в не-git workspace" "$(rc "$S/ws" Bash 'mkdir -p .wt/6-x')" 0
assert_eq "bash -c с записью в main" "$(rc "$S/m" Bash "bash -c 'echo x > f'")" 2
# git
assert_eq "add && commit в main" "$(rc "$S/m" Bash 'git add -A && git commit -m x')" 2
assert_eq "commit -C worktree при cwd на main" "$(rc "$S/f" Bash "git -C $S/wt commit -m x")" 0
assert_eq "worktree add -C из workspace" "$(rc "$S/ws" Bash "git -C $S/m worktree add ../.wt/x -b feat/x")" 0
assert_eq "pull --ff-only на main" "$(rc "$S/m" Bash 'git pull --ff-only')" 0
assert_eq "stash list на main" "$(rc "$S/m" Bash 'git stash list')" 0
assert_eq "-C в переменной — блок" "$(rc "$S/ws" Bash 'git -C "$REPO" commit -m x')" 2
assert_grep "-C в переменной — подсказка git -C" 'git -C <абсолютный путь>' <(err "$S/ws" Bash 'git -C "$REPO" commit -m x')
assert_eq "switch -c && commit на main" "$(rc "$S/m" Bash 'git switch -q -c chore/archive-x && git add openspec && git commit -m a')" 0
assert_eq "switch -c && touch на main" "$(rc "$S/m" Bash 'git switch -q -c feat/16 && touch .bounded')" 0
assert_eq "switch в переменную && commit — блок" "$(rc "$S/m" Bash 'git switch "$B" && git commit -m x')" 2
assert_eq "switch в переменную && touch — пропуск" "$(rc "$S/m" Bash 'git switch "$B" && touch f')" 0
assert_eq "commit вне репозитория" "$(rc "$S/ws" Bash 'git commit -m x')" 0
# push
assert_eq "push с main" "$(rc "$S/m" Bash 'git push')" 2
assert_eq "push --delete фичи с main" "$(rc "$S/m" Bash 'git push origin --delete feat/6-x')" 0
assert_eq "push фичи с main" "$(rc "$S/m" Bash 'git push origin feat/6-x')" 0
assert_eq "push HEAD:main из worktree" "$(rc "$S/wt" Bash 'git push origin HEAD:main')" 2
assert_eq "push из worktree" "$(rc "$S/wt" Bash 'git push -u origin HEAD')" 0
# без исключения для openspec (решение владельца 1)
mkdir -p "$S/m/openspec"; echo a > "$S/m/openspec/x.md"; git -C "$S/m" add openspec
assert_eq "commit только openspec/ в main" "$(rc "$S/m" Bash 'git commit -m archive')" 2
git -C "$S/m" reset -q
assert_eq "openspec archive в main" "$(rc "$S/m" Bash 'openspec archive x')" 2
# граничные случаи (Review Focus)
ln -s "$S/wt/CLAUDE.md" "$S/ws/CLAUDE.md"; ln -s "$S/m/CLAUDE.md" "$S/ws/MAIN.md"
assert_eq "запись через симлинк в worktree" "$(rc "$S/ws" Write "$S/ws/CLAUDE.md")" 0
assert_eq "запись через симлинк в main" "$(rc "$S/ws" Write "$S/ws/MAIN.md")" 2
assert_eq "пустой cwd и относительный путь — пропуск" "$(rc "" Write "a.md")" 0
assert_eq "запись внутри .git/ — не рабочее дерево" "$(rc "$S/m" Bash 'echo x >> .git/info/exclude')" 0
assert_eq "путь с пробелом в main" "$(rc "$S/m" Bash 'touch "a b.md"')" 2
# финальное ревью: I-1, I-2, M-3, M-4, M-5
assert_eq "checkout main && merge && push из worktree (I-1)" "$(rc "$S/wt" Bash 'git checkout main && git pull && git merge --no-ff feat/6-x && git push')" 2
assert_eq "checkout master && commit (I-1)" "$(rc "$S/wt" Bash 'git checkout master && git commit -am x')" 2
assert_eq "switch - && merge (I-1)" "$(rc "$S/wt" Bash 'git switch - && git merge x')" 2
assert_eq "timeout 60 git push origin main (I-2)" "$(rc "$S/wt" Bash 'timeout 60 git push origin main')" 2
assert_eq "nice -n 5 git commit на main (I-2)" "$(rc "$S/m" Bash 'nice -n 5 git commit -m x')" 2
ln -s "$S/m" "$S/ws/mlink"; ln -s /etc/hosts "$S/m/LINK.md"
assert_eq "rm симлинка в workspace на main-репозиторий (M-3)" "$(rc "$S/ws" Bash "rm $S/ws/mlink")" 0
assert_eq "ln -sfn поверх симлинка в workspace (M-3)" "$(rc "$S/ws" Bash "ln -sfn $S/m $S/ws/mlink")" 0
assert_eq "rm симлинка внутри main, указывающего наружу (M-3)" "$(rc "$S/m" Bash 'rm LINK.md')" 2
assert_grep "путь Edit с [скобками] не режется (M-4)" 'docs/\[x\]\.md' <(err "$S/ws" Write "$S/m/docs/[x].md")
assert_eq "push текущей ветки через подстановку из worktree (M-5)" "$(rc "$S/wt" Bash 'git push -u origin "$(git branch --show-current)"')" 0
assert_eq "push --tags с main (M-5)" "$(rc "$S/m" Bash 'git push --tags')" 0
# сбой разбора и сообщение
assert_eq "не JSON — пропуск" "$(printf 'not json' | bash "$G" 2>/dev/null; echo $?)" 0
assert_eq "незакрытая кавычка — пропуск" "$(rc "$S/m" Bash 'echo "x > f')" 0
mkdir -p "$TMP/nopy"
assert_eq "нет python3 — пропуск" "$(env PATH="$TMP/nopy" /bin/bash "$G" <<< "$(j "$S/m" Write "$S/m/a.md")" 2>/dev/null; echo $?)" 0
m=$(err "$S/ws" Write "$S/m/docs/HARNESS.md")
assert_grep "сообщение: префикс" '^BLOCKED \[guard-main\]:' <(printf '%s' "$m")
assert_grep "сообщение: путь отн. корня" 'docs/HARNESS.md' <(printf '%s' "$m")
assert_grep "сообщение: корень и worktree" "$(basename "$S")/m на main.*worktree" <(printf '%s' "$m")
assert_grep "settings.hooks.json регистрирует guard-main" 'guard-main\.sh' "$ROOT/hooks/settings.hooks.json"