|
| 1 | +"""#5747: Codacy Complexity gate checklist + soft classifier-edit hook.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import importlib.util |
| 6 | +import io |
| 7 | +import json |
| 8 | +import sys |
| 9 | +import tempfile |
| 10 | +import unittest |
| 11 | +from contextlib import redirect_stdout |
| 12 | +from pathlib import Path |
| 13 | + |
| 14 | +ROOT = Path(__file__).resolve().parents[2] |
| 15 | +GATE = ROOT / "chaos-engine/references/codacy-complexity-gate.md" |
| 16 | +LEVEL1 = ROOT / "chaos-engine/references/level-1-catalog.md" |
| 17 | +SKILL = ROOT / "chaos-engine/skills/chaos-engine/SKILL.md" |
| 18 | +PLAYBOOK = ROOT / "chaos-engine/references/work-github-playbook.md" |
| 19 | +GUARD = ROOT / "chaos-engine/hooks/guard.py" |
| 20 | + |
| 21 | + |
| 22 | +def load(path: Path, name: str): |
| 23 | + spec = importlib.util.spec_from_file_location(name, path) |
| 24 | + module = importlib.util.module_from_spec(spec) |
| 25 | + sys.modules[spec.name] = module |
| 26 | + spec.loader.exec_module(module) |
| 27 | + return module |
| 28 | + |
| 29 | + |
| 30 | +class CodacyComplexityGate5747Tests(unittest.TestCase): |
| 31 | + @classmethod |
| 32 | + def setUpClass(cls): |
| 33 | + cls.guard = load(GUARD, "ce_guard_codacy_5747") |
| 34 | + |
| 35 | + def test_checklist_documents_unit_red_parity_and_helpers(self): |
| 36 | + text = GATE.read_text(encoding="utf-8") |
| 37 | + self.assertIn("ACTION_REQUIRED", text) |
| 38 | + self.assertIn("unit", text.casefold()) |
| 39 | + self.assertIn("kind-family", text.casefold()) |
| 40 | + self.assertIn("classify*", text) |
| 41 | + self.assertIn("#5747", text) |
| 42 | + |
| 43 | + def test_catalog_and_router_point_at_checklist(self): |
| 44 | + level1 = LEVEL1.read_text(encoding="utf-8") |
| 45 | + skill = SKILL.read_text(encoding="utf-8") |
| 46 | + playbook = PLAYBOOK.read_text(encoding="utf-8") |
| 47 | + self.assertIn("codacy-complexity-gate.md", level1) |
| 48 | + self.assertIn("Codacy Complexity", level1) |
| 49 | + self.assertIn("codacy-complexity-gate.md", skill) |
| 50 | + self.assertIn("| Codacy Complexity |", skill) |
| 51 | + self.assertIn("codacy-complexity-gate.md", playbook) |
| 52 | + self.assertIn("ACTION_REQUIRED", playbook) |
| 53 | + |
| 54 | + def test_hint_fires_for_element_classifier_edit(self): |
| 55 | + hint = self.guard.classifier_complexity_gate_hint( |
| 56 | + "PreToolUse", |
| 57 | + mutation=True, |
| 58 | + tool_name="Edit", |
| 59 | + tool_input={ |
| 60 | + "file_path": ( |
| 61 | + "shaft-engine/src/main/java/com/shaft/gui/element/" |
| 62 | + "internal/interaction/ElementClassifier.java" |
| 63 | + ), |
| 64 | + "old_string": "a", |
| 65 | + "new_string": "b", |
| 66 | + }, |
| 67 | + ) |
| 68 | + self.assertIsNotNone(hint) |
| 69 | + self.assertIn("ACTION_REQUIRED", hint) |
| 70 | + self.assertIn("codacy-complexity-gate.md", hint) |
| 71 | + |
| 72 | + def test_hint_skips_unrelated_mutation(self): |
| 73 | + hint = self.guard.classifier_complexity_gate_hint( |
| 74 | + "PreToolUse", |
| 75 | + mutation=True, |
| 76 | + tool_name="Write", |
| 77 | + tool_input={"file_path": "README.md", "content": "hello"}, |
| 78 | + ) |
| 79 | + self.assertIsNone(hint) |
| 80 | + |
| 81 | + def test_hint_skips_non_mutation_and_non_pretool(self): |
| 82 | + self.assertIsNone( |
| 83 | + self.guard.classifier_complexity_gate_hint( |
| 84 | + "PreToolUse", |
| 85 | + mutation=False, |
| 86 | + tool_name="Edit", |
| 87 | + tool_input={"file_path": "ElementClassifier.java"}, |
| 88 | + ) |
| 89 | + ) |
| 90 | + self.assertIsNone( |
| 91 | + self.guard.classifier_complexity_gate_hint( |
| 92 | + "PostToolUse", |
| 93 | + mutation=True, |
| 94 | + tool_name="Edit", |
| 95 | + tool_input={"file_path": "ElementClassifier.java"}, |
| 96 | + ) |
| 97 | + ) |
| 98 | + |
| 99 | + def test_pretool_use_emits_soft_context_without_blocking(self): |
| 100 | + with tempfile.TemporaryDirectory() as temporary: |
| 101 | + event = { |
| 102 | + "hook_event_name": "PreToolUse", |
| 103 | + "session_id": "codacy-5747-soft", |
| 104 | + "cwd": temporary, |
| 105 | + "tool_name": "Edit", |
| 106 | + "tool_input": { |
| 107 | + "file_path": ( |
| 108 | + "shaft-engine/src/main/java/com/shaft/gui/element/" |
| 109 | + "internal/interaction/ElementClassifier.java" |
| 110 | + ), |
| 111 | + "old_string": "x", |
| 112 | + "new_string": "y", |
| 113 | + }, |
| 114 | + } |
| 115 | + buffer = io.StringIO() |
| 116 | + with redirect_stdout(buffer): |
| 117 | + code = self.guard._run_event(event, "claude") |
| 118 | + self.assertEqual(0, code) |
| 119 | + payload = json.loads(buffer.getvalue().strip().splitlines()[-1]) |
| 120 | + self.assertIn("additionalContext", payload) |
| 121 | + self.assertIn("ACTION_REQUIRED", payload["additionalContext"]) |
| 122 | + self.assertNotEqual("block", payload.get("decision")) |
| 123 | + |
| 124 | + |
| 125 | +if __name__ == "__main__": |
| 126 | + unittest.main() |
0 commit comments