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 uuid import UUID
from fastapi import APIRouter, Query from fastapi import APIRouter, Query
from pydantic import BaseModel from pydantic import BaseModel, Field
from src.services.channel_event_dossier_service import ( from src.services.channel_event_dossier_service import (
fetch_channel_event_dossier, fetch_channel_event_dossier,
@@ -123,6 +123,10 @@ class ChannelEventRecurrenceSummary(BaseModel):
duplicate_event_total: int duplicate_event_total: int
linked_run_total: int linked_run_total: int
unlinked_event_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 latest_received_at: datetime | None
@@ -140,6 +144,10 @@ class ChannelEventRecurrenceItem(BaseModel):
latest_run_id: UUID | None latest_run_id: UUID | None
latest_run_state: str | None latest_run_state: str | None
latest_agent_id: 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 occurrence_total: int
duplicate_total: int duplicate_total: int
linked_run_total: int linked_run_total: int
@@ -172,7 +180,9 @@ class ChannelEventRecurrenceResponse(BaseModel):
async def get_event_dossier( async def get_event_dossier(
project_id: str | None = Query(None, description="租戶 ID可選"), project_id: str | None = Query(None, description="租戶 ID可選"),
run_id: UUID | None = Query(None, description="Run 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="最多返回筆數"), limit: int = Query(20, ge=1, le=50, description="最多返回筆數"),
) -> dict[str, Any]: ) -> dict[str, Any]:
return await fetch_channel_event_dossier( return await fetch_channel_event_dossier(
@@ -194,7 +204,9 @@ async def get_event_dossier(
) )
async def get_event_dossier_coverage( async def get_event_dossier_coverage(
project_id: str | None = Query(None, description="租戶 ID可選"), 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="最多納入統計筆數"), limit: int = Query(100, ge=1, le=200, description="最多納入統計筆數"),
) -> dict[str, Any]: ) -> dict[str, Any]:
return await fetch_channel_event_dossier_coverage( return await fetch_channel_event_dossier_coverage(
@@ -215,7 +227,9 @@ async def get_event_dossier_coverage(
) )
async def get_event_dossier_recurrence( async def get_event_dossier_recurrence(
project_id: str | None = Query(None, description="租戶 ID可選"), 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="最多納入統計筆數"), limit: int = Query(100, ge=1, le=300, description="最多納入統計筆數"),
) -> dict[str, Any]: ) -> dict[str, Any]:
return await fetch_channel_event_dossier_recurrence( return await fetch_channel_event_dossier_recurrence(
@@ -237,7 +251,9 @@ async def get_event_dossier_recurrence(
async def list_recent_events( async def list_recent_events(
project_id: str | None = Query(None, description="租戶 ID可選"), project_id: str | None = Query(None, description="租戶 ID可選"),
channel_type: str | None = Query(None, description="通道類型(可選)"), 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="最多返回筆數"), limit: int = Query(20, ge=1, le=100, description="最多返回筆數"),
) -> dict[str, Any]: ) -> dict[str, Any]:
return await list_recent_channel_events( return await list_recent_channel_events(

View File

@@ -5,6 +5,7 @@ from uuid import UUID
import pytest import pytest
from fastapi import HTTPException from fastapi import HTTPException
from src.api.v1.platform.events import ChannelEventRecurrenceResponse
from src.services import channel_event_dossier_service from src.services import channel_event_dossier_service
from src.services.channel_event_dossier_service import ( from src.services.channel_event_dossier_service import (
build_dossier_coverage, 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 @pytest.mark.asyncio
async def test_fetch_channel_event_dossier_requires_source() -> None: async def test_fetch_channel_event_dossier_requires_source() -> None:
with pytest.raises(HTTPException) as exc_info: with pytest.raises(HTTPException) as exc_info: