feat: Add automatic PR summary generator
All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 7s
All checks were successful
Enterprise AI Code Review / ai-review (pull_request) Successful in 7s
Implements automatic PR summary generation feature that analyzes pull request diffs and generates comprehensive summaries. Features: - Auto-generates summaries for PRs with empty descriptions - Manual trigger via @codebot summarize command in PR comments - Structured output with change type, files affected, and impact assessment - Configurable (enable/disable, comment vs description update) Implementation: - Added pr_summary.md prompt template for LLM - Extended PRAgent with summary generation methods - Added auto_summary configuration in config.yml - Comprehensive test suite with 10 new tests - Updated documentation in README.md and CLAUDE.md Usage: - Automatic: Opens PR with no description → auto-generates summary - Manual: Comment '@codebot summarize' on any PR Related: Issue #2 - Milestone 2 feature delivery
This commit is contained in:
@@ -588,5 +588,242 @@ class TestLabelSetup:
|
||||
assert "Kind/Bug" in config["aliases"]
|
||||
|
||||
|
||||
class TestPRSummaryGeneration:
|
||||
"""Test PR summary generation functionality."""
|
||||
|
||||
def test_pr_summary_prompt_exists(self):
|
||||
"""Verify pr_summary.md prompt file exists."""
|
||||
prompt_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..",
|
||||
"tools",
|
||||
"ai-review",
|
||||
"prompts",
|
||||
"pr_summary.md",
|
||||
)
|
||||
assert os.path.exists(prompt_path), "pr_summary.md prompt file not found"
|
||||
|
||||
def test_pr_summary_prompt_formatting(self):
|
||||
"""Test that pr_summary.md can be loaded without errors."""
|
||||
prompt_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..",
|
||||
"tools",
|
||||
"ai-review",
|
||||
"prompts",
|
||||
"pr_summary.md",
|
||||
)
|
||||
with open(prompt_path) as f:
|
||||
prompt = f.read()
|
||||
|
||||
# Check for key elements
|
||||
assert "summary" in prompt.lower()
|
||||
assert "change_type" in prompt.lower()
|
||||
assert "files_affected" in prompt.lower()
|
||||
assert "impact" in prompt.lower()
|
||||
assert "JSON" in prompt
|
||||
|
||||
# Verify JSON examples use double curly braces (escaped)
|
||||
# Should not raise KeyError when formatted with empty string
|
||||
try:
|
||||
formatted = prompt.format()
|
||||
except KeyError as e:
|
||||
pytest.fail(f"Prompt has unescaped placeholders: {e}")
|
||||
|
||||
def test_pr_agent_has_summary_marker(self):
|
||||
"""Verify PRAgent has PR_SUMMARY_MARKER constant."""
|
||||
from agents.pr_agent import PRAgent
|
||||
|
||||
assert hasattr(PRAgent, "PR_SUMMARY_MARKER")
|
||||
assert PRAgent.PR_SUMMARY_MARKER == "<!-- AI_PR_SUMMARY -->"
|
||||
|
||||
def test_pr_agent_can_handle_summarize_command(self):
|
||||
"""Test that PRAgent can handle @codebot summarize in PR comments."""
|
||||
from agents.pr_agent import PRAgent
|
||||
|
||||
config = {
|
||||
"agents": {
|
||||
"pr": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
"interaction": {
|
||||
"mention_prefix": "@codebot",
|
||||
},
|
||||
}
|
||||
|
||||
agent = PRAgent(config=config)
|
||||
|
||||
# Test summarize command in PR comment
|
||||
event_data = {
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"number": 123,
|
||||
"pull_request": {}, # Indicates this is a PR
|
||||
},
|
||||
"comment": {
|
||||
"body": "@codebot summarize this PR please",
|
||||
},
|
||||
}
|
||||
|
||||
assert agent.can_handle("issue_comment", event_data) is True
|
||||
|
||||
def test_pr_agent_can_handle_summarize_case_insensitive(self):
|
||||
"""Test that summarize command is case-insensitive."""
|
||||
from agents.pr_agent import PRAgent
|
||||
|
||||
config = {
|
||||
"agents": {
|
||||
"pr": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
"interaction": {
|
||||
"mention_prefix": "@codebot",
|
||||
},
|
||||
}
|
||||
|
||||
agent = PRAgent(config=config)
|
||||
|
||||
# Test various casings
|
||||
for body in [
|
||||
"@codebot SUMMARIZE",
|
||||
"@codebot Summarize",
|
||||
"@codebot SuMmArIzE",
|
||||
]:
|
||||
event_data = {
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"number": 123,
|
||||
"pull_request": {},
|
||||
},
|
||||
"comment": {"body": body},
|
||||
}
|
||||
assert agent.can_handle("issue_comment", event_data) is True
|
||||
|
||||
def test_pr_agent_ignores_summarize_on_non_pr(self):
|
||||
"""Test that summarize command is ignored on regular issues."""
|
||||
from agents.pr_agent import PRAgent
|
||||
|
||||
config = {
|
||||
"agents": {
|
||||
"pr": {
|
||||
"enabled": True,
|
||||
}
|
||||
},
|
||||
"interaction": {
|
||||
"mention_prefix": "@codebot",
|
||||
},
|
||||
}
|
||||
|
||||
agent = PRAgent(config=config)
|
||||
|
||||
# Regular issue (no pull_request field)
|
||||
event_data = {
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"number": 123,
|
||||
# No pull_request field
|
||||
},
|
||||
"comment": {
|
||||
"body": "@codebot summarize",
|
||||
},
|
||||
}
|
||||
|
||||
assert agent.can_handle("issue_comment", event_data) is False
|
||||
|
||||
def test_format_pr_summary_structure(self):
|
||||
"""Test _format_pr_summary generates correct markdown structure."""
|
||||
from agents.pr_agent import PRAgent
|
||||
|
||||
agent = PRAgent(config={})
|
||||
|
||||
summary_data = {
|
||||
"summary": "This PR adds a new feature",
|
||||
"change_type": "Feature",
|
||||
"key_changes": {
|
||||
"added": ["New authentication module", "User login endpoint"],
|
||||
"modified": ["Updated config file"],
|
||||
"removed": ["Deprecated legacy auth"],
|
||||
},
|
||||
"files_affected": [
|
||||
{
|
||||
"path": "src/auth.py",
|
||||
"description": "New authentication module",
|
||||
"change_type": "added",
|
||||
},
|
||||
{
|
||||
"path": "config.yml",
|
||||
"description": "Added auth settings",
|
||||
"change_type": "modified",
|
||||
},
|
||||
],
|
||||
"impact": {
|
||||
"scope": "medium",
|
||||
"description": "Adds authentication without breaking existing features",
|
||||
},
|
||||
}
|
||||
|
||||
result = agent._format_pr_summary(summary_data)
|
||||
|
||||
# Verify structure
|
||||
assert "## 📋 Pull Request Summary" in result
|
||||
assert "This PR adds a new feature" in result
|
||||
assert "**Type:** ✨ Feature" in result
|
||||
assert "## Changes" in result
|
||||
assert "**✅ Added:**" in result
|
||||
assert "**📝 Modified:**" in result
|
||||
assert "**❌ Removed:**" in result
|
||||
assert "## Files Affected" in result
|
||||
assert "➕ `src/auth.py`" in result
|
||||
assert "📝 `config.yml`" in result
|
||||
assert "## Impact" in result
|
||||
assert "🟡 **Scope:** Medium" in result
|
||||
|
||||
def test_format_pr_summary_change_types(self):
|
||||
"""Test that all change types have correct emojis."""
|
||||
from agents.pr_agent import PRAgent
|
||||
|
||||
agent = PRAgent(config={})
|
||||
|
||||
change_types = {
|
||||
"Feature": "✨",
|
||||
"Bugfix": "🐛",
|
||||
"Refactor": "♻️",
|
||||
"Documentation": "📚",
|
||||
"Testing": "🧪",
|
||||
"Mixed": "🔀",
|
||||
}
|
||||
|
||||
for change_type, expected_emoji in change_types.items():
|
||||
summary_data = {
|
||||
"summary": "Test",
|
||||
"change_type": change_type,
|
||||
"key_changes": {},
|
||||
"files_affected": [],
|
||||
"impact": {},
|
||||
}
|
||||
|
||||
result = agent._format_pr_summary(summary_data)
|
||||
assert f"**Type:** {expected_emoji} {change_type}" in result
|
||||
|
||||
def test_config_has_auto_summary_settings(self):
|
||||
"""Verify config.yml has auto_summary configuration."""
|
||||
config_path = os.path.join(
|
||||
os.path.dirname(__file__), "..", "tools", "ai-review", "config.yml"
|
||||
)
|
||||
|
||||
import yaml
|
||||
|
||||
with open(config_path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
assert "agents" in config
|
||||
assert "pr" in config["agents"]
|
||||
assert "auto_summary" in config["agents"]["pr"]
|
||||
assert "enabled" in config["agents"]["pr"]["auto_summary"]
|
||||
assert "post_as_comment" in config["agents"]["pr"]["auto_summary"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
Reference in New Issue
Block a user