Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions packages/claude-code-plugin/hooks/pre-tool-use.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@
_GIT_COMMIT_RE = re.compile(r"\bgit\s+commit\b")

QUALITY_GATE_CONTEXT = (
"[CodingBuddy Quality Gate] Before committing, ensure:\n"
"- All tests pass\n"
"- Code follows project conventions\n"
"- Changes are reviewed (self-review at minimum)\n"
"- Commit message follows project convention"
"[Quality Gate] Verify: tests pass, conventions followed, changes self-reviewed."
)

FILE_WATCHER_CONTEXT = (
Expand Down Expand Up @@ -144,26 +140,43 @@ def _get_staged_files() -> List[str]:


def _get_test_suggestion(staged_files: List[str]) -> Optional[str]:
"""Use SmartTestRunner to build a test-run suggestion for staged files."""
"""Use SmartTestRunner to build a compact test-run suggestion for staged files.

Returns a collapsed count instead of listing individual files (#1039).
"""
try:
from smart_test_runner import SmartTestRunner

runner = SmartTestRunner()
related = runner.find_related_tests(staged_files)
if not related:
return None
return runner.format_suggestion(related)
count = len(related)
return f"{count} related test(s) found — consider running before commit"
except Exception:
return None


def _get_checklist_warning(staged_files: List[str]) -> Optional[str]:
"""Use ChecklistVerifier to build a checklist warning for staged files (#1001)."""
"""Use ChecklistVerifier to build a compact checklist summary for staged files.

Returns collapsed domain counts instead of detailed items (#1039).
"""
try:
from checklist_verifier import ChecklistVerifier

verifier = ChecklistVerifier()
return verifier.verify(staged_files)
domains = verifier.detect_domains(staged_files)
if not domains:
return None
domain_counts = []
for domain in domains:
items = verifier.get_checklist_items(domain)
if items:
domain_counts.append(f"{domain}({len(items)})")
if not domain_counts:
return None
return f"[Checklist] {', '.join(domain_counts)}"
except Exception:
return None

Expand Down
92 changes: 84 additions & 8 deletions packages/claude-code-plugin/tests/test_pre_tool_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def test_git_commit_with_gates_disabled(self, monkeypatch, capsys):
assert result is None

def test_git_commit_with_gates_enabled_returns_context(self, monkeypatch, capsys):
"""When qualityGates.enabled is True, should return additionalContext."""
"""When qualityGates.enabled is True, should return compact additionalContext."""
config = {"qualityGates": {"enabled": True}}
result = _run_hook(
{"tool_name": "Bash", "tool_input": {"command": "git commit -m 'feat: test'"}},
Expand All @@ -123,8 +123,12 @@ def test_git_commit_with_gates_enabled_returns_context(self, monkeypatch, capsys
assert result is not None
assert "hookSpecificOutput" in result
hook_output = result["hookSpecificOutput"]
# Should have additionalContext with quality gate reminder
# Should have compact additionalContext with quality gate reminder (#1039)
assert "additionalContext" in hook_output
ctx = hook_output["additionalContext"]
assert "[Quality Gate]" in ctx
# Compact: single line, no bullet points
assert ctx.count("\n") == 0

def test_git_commit_amend_with_gates(self, monkeypatch, capsys):
"""git commit --amend should also trigger quality gates."""
Expand Down Expand Up @@ -160,7 +164,7 @@ class TestPreToolUseSmartTestRunner:
"""Tests for SmartTestRunner integration in pre-tool-use hook."""

def test_git_commit_injects_test_suggestion(self, monkeypatch, capsys, tmp_path):
"""git commit should inject related test suggestion into additionalContext."""
"""git commit should inject compact test count into additionalContext (#1039)."""
# Create a fake staged file list
monkeypatch.setattr(
"subprocess.check_output",
Expand All @@ -173,8 +177,9 @@ def test_git_commit_injects_test_suggestion(self, monkeypatch, capsys, tmp_path)
)
assert result is not None
ctx = result["hookSpecificOutput"]["additionalContext"]
assert "Consider running" in ctx
assert "foo.spec.ts" in ctx
# Collapsed format: count only, no individual file listing
assert "related test(s) found" in ctx
assert "foo.spec.ts" not in ctx

def test_git_commit_no_staged_files_no_suggestion(self, monkeypatch, capsys):
"""git commit with no staged files should not inject suggestion."""
Expand Down Expand Up @@ -203,7 +208,7 @@ def test_git_commit_config_files_only_no_suggestion(self, monkeypatch, capsys):
assert result is None

def test_git_commit_combines_quality_gate_and_test_suggestion(self, monkeypatch, capsys):
"""When both quality gates and test suggestion active, both in context."""
"""When both quality gates and test suggestion active, both in compact context (#1039)."""
monkeypatch.setattr(
"subprocess.check_output",
lambda *a, **kw: b"src/bar.ts\n",
Expand All @@ -215,7 +220,7 @@ def test_git_commit_combines_quality_gate_and_test_suggestion(self, monkeypatch,
)
assert result is not None
ctx = result["hookSpecificOutput"]["additionalContext"]
assert "Consider running" in ctx
assert "related test(s) found" in ctx
assert "Quality Gate" in ctx

def test_non_git_commit_no_test_suggestion(self, monkeypatch, capsys):
Expand All @@ -241,6 +246,77 @@ def _raise(*a, **kw):
assert result is None


class TestPreToolUseCompactOutput:
"""Tests for compact output format (#1039)."""

def test_checklist_collapsed_to_domain_counts(self, monkeypatch, capsys):
"""Checklist should show domain counts, not individual items (#1039)."""
monkeypatch.setattr(
"subprocess.check_output",
lambda *a, **kw: b"src/auth/login.ts\n",
)
config = {"qualityGates": {"enabled": False}}
result = _run_hook(
{"tool_name": "Bash", "tool_input": {"command": "git commit -m 'feat: auth'"}},
monkeypatch, capsys, config=config,
)
assert result is not None
ctx = result["hookSpecificOutput"]["additionalContext"]
# Collapsed: "[Checklist] security(5)" not individual items
assert "[Checklist]" in ctx
assert "security(" in ctx
# Should NOT contain individual checklist items
assert "Validate and sanitize" not in ctx

def test_quality_gate_compact_single_line(self, monkeypatch, capsys):
"""Quality gate should be a single line (#1039)."""
config = {"qualityGates": {"enabled": True}}
result = _run_hook(
{"tool_name": "Bash", "tool_input": {"command": "git commit -m 'test'"}},
monkeypatch, capsys, config=config,
)
assert result is not None
ctx = result["hookSpecificOutput"]["additionalContext"]
# Quality gate alone: single line, no newlines
assert "[Quality Gate]" in ctx
assert ctx.count("\n") == 0

def test_full_commit_output_max_5_lines(self, monkeypatch, capsys):
"""Combined output (gate + tests + checklist) should be max 5 lines (#1039)."""
monkeypatch.setattr(
"subprocess.check_output",
lambda *a, **kw: b"src/auth/login.ts\nsrc/api/users.ts\n",
)
config = {"qualityGates": {"enabled": True}}
result = _run_hook(
{"tool_name": "Bash", "tool_input": {"command": "git commit -m 'feat: all'"}},
monkeypatch, capsys, config=config,
)
assert result is not None
ctx = result["hookSpecificOutput"]["additionalContext"]
line_count = ctx.count("\n") + 1
assert line_count <= 5, f"Output has {line_count} lines, max is 5:\n{ctx}"

def test_test_suggestion_shows_count_not_files(self, monkeypatch, capsys):
"""Test suggestion should show count, not individual file paths (#1039)."""
monkeypatch.setattr(
"subprocess.check_output",
lambda *a, **kw: b"src/foo.ts\nsrc/bar.ts\nsrc/baz.ts\n",
)
config = {"qualityGates": {"enabled": False}}
result = _run_hook(
{"tool_name": "Bash", "tool_input": {"command": "git commit -m 'feat: multi'"}},
monkeypatch, capsys, config=config,
)
assert result is not None
ctx = result["hookSpecificOutput"]["additionalContext"]
# Should show count (3 files * ~5 candidates each = many, but deduplicated)
assert "related test(s) found" in ctx
# Should NOT list individual test files
assert ".spec.ts" not in ctx
assert ".test.ts" not in ctx


class TestPreToolUseStatusMessage:
"""Tests for agent statusMessage in hook output (#974)."""

Expand Down Expand Up @@ -325,4 +401,4 @@ def test_status_combined_with_quality_gate(self, monkeypatch, capsys, tmp_path):
hook_out = result["hookSpecificOutput"]
assert "statusMessage" in hook_out
assert "additionalContext" in hook_out
assert "Quality Gate" in hook_out["additionalContext"]
assert "[Quality Gate]" in hook_out["additionalContext"]
Loading