fix(awooop): preserve recurrence repair fields
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user