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
14 changes: 12 additions & 2 deletions packages/claude-code-plugin/hooks/lib/prompt_injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
"""
from typing import Any, Dict

from runtime_mode import is_mcp_available

CHAR_LIMIT = 2000

# --- Section builders (each returns a string or empty) ---
Expand All @@ -16,6 +18,12 @@
"Follow TDD cycle: RED (failing test) -> GREEN (minimal code) -> REFACTOR."
)

_BASE_RULES_STANDALONE = (
"[CodingBuddy] When user message starts with PLAN/ACT/EVAL/AUTO, "
"follow the mode instructions provided by the mode detection hook. "
"Follow TDD cycle: RED (failing test) -> GREEN (minimal code) -> REFACTOR."
)

_DISPATCH_ENFORCEMENT = (
"[CodingBuddy] When parse_mode returns dispatch=\"auto\", "
"you MUST dispatch ALL recommended specialist agents. "
Expand Down Expand Up @@ -102,12 +110,14 @@ def build_system_prompt(self, config: Dict[str, Any], cwd: str) -> str:
if not isinstance(sections_cfg, dict):
sections_cfg = {}

mcp_mode = is_mcp_available()

parts: list[str] = []

if sections_cfg.get("baseRules", True):
parts.append(_BASE_RULES)
parts.append(_BASE_RULES if mcp_mode else _BASE_RULES_STANDALONE)

if sections_cfg.get("dispatchEnforcement", True):
if mcp_mode and sections_cfg.get("dispatchEnforcement", True):
parts.append(_DISPATCH_ENFORCEMENT)

if sections_cfg.get("qualityGates", True):
Expand Down
49 changes: 49 additions & 0 deletions packages/claude-code-plugin/tests/test_prompt_injection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for PromptInjector — system prompt injection module (#828)."""
import os
import sys
from unittest import mock
import pytest

# Ensure hooks/lib is on path
Expand All @@ -12,6 +13,13 @@
from prompt_injection import PromptInjector


@pytest.fixture(autouse=True)
def _mock_mcp_available():
"""Default: mock is_mcp_available to return True (MCP mode) for all existing tests."""
with mock.patch("prompt_injection.is_mcp_available", return_value=True):
yield


@pytest.fixture
def injector():
return PromptInjector()
Expand Down Expand Up @@ -225,3 +233,44 @@ def test_returns_empty_when_no_config(self, injector):
def test_returns_empty_when_none_config(self, injector):
result = injector.build_system_prompt({"promptInjection": None}, "/tmp")
assert result == ""


class TestStandaloneMode:
"""Tests for standalone mode (MCP not available)."""

@pytest.fixture(autouse=True)
def _standalone(self):
with mock.patch("prompt_injection.is_mcp_available", return_value=False):
yield

def test_standalone_contains_mode_hook_instruction(self, injector, base_config):
result = injector.build_system_prompt(base_config, "/tmp")
assert "follow the mode instructions provided by the mode detection hook" in result

def test_standalone_no_parse_mode_must(self, injector, base_config):
result = injector.build_system_prompt(base_config, "/tmp")
assert "MUST call parse_mode" not in result

def test_standalone_no_dispatch_enforcement(self, injector, base_config):
result = injector.build_system_prompt(base_config, "/tmp")
assert "dispatch" not in result.lower() or "dispatch=\"auto\"" not in result

def test_standalone_still_has_tdd(self, injector, base_config):
result = injector.build_system_prompt(base_config, "/tmp")
assert "TDD" in result

def test_standalone_still_has_quality_gates(self, injector, base_config):
result = injector.build_system_prompt(base_config, "/tmp")
assert "lint" in result.lower() or "test" in result.lower()


class TestMcpMode:
"""Explicit MCP mode tests — verify original behavior is preserved."""

def test_mcp_contains_parse_mode_must(self, injector, base_config):
result = injector.build_system_prompt(base_config, "/tmp")
assert "MUST call parse_mode FIRST" in result

def test_mcp_contains_dispatch_enforcement(self, injector, base_config):
result = injector.build_system_prompt(base_config, "/tmp")
assert 'dispatch="auto"' in result
Loading