feat: Add automatic PR summary generator
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:
2025-12-29 10:15:08 +00:00
parent 1ca6ac7913
commit e21ec5f57a
6 changed files with 643 additions and 7 deletions

View File

@@ -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"])