"""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