Files
wehub-resource-sync acf542cac6
Sync SKILL.md and rules / sync (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:07:10 +08:00

220 lines
8.6 KiB
Python

import json
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
class HookScriptTests(unittest.TestCase):
def run_cmd(self, cmd, home, extra_env=None):
env = os.environ.copy()
env.pop("CLAUDE_PLUGIN_ROOT", None)
env["HOME"] = str(home)
env["USERPROFILE"] = str(home)
if extra_env:
env.update(extra_env)
return subprocess.run(
cmd,
cwd=REPO_ROOT,
env=env,
text=True,
capture_output=True,
check=True,
)
def test_install_upgrades_old_two_file_install(self):
with tempfile.TemporaryDirectory(prefix="caveman-hooks-upgrade-") as tmp:
home = Path(tmp)
hooks_dir = home / ".claude" / "hooks"
hooks_dir.mkdir(parents=True)
(home / ".claude" / "settings.json").write_text("{}\n")
(hooks_dir / "caveman-activate.js").write_text("")
(hooks_dir / "caveman-mode-tracker.js").write_text("")
self.run_cmd(["bash", "src/hooks/install.sh"], home)
statusline = hooks_dir / "caveman-statusline.sh"
self.assertTrue(statusline.exists(), "upgrade should install statusline script")
settings = json.loads((home / ".claude" / "settings.json").read_text())
self.assertIn("statusLine", settings)
self.assertIn(str(statusline), settings["statusLine"]["command"])
def test_install_reconfigures_missing_statusline(self):
with tempfile.TemporaryDirectory(prefix="caveman-hooks-statusline-") as tmp:
home = Path(tmp)
claude_dir = home / ".claude"
hooks_dir = claude_dir / "hooks"
hooks_dir.mkdir(parents=True)
for name in ("caveman-activate.js", "caveman-mode-tracker.js", "caveman-statusline.sh"):
(hooks_dir / name).write_text("")
settings = {
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": f'node "{hooks_dir / "caveman-activate.js"}"',
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": f'node "{hooks_dir / "caveman-mode-tracker.js"}"',
}
]
}
],
}
}
(claude_dir / "settings.json").write_text(json.dumps(settings, indent=2) + "\n")
result = self.run_cmd(["bash", "src/hooks/install.sh"], home)
self.assertNotIn("Nothing to do", result.stdout)
updated = json.loads((claude_dir / "settings.json").read_text())
self.assertIn("statusLine", updated)
self.assertIn(str(hooks_dir / "caveman-statusline.sh"), updated["statusLine"]["command"])
def test_uninstall_preserves_custom_statusline(self):
with tempfile.TemporaryDirectory(prefix="caveman-hooks-uninstall-") as tmp:
home = Path(tmp)
claude_dir = home / ".claude"
hooks_dir = claude_dir / "hooks"
hooks_dir.mkdir(parents=True)
for name in ("caveman-activate.js", "caveman-mode-tracker.js", "caveman-statusline.sh"):
(hooks_dir / name).write_text("")
settings = {
"statusLine": {
"type": "command",
"command": "bash /tmp/custom-status-with-caveman.sh",
},
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": f'node "{hooks_dir / "caveman-activate.js"}"',
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": f'node "{hooks_dir / "caveman-mode-tracker.js"}"',
}
]
}
],
},
}
(claude_dir / "settings.json").write_text(json.dumps(settings, indent=2) + "\n")
self.run_cmd(["bash", "src/hooks/uninstall.sh"], home)
updated = json.loads((claude_dir / "settings.json").read_text())
self.assertEqual(
updated["statusLine"]["command"],
"bash /tmp/custom-status-with-caveman.sh",
)
self.assertNotIn("hooks", updated)
def test_activate_does_not_nudge_when_custom_statusline_exists(self):
with tempfile.TemporaryDirectory(prefix="caveman-hooks-activate-") as tmp:
home = Path(tmp)
claude_dir = home / ".claude"
claude_dir.mkdir(parents=True)
(claude_dir / "settings.json").write_text(
json.dumps(
{
"statusLine": {
"type": "command",
"command": "bash /tmp/my-statusline.sh",
}
}
)
+ "\n"
)
result = self.run_cmd(["node", "src/hooks/caveman-activate.js"], home)
self.assertNotIn("STATUSLINE SETUP NEEDED", result.stdout)
self.assertEqual((claude_dir / ".caveman-active").read_text(), "full")
# Regression for #587/#589 — hook at <root>/src/hooks/ must resolve SKILL.md
# at <root>/skills/caveman/, not the nonexistent <root>/src/skills/.
def test_activate_emits_skill_md_not_fallback_from_repo_layout(self):
with tempfile.TemporaryDirectory(prefix="caveman-hooks-skillpath-") as tmp:
home = Path(tmp)
(home / ".claude").mkdir(parents=True)
result = self.run_cmd(["node", "src/hooks/caveman-activate.js"], home)
# Intensity table exists only in SKILL.md, never in the fallback
self.assertIn("## Intensity", result.stdout)
# Default mode is full — table filtered to the active level's row
self.assertIn("| **full** |", result.stdout)
self.assertNotIn("| **lite** |", result.stdout)
def test_activate_finds_skill_beside_config_dir_hooks(self):
# Standalone layout: hooks at $CLAUDE_CONFIG_DIR/hooks/, skill installed
# at $CLAUDE_CONFIG_DIR/skills/caveman/SKILL.md
with tempfile.TemporaryDirectory(prefix="caveman-hooks-standalone-") as tmp:
home = Path(tmp)
claude_dir = home / ".claude"
hooks_dir = claude_dir / "hooks"
hooks_dir.mkdir(parents=True)
for name in ("caveman-activate.js", "caveman-config.js", "package.json"):
shutil.copy(REPO_ROOT / "src" / "hooks" / name, hooks_dir / name)
skill_dir = claude_dir / "skills" / "caveman"
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(
"---\nname: caveman\n---\nSTANDALONE MARKER RULESET\n"
)
result = self.run_cmd(["node", str(hooks_dir / "caveman-activate.js")], home)
self.assertIn("STANDALONE MARKER RULESET", result.stdout)
def test_activate_prefers_claude_plugin_root(self):
with tempfile.TemporaryDirectory(prefix="caveman-hooks-pluginroot-") as tmp:
home = Path(tmp)
(home / ".claude").mkdir(parents=True)
plugin_root = home / "plugin-cache"
skill_dir = plugin_root / "skills" / "caveman"
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(
"---\nname: caveman\n---\nPLUGIN ROOT MARKER RULESET\n"
)
result = self.run_cmd(
["node", "src/hooks/caveman-activate.js"],
home,
extra_env={"CLAUDE_PLUGIN_ROOT": str(plugin_root)},
)
self.assertIn("PLUGIN ROOT MARKER RULESET", result.stdout)
if __name__ == "__main__":
unittest.main()