claude-harness/tests/test-doctor.sh
andrey.epifantsev 897fa0f3a7 feat: install.sh ставит allow-правила цикла фичи, --no-permissions для отказа (#24, 0.5.0)
Пересмотр решения владельца 2: allow-правила (git worktree, git branch
-m/-D, push фичевых и chore-веток, push --delete, tea issues/pulls
create/list/edit/comments) ставит install.sh в ~/.claude/settings.json,
а не владелец руками. Список — global/permissions.json; merge и push в
main в нём нет. Правила только дописываются, чужие не трогаются; копия
списка — ~/.claude/harness/permissions.json для doctor.sh.

- install.sh: шаг 5 — слияние permissions.allow; --no-permissions;
  неизвестный аргумент — ошибка; --check показывает permissions N/M.
- doctor.sh, «Машина»: ok / WARN со списком недостающих / MISS.
- adopt/new-project: строка push --delete — для --no-permissions.
- harness-doctor: агент чинит install.sh --no-permissions, права ставит
  владелец (иначе обход отказа классификатора auto mode).
- EnterWorktree не входит: вход в worktree вне .claude/worktrees/
  спрашивает подтверждение при любых правилах (#25).
- README, HARNESS.md, roles/dev.md, CLAUDE.md, ретро (пометка о
  пересмотре решения 2), CHANGELOG 0.5.0.

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

75 lines
7.8 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.

# doctor.sh: новые проверки — файлы ролей, самотест role-hint, режим workspace, размеры.
D="$ROOT/skills/harness-doctor/scripts/doctor.sh"
CC="$TMP/cc"; mkdir -p "$CC/hooks" "$CC/roles" "$CC/skills/role" "$CC/agents" "$CC/harness" "$CC/projects"
cp "$ROOT"/hooks/*.sh "$ROOT/hooks/harness_target.py" "$CC/hooks/"; chmod +x "$CC/hooks/"*.sh
mkdir -p "$CC/skills"; cp -R "$ROOT/skills/harness-doctor" "$CC/skills/"
cp "$ROOT"/roles/*.md "$CC/roles/"; cp "$ROOT/skills/role/SKILL.md" "$CC/skills/role/"; cp "$ROOT/agents/briefing.md" "$CC/agents/"
cp "$ROOT/link-roles.sh" "$ROOT/adopt-project.sh" "$ROOT/global/permissions.json" "$CC/harness/"; cp "$ROOT/global/CLAUDE.md" "$CC/CLAUDE.md"
printf '{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"\\"$HOME/.claude/hooks/role-hint.sh\\""}]}]}}' > "$CC/settings.json"
# workspace с одной целой и одной битой ссылкой
W="$TMP/ws"; mkdir -p "$W/admin/.claude/roles" "$W/.claude/roles" "$W/.claude/agents"
echo '# Роль: admin — x' > "$W/admin/.claude/roles/admin.md"
printf '# p\n\n| Каталог | Репозиторий | Роль |\n|---|---|---|\n| admin/ | o/a | admin |\n' > "$W/CLAUDE.md"
ln -s ../../admin/.claude/roles/admin.md "$W/.claude/roles/admin.md"
ln -s ../../qa/.claude/agents/none.md "$W/.claude/agents/none.md"
slug=$(printf '%s' "$W" | sed 's#[^A-Za-z0-9]#-#g'); mkdir -p "$CC/projects/$slug/memory"; head -c 4000 /dev/zero | tr '\0' 'x' > "$CC/projects/$slug/memory/MEMORY.md"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1)
assert_grep "машина: roles" 'ok +roles/ \(dev admin analyst qa\)' <(printf '%s' "$out")
assert_grep "машина: skill role" 'ok +skill role' <(printf '%s' "$out")
assert_grep "машина: агент briefing" 'ok +agent briefing' <(printf '%s' "$out")
assert_grep "машина: link-roles.sh" 'ok +.*link-roles.sh' <(printf '%s' "$out")
assert_grep "самотест: role-hint admin" 'ok +role-hint: заголовок admin' <(printf '%s' "$out")
assert_grep "самотест: role-hint тишина" 'ok +role-hint: без заголовка' <(printf '%s' "$out")
assert_grep "машина: harness_target.py" 'ok +hooks/harness_target\.py' <(printf '%s' "$out")
assert_grep "самотест: openspec-only коммит на main — блок" 'ok +guard-main: git commit openspec-only на main → exit 2' <(printf '%s' "$out")
assert_grep "самотест: Write на main" 'ok +guard-main: Write на main → exit 2' <(printf '%s' "$out")
assert_grep "самотест: Write на ветке" 'ok +guard-main: Write на ветке → exit 0' <(printf '%s' "$out")
assert_grep "корпус хуков сходится" 'ok +корпус хуков: [0-9]+/[0-9]+' <(printf '%s' "$out")
assert_grep "заголовок раздела проекта (M-8)" '^## Проект: /' <(printf '%s' "$out")
assert_grep "машина: правил обвязки нет — WARN с подсказкой install.sh (#24)" 'WARN +permissions обвязки: 0/[0-9]+.*bash install\.sh' <(printf '%s' "$out")
assert_grep "проект: режим workspace" 'workspace' <(printf '%s' "$out")
assert_grep "проект: целая ссылка" 'ok +roles/admin.md' <(printf '%s' "$out")
assert_grep "проект: битая ссылка" 'WARN +agents/none.md' <(printf '%s' "$out")
assert_grep "проект: индекс памяти > 3 КБ" 'WARN +индекс auto-memory' <(printf '%s' "$out")
assert_grep "workspace: нет allow push --delete — WARN с готовой строкой (B3)" 'WARN +нет allow-правила для git push origin --delete .*"Bash\(git push origin --delete:\*\)"' <(printf '%s' "$out")
# allow-правило push --delete (B3, решение владельца 2): git-проект, проектные и глобальные настройки
P="$TMP/p"; git init -q -b main "$P" && git -C "$P" commit -q --allow-empty -m init
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1)
assert_grep "git-проект: нет allow push --delete — WARN" 'WARN +нет allow-правила для git push origin --delete' <(printf '%s' "$out")
mkdir -p "$P/.claude"; printf '{"permissions":{"allow":["Bash(git push origin --delete:*)"]}}' > "$P/.claude/settings.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1)
assert_grep "git-проект: правило в .claude/settings.json — ok" 'ok +allow git push origin --delete \(.*/p/\.claude/settings\.json\)' <(printf '%s' "$out")
printf '{"permissions":{"allow":["Bash(git push origin --delete feat/:*)"]}}' > "$P/.claude/settings.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1)
assert_grep "git-проект: правило только для feat/ не покрывает — WARN" 'WARN +нет allow-правила для git push origin --delete' <(printf '%s' "$out")
printf '{"permissions":{"allow":["Bash(git push:*)"]}}' > "$P/.claude/settings.local.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1)
assert_grep "git-проект: широкое правило в settings.local.json — ok" 'ok +allow git push origin --delete \(.*settings\.local\.json\)' <(printf '%s' "$out")
rm -f "$P/.claude/settings.local.json"; printf '{"permissions":' > "$P/.claude/settings.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$P" 2>&1); rc=$?
assert_eq "git-проект: битый settings.json — doctor не падает" "$rc" "0"
assert_grep "git-проект: битый settings.json — info" '^ - +.*settings\.json — не JSON' <(printf '%s' "$out")
rm -f "$P/.claude/settings.json"; cp "$CC/settings.json" "$TMP/settings.bak"
printf '{"permissions":{"allow":["Bash(git push origin --delete:*)"]},"hooks":{}}' > "$CC/settings.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1)
assert_grep "workspace: правило в глобальном settings.json — ok" 'ok +allow git push origin --delete \(.*/cc/settings\.json\)' <(printf '%s' "$out")
cp "$TMP/settings.bak" "$CC/settings.json"
# правила обвязки (#24): все на месте — ok; нет harness/permissions.json — MISS
python3 -c 'import json,sys; s=json.load(open(sys.argv[1])); s["permissions"]=json.load(open(sys.argv[2]))["permissions"]; json.dump(s,open(sys.argv[1],"w"))' "$CC/settings.json" "$ROOT/global/permissions.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1)
assert_grep "машина: правила обвязки на месте — ok" 'ok +permissions обвязки: [0-9]+/[0-9]+' <(printf '%s' "$out")
rm "$CC/harness/permissions.json"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1)
assert_grep "машина: нет harness/permissions.json — MISS" 'MISS +permissions обвязки' <(printf '%s' "$out")
cp "$TMP/settings.bak" "$CC/settings.json"
rm -f "$CC/roles/qa.md"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W" 2>&1)
assert_grep "машина: MISS roles/qa.md" 'MISS +roles/qa.md' <(printf '%s' "$out")
# workspace с «выровненной» шапкой таблицы (лишние пробелы) определяется так же
W6="$TMP/ws6"; mkdir -p "$W6/admin/.claude/roles" "$W6/.claude/roles"
printf '# p\n\n| Каталог | Репозиторий | Роль |\n|---|---|---|\n| admin/ | o/a | admin |\n| | x | qa |\n| - | y | qa |\n' > "$W6/CLAUDE.md"
out=$(CLAUDE_CONFIG_DIR="$CC" HOME="$TMP/home" bash "$D" "$W6" 2>&1)
assert_grep "workspace с выровненной шапкой" 'workspace \(не git\)' <(printf '%s' "$out")
assert_grep "выровненная шапка: нет файла роли admin" 'нет admin/\.claude/roles/admin\.md' <(printf '%s' "$out")
assert_eq "пустой и «-» каталоги пропущены" "$(printf '%s' "$out" | grep -c 'нет .*roles/qa\.md')" "0"