Create test_claude_plugin_hook_wrappers.py

This commit is contained in:
fatkobra
2026-04-16 10:32:17 +02:00
committed by GitHub
parent d4c942417a
commit e083cd6c84
+161
View File
@@ -0,0 +1,161 @@
"""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()