diff --git a/install.sh b/install.sh index df239f8e..d2c16ec7 100755 --- a/install.sh +++ b/install.sh @@ -230,6 +230,7 @@ mkdir -p "$HOME/.claude/hooks" link_item "_markers" "$REPO_DIR/engine/hooks/_markers" "$HOME/.claude/hooks/_markers" link_item "_runner" "$REPO_DIR/engine/hooks/_runner" "$HOME/.claude/hooks/_runner" link_item "_flags" "$REPO_DIR/engine/hooks/_flags" "$HOME/.claude/hooks/_flags" +link_item "_sdk" "$REPO_DIR/engine/hooks/_sdk" "$HOME/.claude/hooks/_sdk" link_item "diu-stop" "$REPO_DIR/engine/hooks/diu-stop" "$HOME/.claude/hooks/diu-stop" link_item "bug-complaint-leak" "$REPO_DIR/engine/hooks/bug-complaint-leak" "$HOME/.claude/hooks/bug-complaint-leak" link_item "demo-freeze" "$REPO_DIR/engine/hooks/demo-freeze" "$HOME/.claude/hooks/demo-freeze" @@ -285,6 +286,8 @@ echo "--- cursor hooks dir (\$HOME/.cursor/hooks) ---" mkdir -p "$HOME/.cursor/hooks" link_item "_runner" "$REPO_DIR/engine/hooks/_runner" "$HOME/.cursor/hooks/_runner" link_item "_flags" "$REPO_DIR/engine/hooks/_flags" "$HOME/.cursor/hooks/_flags" +link_item "_sdk" "$REPO_DIR/engine/hooks/_sdk" "$HOME/.cursor/hooks/_sdk" +link_item "_markers" "$REPO_DIR/engine/hooks/_markers" "$HOME/.cursor/hooks/_markers" link_item "bug-complaint-leak" "$REPO_DIR/engine/hooks/bug-complaint-leak" "$HOME/.cursor/hooks/bug-complaint-leak" link_item "reflect-on-thrash" "$REPO_DIR/engine/hooks/reflect-on-thrash" "$HOME/.cursor/hooks/reflect-on-thrash" link_item "scope-lock" "$REPO_DIR/engine/hooks/scope-lock" "$HOME/.cursor/hooks/scope-lock" @@ -304,6 +307,8 @@ echo "--- codex hooks (\$HOME/.codex/hooks) ---" mkdir -p "$HOME/.codex/hooks" link_item "_runner" "$REPO_DIR/engine/hooks/_runner" "$HOME/.codex/hooks/_runner" link_item "_flags" "$REPO_DIR/engine/hooks/_flags" "$HOME/.codex/hooks/_flags" +link_item "_sdk" "$REPO_DIR/engine/hooks/_sdk" "$HOME/.codex/hooks/_sdk" +link_item "_markers" "$REPO_DIR/engine/hooks/_markers" "$HOME/.codex/hooks/_markers" link_item "diu-stop" "$REPO_DIR/engine/hooks/diu-stop" "$HOME/.codex/hooks/diu-stop" link_item "scope-lock" "$REPO_DIR/engine/hooks/scope-lock" "$HOME/.codex/hooks/scope-lock" link_item "auto-pr" "$REPO_DIR/engine/hooks/auto-pr" "$HOME/.codex/hooks/auto-pr" diff --git a/tests/test_install.py b/tests/test_install.py index bbb974db..ba9368c8 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -17,6 +17,7 @@ $REPO_DIR/engine/hooks -- correct, since that's the real content under test -- and nothing anywhere writes back into $REPO_DIR. """ +import glob import json import os import re @@ -1473,3 +1474,45 @@ def test_each_installer_survives_dangling_link(self): self.assertFalse(os.path.islink(hooks_path)) with open(hooks_path) as handle: json.load(handle) + + +class TestInstalledHookScriptsImport(unittest.TestCase): + SKIP_PREFIXES = ("install_", "test_") + SKIP_NAMES = ("detect.py", "state.py") + + def test_every_installed_hook_script_finds_its_shared_modules(self): + with tempfile.TemporaryDirectory() as fake_home, tempfile.TemporaryDirectory() as cwd: + proc = run_install(fake_home) + self.assertEqual(proc.returncode, 0, proc.stderr) + failures = [] + still_running = [] + checked = 0 + for harness in (".claude", ".cursor", ".codex"): + for path in sorted(glob.glob(os.path.join(fake_home, harness, "hooks", "*", "*.py"))): + hook = os.path.basename(os.path.dirname(path)) + name = os.path.basename(path) + if hook.startswith("_") or name.startswith(self.SKIP_PREFIXES) or name in self.SKIP_NAMES: + continue + checked += 1 + try: + result = subprocess.run( + [sys.executable, path], + input="{}", + env={**os.environ, "HOME": fake_home}, + cwd=cwd, + capture_output=True, + text=True, + timeout=5, + ) + except subprocess.TimeoutExpired as exc: + stderr = exc.stderr.decode() if isinstance(exc.stderr, bytes) else (exc.stderr or "") + if "ModuleNotFoundError" in stderr or "ImportError" in stderr: + failures.append(f"{harness}/hooks/{hook}/{name}: import error, then hung") + else: + still_running.append(f"{harness}/hooks/{hook}/{name}") + continue + if "ModuleNotFoundError" in result.stderr or "ImportError" in result.stderr: + failures.append(f"{harness}/hooks/{hook}/{name}: {result.stderr.strip().splitlines()[-1]}") + self.assertGreater(checked, 0) + self.assertLess(len(still_running), checked, still_running) + self.assertEqual(failures, [], "\n".join(failures))