Files
awoooi/apps/api/tests/test_playbook_generator.py
Your Name 6e04fe9c8a
Some checks failed
CD Pipeline / tests (push) Successful in 1m28s
Code Review / ai-code-review (push) Successful in 29s
Type Sync Check / check-type-sync (push) Failing after 2m41s
CD Pipeline / build-and-deploy (push) Successful in 8m40s
CD Pipeline / post-deploy-checks (push) Successful in 3m10s
feat(playbook): generate drafts with local llm
2026-04-30 23:04:58 +08:00

156 lines
5.5 KiB
Python

from src.jobs.playbook_generation_governance_job import run_playbook_generation_governance_once
from src.models.incident import Incident, IncidentOutcome, IncidentStatus, Severity, Signal
from src.models.playbook import ActionType, PlaybookStatus, RepairStep, RiskLevel
from src.services.playbook_generator import LLMPlaybookGenerator
from src.utils.timezone import now_taipei
class InMemoryPlaybookService:
def __init__(self):
self.items = {}
async def create(self, playbook):
self.items[playbook.playbook_id] = playbook
return playbook
async def list_playbooks(self, status=None, tags=None, limit=20, offset=0):
values = list(self.items.values())
if status is not None:
values = [pb for pb in values if pb.status == status]
return values[offset : offset + limit], len(values)
async def update_with_validation(self, playbook_id, update_data):
playbook = self.items[playbook_id]
for key, value in update_data.items():
if key == "status" and isinstance(value, str):
value = PlaybookStatus(value)
setattr(playbook, key, value)
self.items[playbook_id] = playbook
return playbook
def make_resolved_incident(action: str = "kubectl rollout restart deployment/awoooi-api -n awoooi-prod") -> Incident:
return Incident(
incident_id="INC-20260430-PLAYBK",
status=IncidentStatus.RESOLVED,
severity=Severity.P2,
signals=[
Signal(
alert_name="ApiErrorRateHigh",
severity=Severity.P2,
source="alertmanager",
fired_at=now_taipei(),
labels={"namespace": "awoooi-prod", "deployment": "awoooi-api"},
annotations={"summary": "API error rate high"},
)
],
affected_services=["awoooi-api"],
outcome=IncidentOutcome(
proposal_executed=True,
execution_success=True,
effectiveness_score=5,
learning_notes=action,
),
)
async def local_llm_ok(_prompt, _context):
return (
"""
{
"name": "API error rate recovery",
"description": "Restart the affected API deployment after error-rate alert confirmation.",
"alert_names": ["ApiErrorRateHigh"],
"affected_services": ["awoooi-api"],
"severity_range": ["P2"],
"keywords": ["error rate", "api"],
"repair_steps": [
{
"action_type": "kubectl",
"command": "kubectl rollout restart deployment/awoooi-api -n awoooi-prod",
"expected_result": "new pods become ready",
"risk_level": "MEDIUM"
}
],
"estimated_duration_minutes": 5,
"confidence": 0.86,
"tags": ["api", "rollout"]
}
""",
"ollama",
True,
)
async def local_llm_unsafe(_prompt, _context):
return (
"""
{
"name": "Unsafe namespace cleanup",
"description": "Bad suggestion should be gated.",
"alert_names": ["ApiErrorRateHigh"],
"affected_services": ["awoooi-api"],
"repair_steps": [
{
"action_type": "kubectl",
"command": "kubectl delete namespace awoooi-prod",
"risk_level": "CRITICAL"
}
],
"confidence": 0.95,
"tags": ["unsafe"]
}
""",
"ollama",
True,
)
async def test_llm_playbook_generator_creates_review_playbook():
service = InMemoryPlaybookService()
generator = LLMPlaybookGenerator(playbook_service=service, llm_callable=local_llm_ok)
result = await generator.generate_from_incident(make_resolved_incident())
assert result.outcome == "success"
assert result.playbook is not None
assert result.playbook.status == PlaybookStatus.REVIEW
assert result.playbook.source.value == "llm_generated"
assert result.playbook.repair_steps[0].action_type == ActionType.KUBECTL
assert result.playbook.repair_steps[0].command == "kubectl rollout restart deployment/awoooi-api -n awoooi-prod"
async def test_llm_playbook_generator_downgrades_unsafe_kubectl_to_manual():
service = InMemoryPlaybookService()
generator = LLMPlaybookGenerator(playbook_service=service, llm_callable=local_llm_unsafe)
result = await generator.generate_from_incident(make_resolved_incident())
assert result.playbook is not None
step = result.playbook.repair_steps[0]
assert step.action_type == ActionType.MANUAL
assert step.requires_approval is True
assert step.risk_level == RiskLevel.HIGH
assert "namespace" in step.command
async def test_playbook_generation_governance_promotes_review_to_approved(monkeypatch):
service = InMemoryPlaybookService()
generator = LLMPlaybookGenerator(playbook_service=service, llm_callable=local_llm_ok)
result = await generator.generate_from_incident(make_resolved_incident())
assert result.playbook is not None
result.playbook.ai_confidence = 0.93
class FakeSettings:
ENABLE_PLAYBOOK_DRAFT_GOVERNANCE_JOB = True
import src.jobs.playbook_generation_governance_job as job
monkeypatch.setattr(job, "settings", FakeSettings())
monkeypatch.setattr("src.services.playbook_service.get_playbook_service", lambda: service)
report = await run_playbook_generation_governance_once()
assert report.approved_count == 1
assert service.items[result.playbook.playbook_id].status == PlaybookStatus.APPROVED