"""Execution tests for Claude plugin hook wrapper scripts.""" import os import subprocess from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[1] PLUGIN_HOOKS_DIR = REPO_ROOT / ".claude-plugin" / "hooks" SCRIPT_CASES = [ ("mempal-stop-hook.sh", "stop"), ("mempal-precompact-hook.sh", "precompact"), ] def _write_executable(path: Path, content: str) -> None: path.write_text(content, encoding="utf-8") path.chmod(0o755) def _make_bin_dir(tmp_path: Path, executables: dict[str, str]) -> Path: bin_dir = tmp_path / "bin" bin_dir.mkdir() for name, content in executables.items(): _write_executable(bin_dir / name, content) return bin_dir def _run_hook(script_name: str, payload: str, bin_dir: Path) -> subprocess.CompletedProcess[str]: env = os.environ.copy() env["PATH"] = str(bin_dir) return subprocess.run( ["/bin/bash", str(PLUGIN_HOOKS_DIR / script_name)], input=payload, text=True, capture_output=True, cwd=REPO_ROOT, env=env, ) @pytest.mark.parametrize(("script_name", "hook_name"), SCRIPT_CASES) def test_plugin_hook_wrapper_prefers_mempalace_cli( tmp_path: Path, script_name: str, hook_name: str ) -> None: args_file = tmp_path / "args.txt" stdin_file = tmp_path / "stdin.json" bin_dir = _make_bin_dir( tmp_path, { "mempalace": ( "#!/bin/sh\n" f'printf \'%s\' "$*" > "{args_file}"\n' f'/bin/cat > "{stdin_file}"\n' "printf '{}\\n'\n" ), "python": "#!/bin/sh\nexit 99\n", "python3": "#!/bin/sh\nexit 99\n", }, ) payload = '{"session_id":"abc123"}' result = _run_hook(script_name, payload, bin_dir) assert result.returncode == 0 assert result.stdout == "{}\n" assert ( args_file.read_text(encoding="utf-8") == f"hook run --hook {hook_name} --harness claude-code" ) assert stdin_file.read_text(encoding="utf-8") == payload @pytest.mark.parametrize(("script_name", "hook_name"), SCRIPT_CASES) @pytest.mark.parametrize("python_name", ["python3", "python"]) def test_plugin_hook_wrapper_falls_back_to_importable_python( tmp_path: Path, script_name: str, hook_name: str, python_name: str ) -> None: args_file = tmp_path / "args.txt" stdin_file = tmp_path / "stdin.json" python_stub = ( "#!/bin/sh\n" 'if [ "$1" = "-c" ]; then\n' " exit 0\n" "fi\n" f'printf \'%s\' "$*" > "{args_file}"\n' f'/bin/cat > "{stdin_file}"\n' "printf '{}\\n'\n" ) bin_dir = _make_bin_dir(tmp_path, {python_name: python_stub}) payload = '{"session_id":"xyz789"}' result = _run_hook(script_name, payload, bin_dir) assert result.returncode == 0 assert result.stdout == "{}\n" assert ( args_file.read_text(encoding="utf-8") == f"-m mempalace hook run --hook {hook_name} --harness claude-code" ) assert stdin_file.read_text(encoding="utf-8") == payload @pytest.mark.parametrize(("script_name", "hook_name"), SCRIPT_CASES) def test_plugin_hook_wrapper_errors_cleanly_when_no_runner_exists( tmp_path: Path, script_name: str, hook_name: str ) -> None: bin_dir = _make_bin_dir(tmp_path, {}) payload = '{"session_id":"no-runner"}' result = _run_hook(script_name, payload, bin_dir) assert result.returncode != 0 assert result.stdout == "" assert "could not find a runnable mempalace command or module" in result.stderr @pytest.mark.parametrize(("script_name", "hook_name"), SCRIPT_CASES) def test_plugin_hook_wrapper_falls_back_to_python_when_python3_cannot_import( tmp_path: Path, script_name: str, hook_name: str ) -> None: args_file = tmp_path / "args.txt" stdin_file = tmp_path / "stdin.json" bad_python3_used = tmp_path / "bad_python3_used.txt" bin_dir = _make_bin_dir( tmp_path, { "python3": ( "#!/bin/sh\n" 'if [ "$1" = "-c" ]; then\n' " exit 1\n" "fi\n" f"printf 'used' > \"{bad_python3_used}\"\n" "echo 'No module named mempalace' >&2\n" "exit 1\n" ), "python": ( "#!/bin/sh\n" 'if [ "$1" = "-c" ]; then\n' " exit 0\n" "fi\n" f'printf \'%s\' "$*" > "{args_file}"\n' f'/bin/cat > "{stdin_file}"\n' "printf '{}\\n'\n" ), }, ) payload = '{"session_id":"fallback"}' result = _run_hook(script_name, payload, bin_dir) assert result.returncode == 0 assert result.stdout == "{}\n" assert ( args_file.read_text(encoding="utf-8") == f"-m mempalace hook run --hook {hook_name} --harness claude-code" ) assert stdin_file.read_text(encoding="utf-8") == payload assert not bad_python3_used.exists()