Skip to content

Commit 5a8886e

Browse files
authored
feat: add AI features section to PR welcome message (#1053)
## Summary - Add collapsed `<details>` section to PR welcome message showing enabled AI features - Shows AI Conventional Title (mode + provider), Cherry-Pick Conflict Resolution (provider), and Test Oracle (triggers + command hint + provider) - Section only appears when at least one AI feature is configured - Uses direct dict access for schema-required keys (anti-defensive per CLAUDE.md) Closes #1052 ## Test plan - [x] `test_prepare_ai_features_section_no_features` - empty when no AI features - [x] `test_prepare_ai_features_section_with_conventional_title` - shows mode and provider - [x] `test_prepare_ai_features_section_with_cherry_pick_ai` - shows enabled status - [x] `test_prepare_ai_features_section_with_test_oracle` - shows triggers and `/test-oracle` hint - [x] `test_prepare_ai_features_section_all_features` - all features shown together - [x] `test_prepare_ai_features_section_disabled_features` - empty when features disabled - [x] All 132 pull request handler tests pass <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added an "AI Features" section to pull request welcome comments, displaying enabled AI capabilities such as conventional title detection, cherry-pick conflict resolution, and test oracle configuration. * **Tests** * Added comprehensive test coverage for AI Features welcome section behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent f1d6e8b commit 5a8886e

3 files changed

Lines changed: 154 additions & 10 deletions

File tree

CLAUDE.md

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -636,7 +636,7 @@ def mock_github_api():
636636
- Store tokens in environment variables or secret management systems
637637
- Use multiple tokens for rate limit distribution
638638
- Never commit tokens to repository
639-
- Mask sensitive data in logs (default: `mask-sensitive-data: true`)
639+
- Mask sensitive data in logs (see `mask-sensitive-data` in schema)
640640

641641
## Common Development Tasks
642642

@@ -657,11 +657,9 @@ def mock_github_api():
657657

658658
### PR Test Oracle Integration
659659

660-
External AI service integration for test recommendations via [pr-test-oracle](https://github.com/myk-org/pr-test-oracle). Configured via `test-oracle` in config (global or per-repo).
660+
External AI service integration for test recommendations via [pr-test-oracle](https://github.com/myk-org/pr-test-oracle).
661661

662-
**Config keys:** `server-url` (required), `ai-provider` (required: claude/gemini/cursor), `ai-model` (required), `test-patterns` (optional), `triggers` (optional, default: [approved])
663-
664-
**Trigger events:** `approved`, `pr-opened`, `pr-synchronized`
662+
**Schema:** `webhook_server/config/schema.yaml` (`test-oracle`), configurable globally or per-repo
665663

666664
**Comment command:** `/test-oracle` (always works when configured, no trigger needed)
667665

@@ -677,12 +675,9 @@ External AI service integration for test recommendations via [pr-test-oracle](ht
677675

678676
AI-powered enhancements controlled by `ai-features` config (global or per-repo).
679677

680-
**Config keys:** `ai-provider` (required: claude/gemini/cursor), `ai-model` (required), `conventional-title` (optional: "true"/"false"/"fix", default: "false")
678+
**Schema:** `webhook_server/config/schema.yaml` (`$defs.ai-features`)
681679

682-
**Conventional title modes:**
683-
- `"true"`: Show AI-suggested title in check run output when validation fails
684-
- `"false"`: Disabled (default)
685-
- `"fix"`: Auto-update PR title with AI suggestion when validation fails (suggestion is validated before applying)
680+
**Sub-features:** `conventional-title`, `resolve-cherry-pick-conflicts-with-ai`
686681

687682
**On AI CLI failure:** Error is logged, flow continues without suggestion
688683

webhook_server/libs/handlers/pull_request_handler.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ def _prepare_welcome_comment(self) -> str:
332332
333333
{self._prepare_available_labels_section}
334334
</details>
335+
{self._prepare_ai_features_welcome_section}\
335336
336337
### 💡 Tips
337338
@@ -493,6 +494,64 @@ def _prepare_tips_section(self) -> str:
493494

494495
return "\n".join(tips)
495496

497+
@property
498+
def _prepare_ai_features_welcome_section(self) -> str:
499+
"""Prepare the AI Features section for the welcome comment.
500+
501+
Only shown if at least one AI feature is configured.
502+
"""
503+
try:
504+
return self._build_ai_features_section()
505+
except Exception:
506+
self.logger.exception(f"{self.log_prefix} Failed to build AI features welcome section")
507+
return ""
508+
509+
def _build_ai_features_section(self) -> str:
510+
features: list[str] = []
511+
512+
# Check ai-features config
513+
ai_features = self.github_webhook.ai_features
514+
if ai_features:
515+
ai_provider = ai_features["ai-provider"]
516+
ai_model = ai_features["ai-model"]
517+
provider_info = f" ({ai_provider}/{ai_model})"
518+
519+
# Conventional title
520+
conv_title = ai_features.get("conventional-title")
521+
if conv_title and conv_title["enabled"]:
522+
mode = conv_title.get("mode", "suggest")
523+
features.append(f"* **Conventional Title**: Mode: `{mode}`{provider_info}")
524+
525+
# Cherry-pick conflict resolution
526+
cherry_pick_ai = ai_features.get("resolve-cherry-pick-conflicts-with-ai")
527+
if cherry_pick_ai and cherry_pick_ai["enabled"]:
528+
features.append(f"* **Cherry-Pick Conflict Resolution**: Enabled{provider_info}")
529+
530+
# Check test-oracle config (separate from ai-features)
531+
test_oracle_config = self.github_webhook.config.get_value("test-oracle")
532+
if test_oracle_config:
533+
oracle_provider = test_oracle_config["ai-provider"]
534+
oracle_model = test_oracle_config["ai-model"]
535+
oracle_info = f" ({oracle_provider}/{oracle_model})"
536+
triggers = test_oracle_config.get("triggers", ["approved"])
537+
triggers_str = ", ".join(f"`{t}`" for t in triggers)
538+
test_oracle_line = f"* **Test Oracle**: Triggers: {triggers_str}{oracle_info}"
539+
test_oracle_line += "; `/test-oracle` can be used anytime"
540+
features.append(test_oracle_line)
541+
542+
if not features:
543+
return ""
544+
545+
features_list = "\n".join(features)
546+
return f"""
547+
<details>
548+
<summary><strong>AI Features</strong></summary>
549+
550+
{features_list}
551+
552+
</details>
553+
"""
554+
496555
@property
497556
def _prepare_merge_requirements(self) -> str:
498557
"""Prepare the merge requirements section for the welcome comment.

webhook_server/tests/test_pull_request_handler.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def mock_github_webhook(self) -> Mock:
9292
mock_webhook.ctx = None
9393
mock_webhook.enabled_labels = None # Default: all labels enabled
9494
mock_webhook.custom_check_runs = []
95+
mock_webhook.ai_features = None
9596
mock_webhook.required_conversation_resolution = False
9697
mock_webhook.config = Mock()
9798
mock_webhook.config.get_value = Mock(return_value=None)
@@ -467,6 +468,95 @@ def test_prepare_welcome_comment_issue_creation_disabled(self, pull_request_hand
467468
result = pull_request_handler._prepare_welcome_comment()
468469
assert "Disabled for this repository" in result
469470

471+
def test_prepare_ai_features_section_no_features(self, pull_request_handler: PullRequestHandler) -> None:
472+
"""Test AI features section is empty when no features are configured."""
473+
pull_request_handler.github_webhook.ai_features = None
474+
result = pull_request_handler._prepare_ai_features_welcome_section
475+
assert result == ""
476+
477+
def test_prepare_ai_features_section_with_conventional_title(
478+
self, pull_request_handler: PullRequestHandler
479+
) -> None:
480+
"""Test AI features section shows conventional title when enabled."""
481+
pull_request_handler.github_webhook.ai_features = {
482+
"ai-provider": "claude",
483+
"ai-model": "claude-opus-4-6",
484+
"conventional-title": {"enabled": True, "mode": "fix"},
485+
}
486+
result = pull_request_handler._prepare_ai_features_welcome_section
487+
assert "AI Features" in result
488+
assert "Conventional Title" in result
489+
assert "Mode: `fix`" in result
490+
assert "(claude/claude-opus-4-6)" in result
491+
492+
def test_prepare_ai_features_section_with_cherry_pick_ai(self, pull_request_handler: PullRequestHandler) -> None:
493+
"""Test AI features section shows cherry-pick conflict resolution when enabled."""
494+
pull_request_handler.github_webhook.ai_features = {
495+
"ai-provider": "gemini",
496+
"ai-model": "gemini-2.5-pro",
497+
"resolve-cherry-pick-conflicts-with-ai": {"enabled": True},
498+
}
499+
result = pull_request_handler._prepare_ai_features_welcome_section
500+
assert "Cherry-Pick Conflict Resolution" in result
501+
assert "Enabled" in result
502+
assert "(gemini/gemini-2.5-pro)" in result
503+
504+
def test_prepare_ai_features_section_with_test_oracle(self, pull_request_handler: PullRequestHandler) -> None:
505+
"""Test AI features section shows test oracle when configured."""
506+
507+
def _get_value_side_effect(key: str, **_kwargs: object) -> dict[str, Any] | None:
508+
if key == "test-oracle":
509+
return {
510+
"ai-provider": "claude",
511+
"ai-model": "sonnet",
512+
"triggers": ["approved", "pr-opened"],
513+
}
514+
return None
515+
516+
pull_request_handler.github_webhook.config.get_value = Mock(side_effect=_get_value_side_effect)
517+
result = pull_request_handler._prepare_ai_features_welcome_section
518+
assert "Test Oracle" in result
519+
assert "`approved`" in result
520+
assert "`pr-opened`" in result
521+
assert "(claude/sonnet)" in result
522+
assert "/test-oracle" in result
523+
524+
def test_prepare_ai_features_section_all_features(self, pull_request_handler: PullRequestHandler) -> None:
525+
"""Test AI features section shows all features when all are configured."""
526+
pull_request_handler.github_webhook.ai_features = {
527+
"ai-provider": "claude",
528+
"ai-model": "claude-opus-4-6",
529+
"conventional-title": {"enabled": True, "mode": "suggest"},
530+
"resolve-cherry-pick-conflicts-with-ai": {"enabled": True},
531+
}
532+
533+
def _get_value_side_effect(key: str, **_kwargs: object) -> dict[str, Any] | None:
534+
if key == "test-oracle":
535+
return {
536+
"ai-provider": "gemini",
537+
"ai-model": "gemini-2.5-pro",
538+
"triggers": ["approved"],
539+
}
540+
return None
541+
542+
pull_request_handler.github_webhook.config.get_value = Mock(side_effect=_get_value_side_effect)
543+
result = pull_request_handler._prepare_ai_features_welcome_section
544+
assert "Conventional Title" in result
545+
assert "Cherry-Pick Conflict Resolution" in result
546+
assert "Test Oracle" in result
547+
assert "/test-oracle" in result
548+
549+
def test_prepare_ai_features_section_disabled_features(self, pull_request_handler: PullRequestHandler) -> None:
550+
"""Test AI features section is empty when features exist but are disabled."""
551+
pull_request_handler.github_webhook.ai_features = {
552+
"ai-provider": "claude",
553+
"ai-model": "claude-opus-4-6",
554+
"conventional-title": {"enabled": False},
555+
"resolve-cherry-pick-conflicts-with-ai": {"enabled": False},
556+
}
557+
result = pull_request_handler._prepare_ai_features_welcome_section
558+
assert result == ""
559+
470560
def test_prepare_owners_welcome_comment(self, pull_request_handler: PullRequestHandler) -> None:
471561
"""Test preparing owners welcome comment."""
472562
result = pull_request_handler._prepare_owners_welcome_comment()

0 commit comments

Comments
 (0)