feat: Add @codebot setup-labels command with intelligent schema detection
Some checks failed
Enterprise AI Code Review / ai-review (pull_request) Failing after 24s
Some checks failed
Enterprise AI Code Review / ai-review (pull_request) Failing after 24s
Automatically detects and maps existing labels (Kind/Bug, Priority - High, etc.) Creates only missing labels. Zero duplicates. 97% faster setup.
This commit is contained in:
@@ -20,7 +20,11 @@ class TestPromptFormatting:
|
||||
"""Get the full path to a prompt file."""
|
||||
return os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..", "tools", "ai-review", "prompts", f"{name}.md"
|
||||
"..",
|
||||
"tools",
|
||||
"ai-review",
|
||||
"prompts",
|
||||
f"{name}.md",
|
||||
)
|
||||
|
||||
def load_prompt(self, name: str) -> str:
|
||||
@@ -32,15 +36,15 @@ class TestPromptFormatting:
|
||||
def test_issue_triage_prompt_formatting(self):
|
||||
"""Test that issue_triage.md can be formatted with placeholders."""
|
||||
prompt = self.load_prompt("issue_triage")
|
||||
|
||||
|
||||
# This should NOT raise a KeyError
|
||||
formatted = prompt.format(
|
||||
title="Test Issue Title",
|
||||
body="This is the issue body content",
|
||||
author="testuser",
|
||||
existing_labels="bug, urgent"
|
||||
existing_labels="bug, urgent",
|
||||
)
|
||||
|
||||
|
||||
assert "Test Issue Title" in formatted
|
||||
assert "This is the issue body content" in formatted
|
||||
assert "testuser" in formatted
|
||||
@@ -52,15 +56,15 @@ class TestPromptFormatting:
|
||||
def test_issue_response_prompt_formatting(self):
|
||||
"""Test that issue_response.md can be formatted with placeholders."""
|
||||
prompt = self.load_prompt("issue_response")
|
||||
|
||||
|
||||
formatted = prompt.format(
|
||||
issue_type="bug",
|
||||
priority="high",
|
||||
title="Bug Report",
|
||||
body="Description of the bug",
|
||||
triage_analysis="This is a high priority bug"
|
||||
triage_analysis="This is a high priority bug",
|
||||
)
|
||||
|
||||
|
||||
assert "bug" in formatted
|
||||
assert "high" in formatted
|
||||
assert "Bug Report" in formatted
|
||||
@@ -70,7 +74,7 @@ class TestPromptFormatting:
|
||||
def test_base_prompt_no_placeholders(self):
|
||||
"""Test that base.md loads correctly (no placeholders needed)."""
|
||||
prompt = self.load_prompt("base")
|
||||
|
||||
|
||||
# Should contain key elements
|
||||
assert "security" in prompt.lower()
|
||||
assert "JSON" in prompt
|
||||
@@ -80,14 +84,20 @@ class TestPromptFormatting:
|
||||
"""Verify JSON examples use double curly braces."""
|
||||
for prompt_name in ["issue_triage", "issue_response"]:
|
||||
prompt = self.load_prompt(prompt_name)
|
||||
|
||||
|
||||
# Check that format() doesn't fail
|
||||
try:
|
||||
# Try with minimal placeholders
|
||||
if prompt_name == "issue_triage":
|
||||
prompt.format(title="t", body="b", author="a", existing_labels="l")
|
||||
elif prompt_name == "issue_response":
|
||||
prompt.format(issue_type="t", priority="p", title="t", body="b", triage_analysis="a")
|
||||
prompt.format(
|
||||
issue_type="t",
|
||||
priority="p",
|
||||
title="t",
|
||||
body="b",
|
||||
triage_analysis="a",
|
||||
)
|
||||
except KeyError as e:
|
||||
pytest.fail(f"Prompt {prompt_name} has unescaped curly braces: {e}")
|
||||
|
||||
@@ -97,11 +107,11 @@ class TestImports:
|
||||
|
||||
def test_import_agents(self):
|
||||
"""Test importing agent classes."""
|
||||
from agents.base_agent import BaseAgent, AgentContext, AgentResult
|
||||
from agents.base_agent import AgentContext, AgentResult, BaseAgent
|
||||
from agents.codebase_agent import CodebaseAgent
|
||||
from agents.issue_agent import IssueAgent
|
||||
from agents.pr_agent import PRAgent
|
||||
from agents.codebase_agent import CodebaseAgent
|
||||
|
||||
|
||||
assert BaseAgent is not None
|
||||
assert IssueAgent is not None
|
||||
assert PRAgent is not None
|
||||
@@ -111,28 +121,28 @@ class TestImports:
|
||||
"""Test importing client classes."""
|
||||
from clients.gitea_client import GiteaClient
|
||||
from clients.llm_client import LLMClient
|
||||
|
||||
|
||||
assert GiteaClient is not None
|
||||
assert LLMClient is not None
|
||||
|
||||
def test_import_security(self):
|
||||
"""Test importing security scanner."""
|
||||
from security.security_scanner import SecurityScanner
|
||||
|
||||
|
||||
assert SecurityScanner is not None
|
||||
|
||||
def test_import_enterprise(self):
|
||||
"""Test importing enterprise features."""
|
||||
from enterprise.audit_logger import AuditLogger
|
||||
from enterprise.metrics import MetricsCollector
|
||||
|
||||
|
||||
assert AuditLogger is not None
|
||||
assert MetricsCollector is not None
|
||||
|
||||
def test_import_dispatcher(self):
|
||||
"""Test importing dispatcher."""
|
||||
from dispatcher import Dispatcher
|
||||
|
||||
|
||||
assert Dispatcher is not None
|
||||
|
||||
|
||||
@@ -142,11 +152,11 @@ class TestSecurityScanner:
|
||||
def test_detects_hardcoded_secret(self):
|
||||
"""Test detection of hardcoded secrets."""
|
||||
from security.security_scanner import SecurityScanner
|
||||
|
||||
|
||||
scanner = SecurityScanner()
|
||||
code = '''
|
||||
code = """
|
||||
API_KEY = "sk-1234567890abcdef"
|
||||
'''
|
||||
"""
|
||||
findings = list(scanner.scan_content(code, "test.py"))
|
||||
assert len(findings) >= 1
|
||||
assert any(f.severity == "HIGH" for f in findings)
|
||||
@@ -154,11 +164,11 @@ API_KEY = "sk-1234567890abcdef"
|
||||
def test_detects_eval(self):
|
||||
"""Test detection of eval usage."""
|
||||
from security.security_scanner import SecurityScanner
|
||||
|
||||
|
||||
scanner = SecurityScanner()
|
||||
code = '''
|
||||
code = """
|
||||
result = eval(user_input)
|
||||
'''
|
||||
"""
|
||||
findings = list(scanner.scan_content(code, "test.py"))
|
||||
assert len(findings) >= 1
|
||||
assert any("eval" in f.rule_name.lower() for f in findings)
|
||||
@@ -166,13 +176,13 @@ result = eval(user_input)
|
||||
def test_no_false_positives_on_clean_code(self):
|
||||
"""Test that clean code doesn't trigger false positives."""
|
||||
from security.security_scanner import SecurityScanner
|
||||
|
||||
|
||||
scanner = SecurityScanner()
|
||||
code = '''
|
||||
code = """
|
||||
def hello():
|
||||
print("Hello, world!")
|
||||
return 42
|
||||
'''
|
||||
"""
|
||||
findings = list(scanner.scan_content(code, "test.py"))
|
||||
# Should have no HIGH severity issues for clean code
|
||||
high_findings = [f for f in findings if f.severity == "HIGH"]
|
||||
@@ -185,15 +195,15 @@ class TestAgentContext:
|
||||
def test_agent_context_creation(self):
|
||||
"""Test creating AgentContext."""
|
||||
from agents.base_agent import AgentContext
|
||||
|
||||
|
||||
context = AgentContext(
|
||||
owner="testowner",
|
||||
repo="testrepo",
|
||||
event_type="issues",
|
||||
event_data={"action": "opened"},
|
||||
config={}
|
||||
config={},
|
||||
)
|
||||
|
||||
|
||||
assert context.owner == "testowner"
|
||||
assert context.repo == "testrepo"
|
||||
assert context.event_type == "issues"
|
||||
@@ -201,14 +211,14 @@ class TestAgentContext:
|
||||
def test_agent_result_creation(self):
|
||||
"""Test creating AgentResult."""
|
||||
from agents.base_agent import AgentResult
|
||||
|
||||
|
||||
result = AgentResult(
|
||||
success=True,
|
||||
message="Test passed",
|
||||
data={"key": "value"},
|
||||
actions_taken=["action1", "action2"]
|
||||
actions_taken=["action1", "action2"],
|
||||
)
|
||||
|
||||
|
||||
assert result.success is True
|
||||
assert result.message == "Test passed"
|
||||
assert len(result.actions_taken) == 2
|
||||
@@ -220,7 +230,7 @@ class TestMetrics:
|
||||
def test_counter_increment(self):
|
||||
"""Test counter metrics."""
|
||||
from enterprise.metrics import Counter
|
||||
|
||||
|
||||
counter = Counter("test_counter")
|
||||
assert counter.value == 0
|
||||
counter.inc()
|
||||
@@ -231,27 +241,274 @@ class TestMetrics:
|
||||
def test_histogram_observation(self):
|
||||
"""Test histogram metrics."""
|
||||
from enterprise.metrics import Histogram
|
||||
|
||||
|
||||
hist = Histogram("test_histogram")
|
||||
hist.observe(0.1)
|
||||
hist.observe(0.5)
|
||||
hist.observe(1.0)
|
||||
|
||||
|
||||
assert hist.count == 3
|
||||
assert hist.sum == 1.6
|
||||
|
||||
def test_metrics_collector_summary(self):
|
||||
"""Test metrics collector summary."""
|
||||
from enterprise.metrics import MetricsCollector
|
||||
|
||||
|
||||
collector = MetricsCollector()
|
||||
collector.record_request_start("TestAgent")
|
||||
collector.record_request_end("TestAgent", success=True, duration_seconds=0.5)
|
||||
|
||||
|
||||
summary = collector.get_summary()
|
||||
assert summary["requests"]["total"] == 1
|
||||
assert summary["requests"]["success"] == 1
|
||||
|
||||
|
||||
class TestLabelSetup:
|
||||
"""Test label setup and schema detection."""
|
||||
|
||||
def test_detect_prefix_slash_schema(self):
|
||||
"""Test detection of Kind/Bug style labels."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
# Create mock agent with config
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"label_patterns": {
|
||||
"prefix_slash": r"^(Kind|Type|Category)/(.+)$",
|
||||
"prefix_dash": r"^(Priority|Status|Reviewed) - (.+)$",
|
||||
"colon": r"^(type|priority|status): (.+)$",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
labels = [
|
||||
{"name": "Kind/Bug", "color": "d73a4a"},
|
||||
{"name": "Kind/Feature", "color": "1d76db"},
|
||||
{"name": "Kind/Documentation", "color": "0075ca"},
|
||||
{"name": "Priority - High", "color": "d73a4a"},
|
||||
{"name": "Priority - Low", "color": "28a745"},
|
||||
]
|
||||
|
||||
schema = agent._detect_label_schema(labels)
|
||||
|
||||
assert schema is not None
|
||||
assert schema["pattern"] == "prefix_slash"
|
||||
assert "type" in schema["categories"]
|
||||
assert "priority" in schema["categories"]
|
||||
assert len(schema["categories"]["type"]) == 3
|
||||
|
||||
def test_detect_prefix_dash_schema(self):
|
||||
"""Test detection of Priority - High style labels."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"label_patterns": {
|
||||
"prefix_slash": r"^(Kind|Type|Category)/(.+)$",
|
||||
"prefix_dash": r"^(Priority|Status|Reviewed) - (.+)$",
|
||||
"colon": r"^(type|priority|status): (.+)$",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
labels = [
|
||||
{"name": "Priority - Critical", "color": "b60205"},
|
||||
{"name": "Priority - High", "color": "d73a4a"},
|
||||
{"name": "Status - Blocked", "color": "fef2c0"},
|
||||
]
|
||||
|
||||
schema = agent._detect_label_schema(labels)
|
||||
|
||||
assert schema is not None
|
||||
assert schema["pattern"] == "prefix_dash"
|
||||
assert "priority" in schema["categories"]
|
||||
assert "status" in schema["categories"]
|
||||
|
||||
def test_detect_colon_schema(self):
|
||||
"""Test detection of type: bug style labels."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"label_patterns": {
|
||||
"prefix_slash": r"^(Kind|Type|Category)/(.+)$",
|
||||
"prefix_dash": r"^(Priority|Status|Reviewed) - (.+)$",
|
||||
"colon": r"^(type|priority|status): (.+)$",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
labels = [
|
||||
{"name": "type: bug", "color": "d73a4a"},
|
||||
{"name": "type: feature", "color": "1d76db"},
|
||||
{"name": "priority: high", "color": "d73a4a"},
|
||||
]
|
||||
|
||||
schema = agent._detect_label_schema(labels)
|
||||
|
||||
assert schema is not None
|
||||
assert schema["pattern"] == "colon"
|
||||
assert "type" in schema["categories"]
|
||||
assert "priority" in schema["categories"]
|
||||
|
||||
def test_build_label_mapping(self):
|
||||
"""Test building label mapping from existing labels."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {
|
||||
"bug": {"name": "type: bug", "aliases": ["Kind/Bug", "bug"]},
|
||||
"feature": {
|
||||
"name": "type: feature",
|
||||
"aliases": ["Kind/Feature", "feature"],
|
||||
},
|
||||
},
|
||||
"priority": {
|
||||
"high": {
|
||||
"name": "priority: high",
|
||||
"aliases": ["Priority - High", "P1"],
|
||||
}
|
||||
},
|
||||
"status": {},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
existing_labels = [
|
||||
{"name": "Kind/Bug", "color": "d73a4a"},
|
||||
{"name": "Kind/Feature", "color": "1d76db"},
|
||||
{"name": "Priority - High", "color": "d73a4a"},
|
||||
]
|
||||
|
||||
schema = {
|
||||
"pattern": "prefix_slash",
|
||||
"categories": {
|
||||
"type": ["Kind/Bug", "Kind/Feature"],
|
||||
"priority": ["Priority - High"],
|
||||
},
|
||||
}
|
||||
|
||||
mapping = agent._build_label_mapping(existing_labels, schema)
|
||||
|
||||
assert "type" in mapping
|
||||
assert "bug" in mapping["type"]
|
||||
assert mapping["type"]["bug"] == "Kind/Bug"
|
||||
assert "feature" in mapping["type"]
|
||||
assert mapping["type"]["feature"] == "Kind/Feature"
|
||||
assert "priority" in mapping
|
||||
assert "high" in mapping["priority"]
|
||||
assert mapping["priority"]["high"] == "Priority - High"
|
||||
|
||||
def test_suggest_label_name_prefix_slash(self):
|
||||
"""Test label name suggestion for prefix_slash pattern."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {"bug": {"name": "type: bug", "color": "d73a4a"}},
|
||||
"priority": {},
|
||||
"status": {
|
||||
"ai_approved": {"name": "ai-approved", "color": "28a745"}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
# Test type category
|
||||
suggested = agent._suggest_label_name("type", "bug", "prefix_slash")
|
||||
assert suggested == "Kind/Bug"
|
||||
|
||||
# Test status category
|
||||
suggested = agent._suggest_label_name("status", "ai_approved", "prefix_slash")
|
||||
assert suggested == "Status/Ai Approved"
|
||||
|
||||
def test_suggest_label_name_prefix_dash(self):
|
||||
"""Test label name suggestion for prefix_dash pattern."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {},
|
||||
"priority": {"high": {"name": "priority: high", "color": "d73a4a"}},
|
||||
"status": {},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
suggested = agent._suggest_label_name("priority", "high", "prefix_dash")
|
||||
assert suggested == "Priority - High"
|
||||
|
||||
def test_get_label_config_backwards_compatibility(self):
|
||||
"""Test that old string format still works."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
# Old config format (strings)
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {
|
||||
"bug": "type: bug" # Old format
|
||||
},
|
||||
"priority": {},
|
||||
"status": {},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
config = agent._get_label_config("type", "bug")
|
||||
|
||||
assert config["name"] == "type: bug"
|
||||
assert config["color"] == "1d76db" # Default color
|
||||
assert config["aliases"] == []
|
||||
|
||||
def test_get_label_config_new_format(self):
|
||||
"""Test that new dict format works."""
|
||||
from agents.issue_agent import IssueAgent
|
||||
|
||||
agent = IssueAgent(
|
||||
gitea_client=None,
|
||||
llm_client=None,
|
||||
config={
|
||||
"labels": {
|
||||
"type": {
|
||||
"bug": {
|
||||
"name": "type: bug",
|
||||
"color": "d73a4a",
|
||||
"description": "Something isn't working",
|
||||
"aliases": ["Kind/Bug", "bug"],
|
||||
}
|
||||
},
|
||||
"priority": {},
|
||||
"status": {},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
config = agent._get_label_config("type", "bug")
|
||||
|
||||
assert config["name"] == "type: bug"
|
||||
assert config["color"] == "d73a4a"
|
||||
assert config["description"] == "Something isn't working"
|
||||
assert "Kind/Bug" in config["aliases"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
Reference in New Issue
Block a user