fix(api): add quality summary slo metric
All checks were successful
CD Pipeline / tests (push) Successful in 1m19s
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / build-and-deploy (push) Successful in 3m31s
CD Pipeline / post-deploy-checks (push) Successful in 1m29s

This commit is contained in:
Your Name
2026-06-01 17:00:50 +08:00
parent 9954e97710
commit d6c904dd0f
9 changed files with 377 additions and 27 deletions

View File

@@ -1,6 +1,7 @@
from src.services.adr100_slo_metrics_service import (
Adr100SloMetricsSnapshot,
AutomationOperationSample,
QualitySummaryObservation,
VerificationSample,
render_adr100_slo_metrics,
)
@@ -38,6 +39,17 @@ def test_render_adr100_slo_metrics_outputs_required_series() -> None:
knowledge_entries_created_24h=25,
high_confidence_total=9,
high_confidence_success_total=7,
quality_summary_observations=[
QualitySummaryObservation(
project_id="awoooi",
hours=24,
limit=8,
cache_status="miss",
success=True,
duration_seconds=1.234567,
observed_at=1_778_756_100,
),
],
emitted_at=1_778_756_000,
)
@@ -58,6 +70,18 @@ def test_render_adr100_slo_metrics_outputs_required_series() -> None:
assert "approval_records_high_confidence_total 9" in rendered
assert "approval_records_high_confidence_success_total 7" in rendered
assert "adr100_slo_emitter_last_success_timestamp 1778756000" in rendered
assert (
'awooop_truth_chain_quality_summary_last_duration_seconds{project_id="awoooi",'
'hours="24",limit="8",cache_status="miss",success="true"} 1.234567'
) in rendered
assert (
'awooop_truth_chain_quality_summary_last_success{project_id="awoooi",'
'hours="24",limit="8",cache_status="miss",success="true"} 1'
) in rendered
assert (
'awooop_truth_chain_quality_summary_observed_timestamp{project_id="awoooi",'
'hours="24",limit="8",cache_status="miss",success="true"} 1778756100'
) in rendered
def test_render_adr100_slo_metrics_emits_zero_series_when_empty() -> None:
@@ -71,6 +95,10 @@ def test_render_adr100_slo_metrics_emits_zero_series_when_empty() -> None:
assert 'post_execution_verification_created_24h{outcome="none"} 0' in rendered
assert "knowledge_entries_total 0" in rendered
assert "knowledge_entries_created_24h 0" in rendered
assert (
'awooop_truth_chain_quality_summary_last_duration_seconds{project_id="none",'
'hours="0",limit="0",cache_status="none",success="false"} 0'
) in rendered
def test_render_adr100_slo_metrics_escapes_labels() -> None:

View File

@@ -10,6 +10,12 @@ from src.services.adr100_slo_status_service import (
)
QUALITY_SUMMARY_LATENCY_QUERY = (
'max(awooop_truth_chain_quality_summary_last_duration_seconds{'
'project_id="awoooi",limit="8",success="true"})'
)
class _FakePrometheusResponse:
def __init__(self, payload: dict[str, Any]) -> None:
self._payload = payload
@@ -89,6 +95,7 @@ async def test_fetch_report_marks_ratio_slos_low_volume(monkeypatch):
'sum(rate(automation_operation_log_total{outcome="auto_executed"}[5m]))': "0",
"sum(rate(approval_records_high_confidence_total[1h]))": "0",
"max(knowledge_entries_created_24h) or max(sli:km_growth_rate:24h)": "24",
QUALITY_SUMMARY_LATENCY_QUERY: "1.2",
}
monkeypatch.setattr(
@@ -109,6 +116,8 @@ async def test_fetch_report_marks_ratio_slos_low_volume(monkeypatch):
assert by_name["confidence_calibration"]["status"] == "skipped_low_volume"
assert by_name["km_growth_rate"]["status"] == "ok"
assert by_name["km_growth_rate"]["value"] == 24
assert by_name["truth_chain_quality_summary_latency"]["status"] == "ok"
assert by_name["truth_chain_quality_summary_latency"]["direction"] == "below"
assert report["overall_status"] == "partial"
assert report["overall_compliance"] == 1.0
@@ -121,6 +130,7 @@ async def test_fetch_report_classifies_hard_red_line_violation(monkeypatch):
'sum(rate(automation_operation_log_total{outcome="auto_executed"}[5m]))': "0",
"sum(rate(approval_records_high_confidence_total[1h]))": "0",
"max(knowledge_entries_created_24h) or max(sli:km_growth_rate:24h)": "3",
QUALITY_SUMMARY_LATENCY_QUERY: "1.2",
}
monkeypatch.setattr(
@@ -142,6 +152,36 @@ async def test_fetch_report_classifies_hard_red_line_violation(monkeypatch):
assert report["overall_status"] == "violated"
@pytest.mark.asyncio
async def test_fetch_report_classifies_below_direction_slo(monkeypatch):
values = {
"sum(rate(automation_operation_log_total[5m]))": "0",
'sum(rate(automation_operation_log_total{outcome="auto_executed"}[5m]))': "0",
"sum(rate(approval_records_high_confidence_total[1h]))": "0",
"max(knowledge_entries_created_24h) or max(sli:km_growth_rate:24h)": "24",
QUALITY_SUMMARY_LATENCY_QUERY: "9.5",
}
monkeypatch.setattr(
"httpx.AsyncClient",
lambda *args, **kwargs: _FakePrometheusClient(values),
)
monkeypatch.setattr(
Adr100SloStatusService,
"_fetch_verification_coverage",
_low_volume_coverage,
)
report = await Adr100SloStatusService().fetch_report()
by_name = {metric["name"]: metric for metric in report["metrics"]}
latency = by_name["truth_chain_quality_summary_latency"]
assert latency["status"] == "violated"
assert latency["direction"] == "below"
assert latency["value"] == 9.5
assert report["overall_status"] == "violated"
def test_verification_coverage_payload_flags_backlog():
payload = _build_verification_coverage_payload(
{

View File

@@ -797,6 +797,12 @@ class TestRunSelfCheckGlobalFailureAlert:
assert "governance_self_failure" not in calls
QUALITY_SUMMARY_LATENCY_QUERY = (
'max(awooop_truth_chain_quality_summary_last_duration_seconds{'
'project_id="awoooi",limit="8",success="true"})'
)
class _FakePrometheusResponse:
def __init__(self, value: str) -> None:
self._value = value
@@ -809,7 +815,7 @@ class _FakePrometheusResponse:
class _FakePrometheusClient:
def __init__(self, value: str) -> None:
def __init__(self, value: str | dict[str, str]) -> None:
self._value = value
self.queries: list[str] = []
@@ -820,7 +826,10 @@ class _FakePrometheusClient:
return False
async def get(self, *args, **kwargs): # noqa: ANN002, ANN003
self.queries.append(str(kwargs.get("params", {}).get("query", "")))
query = str(kwargs.get("params", {}).get("query", ""))
self.queries.append(query)
if isinstance(self._value, dict):
return _FakePrometheusResponse(self._value.get(query, "NaN"))
return _FakePrometheusResponse(self._value)
@@ -838,6 +847,7 @@ class TestCheckSloCompliance:
"decision_accuracy",
"confidence_calibration",
"km_growth_rate",
"truth_chain_quality_summary_latency",
):
assert result[name]["status"] == "skipped"
assert result[name]["reason"] == "prometheus_nan_or_inf"
@@ -847,7 +857,13 @@ class TestCheckSloCompliance:
async def test_km_growth_prefers_db_derived_24h_gauge(self):
"""KM SLO 要優先使用 DB 24h gauge避免新 counter 暖機時誤報 0."""
agent = _make_agent()
client = _FakePrometheusClient("25")
client = _FakePrometheusClient({
"sli:autonomy_rate:5m": "0.95",
"sli:decision_accuracy:5m": "0.96",
"sli:confidence_calibration:1h": "0.97",
"max(knowledge_entries_created_24h) or max(sli:km_growth_rate:24h)": "25",
QUALITY_SUMMARY_LATENCY_QUERY: "1.1",
})
with patch("httpx.AsyncClient", return_value=client):
result = await agent.check_slo_compliance()
@@ -855,3 +871,4 @@ class TestCheckSloCompliance:
assert "max(knowledge_entries_created_24h) or max(sli:km_growth_rate:24h)" in client.queries
assert result["km_growth_rate"]["status"] == "ok"
assert result["km_growth_rate"]["value"] == 25
assert result["truth_chain_quality_summary_latency"]["status"] == "ok"