agnes-the-ai-analyst/tests/test_lib_hooks.py
ZdenekSrotyr be62ce61b8 feat(cli): install SessionStart hook chaining self-upgrade then pull
Single hook entry: 'agnes self-upgrade --quiet ... || true; agnes pull
--quiet ... || true'. Shell semicolon guarantees ordering across every
Claude Code version (no reliance on undocumented multi-hook execution
semantics); each segment's || true preserves the original property
that an upgrade failure does not abort the pull.
2026-05-06 23:23:23 +02:00

98 lines
3.8 KiB
Python

"""Tests for cli/lib/hooks.py:install_claude_hooks."""
import json
from pathlib import Path
from cli.lib.hooks import install_claude_hooks
def _read_settings(workspace: Path) -> dict:
return json.loads((workspace / ".claude" / "settings.json").read_text())
def test_install_creates_settings_file(tmp_path):
install_claude_hooks(tmp_path)
cfg = _read_settings(tmp_path)
cmd = cfg["hooks"]["SessionStart"][0]["hooks"][0]["command"]
assert "agnes self-upgrade --quiet" in cmd
assert "agnes pull --quiet" in cmd
assert "agnes push --quiet" in cfg["hooks"]["SessionEnd"][0]["hooks"][0]["command"]
def test_install_idempotent(tmp_path):
install_claude_hooks(tmp_path)
install_claude_hooks(tmp_path)
cfg = _read_settings(tmp_path)
assert len(cfg["hooks"]["SessionStart"]) == 1
assert len(cfg["hooks"]["SessionEnd"]) == 1
def test_install_replaces_old_da_sync_entries(tmp_path):
"""Hook from a pre-rewrite workspace gets replaced cleanly."""
settings_path = tmp_path / ".claude" / "settings.json"
settings_path.parent.mkdir(parents=True)
settings_path.write_text(json.dumps({
"hooks": {
"SessionStart": [{"hooks": [{"type": "command", "command": "da sync --quiet"}]}],
"SessionEnd": [{"hooks": [{"type": "command", "command": "da sync --upload-only --quiet"}]}],
}
}))
install_claude_hooks(tmp_path)
cfg = _read_settings(tmp_path)
assert len(cfg["hooks"]["SessionStart"]) == 1
assert "agnes pull" in cfg["hooks"]["SessionStart"][0]["hooks"][0]["command"]
assert "da sync" not in cfg["hooks"]["SessionStart"][0]["hooks"][0]["command"]
def test_install_preserves_third_party_hooks(tmp_path):
settings_path = tmp_path / ".claude" / "settings.json"
settings_path.parent.mkdir(parents=True)
settings_path.write_text(json.dumps({
"hooks": {
"SessionStart": [{"hooks": [{"type": "command", "command": "echo hi from another tool"}]}],
"PreToolUse": [{"hooks": [{"type": "command", "command": "echo pre"}]}],
}
}))
install_claude_hooks(tmp_path)
cfg = _read_settings(tmp_path)
starts = cfg["hooks"]["SessionStart"]
assert any("echo hi from another tool" in s["hooks"][0]["command"] for s in starts)
assert any("agnes pull" in s["hooks"][0]["command"] for s in starts)
assert cfg["hooks"]["PreToolUse"][0]["hooks"][0]["command"] == "echo pre"
def test_install_handles_missing_settings_file(tmp_path):
install_claude_hooks(tmp_path)
assert (tmp_path / ".claude" / "settings.json").exists()
def test_install_handles_invalid_json(tmp_path, capsys):
settings_path = tmp_path / ".claude" / "settings.json"
settings_path.parent.mkdir(parents=True)
settings_path.write_text("not valid json {")
install_claude_hooks(tmp_path)
captured = capsys.readouterr()
assert "not valid JSON" in captured.err or "warning" in captured.err.lower()
def test_install_chains_self_upgrade_then_pull_in_one_entry(tmp_path):
install_claude_hooks(tmp_path)
cfg = _read_settings(tmp_path)
session_start = cfg["hooks"]["SessionStart"]
assert len(session_start) == 1, session_start
cmd = session_start[0]["hooks"][0]["command"]
assert "agnes self-upgrade --quiet" in cmd
assert "agnes pull --quiet" in cmd
# Order is encoded in the shell — self-upgrade must appear first
assert cmd.index("agnes self-upgrade") < cmd.index("agnes pull")
# Both segments carry || true so neither failure aborts the line
assert cmd.count("|| true") >= 2
def test_install_idempotent_chained_entry(tmp_path):
install_claude_hooks(tmp_path)
install_claude_hooks(tmp_path)
cfg = _read_settings(tmp_path)
assert len(cfg["hooks"]["SessionStart"]) == 1
assert len(cfg["hooks"]["SessionEnd"]) == 1