Adds the project-review workflow for code review independent of the git diff. The scope is parsed from the user instruction: 全面/整个项目 -> whole-project (score every source file), otherwise feature + target keyword (locate the code with semantic search + graph queries). - scoring_tools.py: score_review_func gains all_files=True to score every source file in the graph via store.get_all_files() - main.py: score_review_tool gains all_files param; registers the project_review MCP prompt (prompts 6->7) - prompts.py: project_review_prompt(scope, target) with whole-project and feature branches (fixed a precedence bug that truncated the feature text) - skills.py + skills/project-review/: new read-only project-review skill with shared checklists - .opencode/command/code-review-graph-project-review.md: slash command - tests: test_project_review.py (prompt rendering), TestProjectReviewPrompt, skill count assertions 5->6, all_files wiring checks - docs: prompts (6->7) + project-review entries across COMMANDS, CLAUDE, README (+localized), INDEX, architecture, LLM-OPTIMIZED-REFERENCE, CHANGELOG
301 lines
9.6 KiB
Python
301 lines
9.6 KiB
Python
"""Tests for MCP prompt templates."""
|
|
|
|
from fastmcp.prompts.prompt import Message
|
|
|
|
from code_review_graph.prompts import (
|
|
architecture_map_prompt,
|
|
debug_issue_prompt,
|
|
onboard_developer_prompt,
|
|
pre_merge_check_prompt,
|
|
project_review_prompt,
|
|
review_changes_prompt,
|
|
unified_review_prompt,
|
|
)
|
|
|
|
|
|
def _text(msg: Message) -> str:
|
|
"""Extract the text content from a fastmcp Message."""
|
|
return msg.content.text
|
|
|
|
|
|
class TestReviewChangesPrompt:
|
|
def test_returns_list_with_messages(self):
|
|
result = review_changes_prompt()
|
|
assert isinstance(result, list)
|
|
assert len(result) >= 1
|
|
|
|
def test_message_has_role_and_content(self):
|
|
result = review_changes_prompt()
|
|
for msg in result:
|
|
assert isinstance(msg, Message)
|
|
assert msg.role == "user"
|
|
assert _text(msg)
|
|
|
|
def test_default_base(self):
|
|
result = review_changes_prompt()
|
|
assert "HEAD~1" in _text(result[0])
|
|
|
|
def test_custom_base(self):
|
|
result = review_changes_prompt(base="main")
|
|
assert "main" in _text(result[0])
|
|
|
|
def test_mentions_detect_changes(self):
|
|
result = review_changes_prompt()
|
|
assert "detect_changes" in _text(result[0])
|
|
|
|
def test_mentions_affected_flows(self):
|
|
result = review_changes_prompt()
|
|
assert "affected_flows" in _text(result[0])
|
|
|
|
def test_mentions_test_gaps(self):
|
|
result = review_changes_prompt()
|
|
assert "test" in _text(result[0]).lower()
|
|
|
|
|
|
class TestArchitectureMapPrompt:
|
|
def test_returns_list_with_messages(self):
|
|
result = architecture_map_prompt()
|
|
assert isinstance(result, list)
|
|
assert len(result) >= 1
|
|
|
|
def test_message_has_role_and_content(self):
|
|
result = architecture_map_prompt()
|
|
for msg in result:
|
|
assert isinstance(msg, Message)
|
|
assert msg.role == "user"
|
|
assert _text(msg)
|
|
|
|
def test_mentions_communities(self):
|
|
result = architecture_map_prompt()
|
|
assert "communities" in _text(result[0]).lower()
|
|
|
|
def test_mentions_mermaid(self):
|
|
result = architecture_map_prompt()
|
|
assert "Mermaid" in _text(result[0])
|
|
|
|
|
|
class TestDebugIssuePrompt:
|
|
def test_returns_list_with_messages(self):
|
|
result = debug_issue_prompt()
|
|
assert isinstance(result, list)
|
|
assert len(result) >= 1
|
|
|
|
def test_message_has_role_and_content(self):
|
|
result = debug_issue_prompt()
|
|
for msg in result:
|
|
assert isinstance(msg, Message)
|
|
assert msg.role == "user"
|
|
assert _text(msg)
|
|
|
|
def test_includes_description(self):
|
|
result = debug_issue_prompt(description="login fails with 500 error")
|
|
assert "login fails with 500 error" in _text(result[0])
|
|
|
|
def test_empty_description(self):
|
|
result = debug_issue_prompt()
|
|
content = _text(result[0])
|
|
assert "debug" in content.lower()
|
|
|
|
def test_mentions_search(self):
|
|
result = debug_issue_prompt(description="test issue")
|
|
assert "semantic_search_nodes" in _text(result[0])
|
|
|
|
def test_mentions_get_minimal_context(self):
|
|
result = debug_issue_prompt()
|
|
assert "get_minimal_context" in _text(result[0])
|
|
|
|
|
|
class TestOnboardDeveloperPrompt:
|
|
def test_returns_list_with_messages(self):
|
|
result = onboard_developer_prompt()
|
|
assert isinstance(result, list)
|
|
assert len(result) >= 1
|
|
|
|
def test_message_has_role_and_content(self):
|
|
result = onboard_developer_prompt()
|
|
for msg in result:
|
|
assert isinstance(msg, Message)
|
|
assert msg.role == "user"
|
|
assert _text(msg)
|
|
|
|
def test_mentions_stats(self):
|
|
result = onboard_developer_prompt()
|
|
assert "list_graph_stats" in _text(result[0])
|
|
|
|
def test_mentions_architecture(self):
|
|
result = onboard_developer_prompt()
|
|
assert "architecture" in _text(result[0]).lower()
|
|
|
|
def test_mentions_critical_flows(self):
|
|
result = onboard_developer_prompt()
|
|
assert "critical" in _text(result[0]).lower()
|
|
|
|
|
|
class TestPreMergeCheckPrompt:
|
|
def test_returns_list_with_messages(self):
|
|
result = pre_merge_check_prompt()
|
|
assert isinstance(result, list)
|
|
assert len(result) >= 1
|
|
|
|
def test_message_has_role_and_content(self):
|
|
result = pre_merge_check_prompt()
|
|
for msg in result:
|
|
assert isinstance(msg, Message)
|
|
assert msg.role == "user"
|
|
assert _text(msg)
|
|
|
|
def test_default_base(self):
|
|
result = pre_merge_check_prompt()
|
|
# The pre-merge prompt is now generic (doesn't embed the base ref)
|
|
assert "pre-merge" in _text(result[0]).lower()
|
|
|
|
def test_custom_base(self):
|
|
# pre_merge_check_prompt still accepts base but the workflow
|
|
# is now generic — just verify it returns valid prompt
|
|
result = pre_merge_check_prompt(base="develop")
|
|
assert isinstance(result, list)
|
|
assert len(result) >= 1
|
|
|
|
def test_mentions_risk_scoring(self):
|
|
result = pre_merge_check_prompt()
|
|
assert "risk" in _text(result[0]).lower()
|
|
|
|
def test_mentions_test_gaps(self):
|
|
result = pre_merge_check_prompt()
|
|
assert "tests_for" in _text(result[0])
|
|
|
|
def test_mentions_dead_code(self):
|
|
result = pre_merge_check_prompt()
|
|
assert "dead_code" in _text(result[0])
|
|
|
|
|
|
class TestUnifiedReviewPrompt:
|
|
def test_returns_list_with_messages(self):
|
|
result = unified_review_prompt()
|
|
assert isinstance(result, list)
|
|
assert len(result) >= 1
|
|
|
|
def test_message_has_role_and_content(self):
|
|
result = unified_review_prompt()
|
|
for msg in result:
|
|
assert isinstance(msg, Message)
|
|
assert msg.role == "user"
|
|
assert _text(msg)
|
|
|
|
def test_default_base_and_tier(self):
|
|
result = unified_review_prompt()
|
|
text = _text(result[0])
|
|
assert "HEAD~1" in text
|
|
assert "standard tier" in text
|
|
|
|
def test_custom_base_and_tier(self):
|
|
result = unified_review_prompt(base="develop", tier="strict")
|
|
text = _text(result[0])
|
|
assert "base=develop" in text
|
|
assert "strict tier" in text
|
|
|
|
def test_mentions_score_review(self):
|
|
result = unified_review_prompt()
|
|
assert "score_review" in _text(result[0])
|
|
|
|
def test_mentions_dedupe_findings(self):
|
|
result = unified_review_prompt()
|
|
assert "dedupe_findings" in _text(result[0])
|
|
|
|
def test_mentions_generate_report(self):
|
|
result = unified_review_prompt()
|
|
assert "generate_report" in _text(result[0])
|
|
|
|
def test_mentions_read_only(self):
|
|
result = unified_review_prompt()
|
|
assert "READ-ONLY" in _text(result[0])
|
|
|
|
def test_mentions_blocker_rule(self):
|
|
result = unified_review_prompt()
|
|
assert "FAIL" in _text(result[0])
|
|
|
|
|
|
class TestProjectReviewPrompt:
|
|
def test_returns_list_with_messages(self):
|
|
result = project_review_prompt()
|
|
assert isinstance(result, list)
|
|
assert len(result) >= 1
|
|
|
|
def test_message_has_role_and_content(self):
|
|
result = project_review_prompt()
|
|
for msg in result:
|
|
assert isinstance(msg, Message)
|
|
assert msg.role == "user"
|
|
assert _text(msg)
|
|
|
|
def test_default_whole_project_scope(self):
|
|
result = project_review_prompt()
|
|
text = _text(result[0])
|
|
assert "whole-project" in text
|
|
assert "all_files=True" in text
|
|
|
|
def test_feature_scope_with_target(self):
|
|
result = project_review_prompt(scope="feature", target="payment")
|
|
text = _text(result[0])
|
|
assert "scope=feature" in text
|
|
assert "target=payment" in text
|
|
|
|
def test_mentions_architecture_scan(self):
|
|
result = project_review_prompt()
|
|
text = _text(result[0])
|
|
assert "get_architecture_overview" in text
|
|
assert "list_communities" in text
|
|
|
|
def test_mentions_high_risk_scan(self):
|
|
result = project_review_prompt()
|
|
text = _text(result[0])
|
|
assert "get_knowledge_gaps" in text
|
|
assert "get_hub_nodes" in text
|
|
|
|
def test_feature_mentions_semantic_search(self):
|
|
result = project_review_prompt(scope="feature", target="auth")
|
|
text = _text(result[0])
|
|
assert "semantic_search_nodes" in text
|
|
assert "get_impact_radius" in text
|
|
|
|
def test_mentions_read_only(self):
|
|
result = project_review_prompt()
|
|
assert "READ-ONLY" in _text(result[0])
|
|
|
|
def test_mentions_generate_report(self):
|
|
result = project_review_prompt()
|
|
assert "generate_report" in _text(result[0])
|
|
|
|
|
|
class TestTokenEfficiencyPreamble:
|
|
"""All prompts should include the token efficiency preamble."""
|
|
|
|
def test_review_has_preamble(self):
|
|
result = review_changes_prompt()
|
|
assert "get_minimal_context" in _text(result[0])
|
|
assert "detail_level" in _text(result[0])
|
|
|
|
def test_architecture_has_preamble(self):
|
|
result = architecture_map_prompt()
|
|
assert "get_minimal_context" in _text(result[0])
|
|
|
|
def test_debug_has_preamble(self):
|
|
result = debug_issue_prompt()
|
|
assert "get_minimal_context" in _text(result[0])
|
|
|
|
def test_onboard_has_preamble(self):
|
|
result = onboard_developer_prompt()
|
|
assert "get_minimal_context" in _text(result[0])
|
|
|
|
def test_pre_merge_has_preamble(self):
|
|
result = pre_merge_check_prompt()
|
|
assert "get_minimal_context" in _text(result[0])
|
|
|
|
def test_unified_review_has_preamble(self):
|
|
result = unified_review_prompt()
|
|
assert "get_minimal_context" in _text(result[0])
|
|
|
|
def test_project_review_has_preamble(self):
|
|
result = project_review_prompt()
|
|
assert "get_minimal_context" in _text(result[0])
|