surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
| 1 | """Pytest fixtures for hookify integration tests.""" |
| 2 | |
| 3 | import os |
| 4 | import sys |
| 5 | import json |
| 6 | import tempfile |
| 7 | import shutil |
| 8 | from pathlib import Path |
| 9 | from typing import Generator, Dict, Any, List |
| 10 | |
| 11 | import pytest |
| 12 | |
| 13 | # Add parent directories to path for imports |
| 14 | PLUGIN_ROOT = Path(__file__).parent.parent |
| 15 | PLUGINS_DIR = PLUGIN_ROOT.parent |
| 16 | sys.path.insert(0, str(PLUGINS_DIR)) |
| 17 | sys.path.insert(0, str(PLUGIN_ROOT)) |
| 18 | |
| 19 | from hookify.core.config_loader import Rule, Condition, load_rules, extract_frontmatter |
| 20 | from hookify.core.rule_engine import RuleEngine |
| 21 | |
| 22 | |
| 23 | @pytest.fixture |
| 24 | def rule_engine() -> RuleEngine: |
| 25 | """Create a RuleEngine instance.""" |
| 26 | return RuleEngine() |
| 27 | |
| 28 | |
| 29 | @pytest.fixture |
| 30 | def temp_project_dir() -> Generator[Path, None, None]: |
| 31 | """Create a temporary project directory with .claude folder. |
| 32 | |
| 33 | This fixture creates a clean temp directory and changes to it, |
| 34 | then restores the original directory after the test. |
| 35 | """ |
| 36 | original_dir = os.getcwd() |
| 37 | temp_dir = tempfile.mkdtemp(prefix="hookify_test_") |
| 38 | |
| 39 | # Create .claude directory for rule files |
| 40 | claude_dir = Path(temp_dir) / ".claude" |
| 41 | claude_dir.mkdir() |
| 42 | |
| 43 | os.chdir(temp_dir) |
| 44 | |
| 45 | yield Path(temp_dir) |
| 46 | |
| 47 | os.chdir(original_dir) |
| 48 | shutil.rmtree(temp_dir) |
| 49 | |
| 50 | |
| 51 | @pytest.fixture |
| 52 | def sample_rule_file(temp_project_dir: Path) -> Path: |
| 53 | """Create a sample rule file for testing.""" |
| 54 | rule_content = """--- |
| 55 | name: block-rm-rf |
| 56 | enabled: true |
| 57 | event: bash |
| 58 | action: block |
| 59 | conditions: |
| 60 | - field: command |
| 61 | operator: regex_match |
| 62 | pattern: rm\\s+-rf |
| 63 | --- |
| 64 | |
| 65 | **Dangerous command blocked!** |
| 66 | |
| 67 | The `rm -rf` command can permanently delete files. Please use safer alternatives. |
| 68 | """ |
| 69 | rule_file = temp_project_dir / ".claude" / "hookify.dangerous-commands.local.md" |
| 70 | rule_file.write_text(rule_content) |
| 71 | return rule_file |
| 72 | |
| 73 | |
| 74 | @pytest.fixture |
| 75 | def create_rule_file(temp_project_dir: Path): |
| 76 | """Factory fixture to create rule files with custom content.""" |
| 77 | def _create(name: str, content: str) -> Path: |
| 78 | rule_file = temp_project_dir / ".claude" / f"hookify.{name}.local.md" |
| 79 | rule_file.write_text(content) |
| 80 | return rule_file |
| 81 | return _create |
| 82 | |
| 83 | |
| 84 | @pytest.fixture |
| 85 | def sample_bash_input() -> Dict[str, Any]: |
| 86 | """Sample PreToolUse input for Bash tool.""" |
| 87 | return { |
| 88 | "session_id": "test-session-123", |
| 89 | "hook_event_name": "PreToolUse", |
| 90 | "tool_name": "Bash", |
| 91 | "tool_input": { |
| 92 | "command": "ls -la" |
| 93 | }, |
| 94 | "cwd": "/test/project" |
| 95 | } |
| 96 | |
| 97 | |
| 98 | @pytest.fixture |
| 99 | def sample_write_input() -> Dict[str, Any]: |
| 100 | """Sample PreToolUse input for Write tool.""" |
| 101 | return { |
| 102 | "session_id": "test-session-123", |
| 103 | "hook_event_name": "PreToolUse", |
| 104 | "tool_name": "Write", |
| 105 | "tool_input": { |
| 106 | "file_path": "/test/project/src/main.py", |
| 107 | "content": "print('hello world')" |
| 108 | }, |
| 109 | "cwd": "/test/project" |
| 110 | } |
| 111 | |
| 112 | |
| 113 | @pytest.fixture |
| 114 | def sample_edit_input() -> Dict[str, Any]: |
| 115 | """Sample PreToolUse input for Edit tool.""" |
| 116 | return { |
| 117 | "session_id": "test-session-123", |
| 118 | "hook_event_name": "PreToolUse", |
| 119 | "tool_name": "Edit", |
| 120 | "tool_input": { |
| 121 | "file_path": "/test/project/src/main.py", |
| 122 | "old_string": "hello", |
| 123 | "new_string": "goodbye" |
| 124 | }, |
| 125 | "cwd": "/test/project" |
| 126 | } |
| 127 | |
| 128 | |
| 129 | @pytest.fixture |
| 130 | def sample_multiedit_input() -> Dict[str, Any]: |
| 131 | """Sample PreToolUse input for MultiEdit tool.""" |
| 132 | return { |
| 133 | "session_id": "test-session-123", |
| 134 | "hook_event_name": "PreToolUse", |
| 135 | "tool_name": "MultiEdit", |
| 136 | "tool_input": { |
| 137 | "file_path": "/test/project/src/main.py", |
| 138 | "edits": [ |
| 139 | {"old_string": "foo", "new_string": "bar"}, |
| 140 | {"old_string": "baz", "new_string": "qux"} |
| 141 | ] |
| 142 | }, |
| 143 | "cwd": "/test/project" |
| 144 | } |
| 145 | |
| 146 | |
| 147 | @pytest.fixture |
| 148 | def sample_stop_input(temp_project_dir: Path) -> Dict[str, Any]: |
| 149 | """Sample Stop event input with transcript file.""" |
| 150 | # Create a transcript file |
| 151 | transcript_file = temp_project_dir / "transcript.txt" |
| 152 | transcript_file.write_text(""" |
| 153 | User: Please implement the feature |
| 154 | Assistant: I'll implement that feature now. |
| 155 | [Uses Write tool to create file] |
| 156 | User: Great, now run the tests |
| 157 | Assistant: Running tests... |
| 158 | [Uses Bash tool: npm test] |
| 159 | All tests passed! |
| 160 | """) |
| 161 | |
| 162 | return { |
| 163 | "session_id": "test-session-123", |
| 164 | "hook_event_name": "Stop", |
| 165 | "reason": "Task completed", |
| 166 | "transcript_path": str(transcript_file), |
| 167 | "cwd": str(temp_project_dir) |
| 168 | } |
| 169 | |
| 170 | |
| 171 | @pytest.fixture |
| 172 | def sample_userprompt_input() -> Dict[str, Any]: |
| 173 | """Sample UserPromptSubmit event input.""" |
| 174 | return { |
| 175 | "session_id": "test-session-123", |
| 176 | "hook_event_name": "UserPromptSubmit", |
| 177 | "user_prompt": "Please delete all files in the directory", |
| 178 | "cwd": "/test/project" |
| 179 | } |
| 180 | |
| 181 | |
| 182 | def make_rule( |
| 183 | name: str, |
| 184 | event: str, |
| 185 | conditions: List[Dict[str, str]], |
| 186 | action: str = "warn", |
| 187 | message: str = "Test message", |
| 188 | enabled: bool = True, |
| 189 | tool_matcher: str = None |
| 190 | ) -> Rule: |
| 191 | """Helper function to create Rule objects for testing.""" |
| 192 | cond_objects = [ |
| 193 | Condition( |
| 194 | field=c.get("field", ""), |
| 195 | operator=c.get("operator", "regex_match"), |
| 196 | pattern=c.get("pattern", "") |
| 197 | ) |
| 198 | for c in conditions |
| 199 | ] |
| 200 | return Rule( |
| 201 | name=name, |
| 202 | enabled=enabled, |
| 203 | event=event, |
| 204 | conditions=cond_objects, |
| 205 | action=action, |
| 206 | message=message, |
| 207 | tool_matcher=tool_matcher |
| 208 | ) |