fix(awooop): preserve recurrence repair fields
All checks were successful
Code Review / ai-code-review (push) Successful in 21s
CD Pipeline / tests (push) Successful in 1m20s
CD Pipeline / build-and-deploy (push) Successful in 3m37s
CD Pipeline / post-deploy-checks (push) Successful in 1m38s

This commit is contained in:
Your Name
2026-05-18 20:06:02 +08:00
parent e36c9b1800
commit 4b8f946699
2 changed files with 88 additions and 5 deletions

View File

@@ -11,7 +11,7 @@ from typing import Any
from uuid import UUID
from fastapi import APIRouter, Query
from pydantic import BaseModel
from pydantic import BaseModel, Field
from src.services.channel_event_dossier_service import (
fetch_channel_event_dossier,
@@ -123,6 +123,10 @@ class ChannelEventRecurrenceSummary(BaseModel):
duplicate_event_total: int
linked_run_total: int
unlinked_event_total: int
auto_repair_linked_total: int = 0
verified_repair_group_total: int = 0
open_work_item_group_total: int = 0
manual_gate_group_total: int = 0
latest_received_at: datetime | None
@@ -140,6 +144,10 @@ class ChannelEventRecurrenceItem(BaseModel):
latest_run_id: UUID | None
latest_run_state: str | None
latest_agent_id: str | None
latest_incident_id: str | None = None
incident_ids: list[str] = Field(default_factory=list)
repair_summary: dict[str, Any] | None = None
work_item: dict[str, Any] | None = None
occurrence_total: int
duplicate_total: int
linked_run_total: int
@@ -172,7 +180,9 @@ class ChannelEventRecurrenceResponse(BaseModel):
async def get_event_dossier(
project_id: str | None = Query(None, description="租戶 ID可選"),
run_id: UUID | None = Query(None, description="Run ID可選"),
provider_event_id: str | None = Query(None, description="provider_event_id可選"),
provider_event_id: str | None = Query(
None, description="provider_event_id可選"
),
limit: int = Query(20, ge=1, le=50, description="最多返回筆數"),
) -> dict[str, Any]:
return await fetch_channel_event_dossier(
@@ -194,7 +204,9 @@ async def get_event_dossier(
)
async def get_event_dossier_coverage(
project_id: str | None = Query(None, description="租戶 ID可選"),
provider: str | None = Query(None, description="provider可選如 sentry / signoz"),
provider: str | None = Query(
None, description="provider可選如 sentry / signoz"
),
limit: int = Query(100, ge=1, le=200, description="最多納入統計筆數"),
) -> dict[str, Any]:
return await fetch_channel_event_dossier_coverage(
@@ -215,7 +227,9 @@ async def get_event_dossier_coverage(
)
async def get_event_dossier_recurrence(
project_id: str | None = Query(None, description="租戶 ID可選"),
provider: str | None = Query(None, description="provider可選如 alertmanager / sentry / signoz"),
provider: str | None = Query(
None, description="provider可選如 alertmanager / sentry / signoz"
),
limit: int = Query(100, ge=1, le=300, description="最多納入統計筆數"),
) -> dict[str, Any]:
return await fetch_channel_event_dossier_recurrence(
@@ -237,7 +251,9 @@ async def get_event_dossier_recurrence(
async def list_recent_events(
project_id: str | None = Query(None, description="租戶 ID可選"),
channel_type: str | None = Query(None, description="通道類型(可選)"),
provider_prefix: str | None = Query(None, description="provider_event_id 前綴(可選)"),
provider_prefix: str | None = Query(
None, description="provider_event_id 前綴(可選)"
),
limit: int = Query(20, ge=1, le=100, description="最多返回筆數"),
) -> dict[str, Any]:
return await list_recent_channel_events(

View File

@@ -5,6 +5,7 @@ from uuid import UUID
import pytest
from fastapi import HTTPException
from src.api.v1.platform.events import ChannelEventRecurrenceResponse
from src.services import channel_event_dossier_service
from src.services.channel_event_dossier_service import (
build_dossier_coverage,
@@ -284,6 +285,72 @@ def test_build_dossier_recurrence_groups_events_and_run_state() -> None:
}
def test_recurrence_response_model_preserves_repair_work_item_fields() -> None:
response = ChannelEventRecurrenceResponse.model_validate(
{
"project_id": "awoooi",
"limit": 20,
"summary": {
"source_event_total": 1,
"recurrence_group_total": 1,
"recurrent_group_total": 1,
"duplicate_event_total": 0,
"linked_run_total": 1,
"unlinked_event_total": 0,
"auto_repair_linked_total": 1,
"verified_repair_group_total": 1,
"open_work_item_group_total": 0,
"manual_gate_group_total": 0,
"latest_received_at": "2026-05-13T13:47:00",
},
"items": [
{
"recurrence_key": "fingerprint:fp-host-disk",
"provider": "alertmanager",
"alertname": "HostDiskUsageHigh",
"severity": "warning",
"namespace": "node",
"target_resource": "host-110",
"fingerprint": "fp-host-disk",
"latest_event_id": "11111111-1111-4111-8111-111111111111",
"latest_provider_event_id": "alertmanager:received:1",
"latest_content_preview": "Host disk pressure",
"latest_run_id": "22222222-2222-4222-8222-222222222222",
"latest_run_state": "completed",
"latest_agent_id": "openclaw",
"latest_incident_id": "INC-20260513-ABCD",
"incident_ids": ["INC-20260513-ABCD"],
"repair_summary": {
"status": "auto_repair_verified",
"latest_auto_repair_id": "repair-1",
},
"work_item": {
"work_item_id": "verification:INC-20260513-ABCD:repair-1",
"status": "closed",
},
"occurrence_total": 1,
"duplicate_total": 0,
"linked_run_total": 1,
"source_ref_total": 3,
"missing_source_refs_total": 0,
"sentry_ref_total": 0,
"signoz_ref_total": 0,
"alert_ref_total": 1,
"run_state_counts": {"completed": 1},
"first_received_at": "2026-05-13T13:47:00",
"latest_received_at": "2026-05-13T13:47:00",
}
],
}
)
payload = response.model_dump()
assert payload["summary"]["auto_repair_linked_total"] == 1
assert payload["items"][0]["latest_incident_id"] == "INC-20260513-ABCD"
assert payload["items"][0]["repair_summary"]["status"] == "auto_repair_verified"
assert payload["items"][0]["work_item"]["status"] == "closed"
@pytest.mark.asyncio
async def test_fetch_channel_event_dossier_requires_source() -> None:
with pytest.raises(HTTPException) as exc_info: