fix(awooop): record source review dry-run audit
This commit is contained in:
@@ -869,11 +869,20 @@ def _recurrence_handoff_context(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _is_source_correlation_review_payload(payload: dict[str, Any]) -> bool:
|
||||
state = _as_dict(payload.get("current_state_summary"))
|
||||
return (
|
||||
state.get("work_item_kind") == "source_correlation_review"
|
||||
or payload.get("next_step") == "review_provider_source_match"
|
||||
)
|
||||
|
||||
|
||||
async def _record_recurrence_work_item_dry_run_history(
|
||||
payload: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
incident_id = str(payload.get("incident_id") or "")
|
||||
if not incident_id:
|
||||
source_review = _is_source_correlation_review_payload(payload)
|
||||
if not incident_id and not source_review:
|
||||
return {"recorded": False, "reason": "missing_incident_id"}
|
||||
|
||||
history: dict[str, Any] = {
|
||||
@@ -891,7 +900,7 @@ async def _record_recurrence_work_item_dry_run_history(
|
||||
|
||||
record = await get_alert_operation_log_repository().append(
|
||||
"PRE_FLIGHT_PASSED" if allowed else "PRE_FLIGHT_FAILED",
|
||||
incident_id=incident_id,
|
||||
incident_id=incident_id or None,
|
||||
auto_repair_id=str(payload.get("auto_repair_id") or "") or None,
|
||||
actor="awooop_recurrence_work_item_service",
|
||||
action_detail=f"recurrence_work_item_dry_run:{payload.get('mode')}"[:200],
|
||||
@@ -907,26 +916,29 @@ async def _record_recurrence_work_item_dry_run_history(
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
try:
|
||||
from src.services.approval_db import get_timeline_service
|
||||
if incident_id:
|
||||
try:
|
||||
from src.services.approval_db import get_timeline_service
|
||||
|
||||
event = await get_timeline_service().add_event(
|
||||
event_type="verifier",
|
||||
status="success" if allowed else "warning",
|
||||
title="AwoooP recurrence work item dry-run",
|
||||
description=_recurrence_history_description(context),
|
||||
actor="awooop_recurrence_work_item_service",
|
||||
actor_role=str(payload.get("mode") or "dry_run"),
|
||||
incident_id=incident_id,
|
||||
)
|
||||
if event:
|
||||
history["timeline_event_id"] = event.get("id")
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"awooop_recurrence_work_item_timeline_history_failed",
|
||||
incident_id=incident_id,
|
||||
error=str(exc),
|
||||
)
|
||||
event = await get_timeline_service().add_event(
|
||||
event_type="verifier",
|
||||
status="success" if allowed else "warning",
|
||||
title="AwoooP recurrence work item dry-run",
|
||||
description=_recurrence_history_description(context),
|
||||
actor="awooop_recurrence_work_item_service",
|
||||
actor_role=str(payload.get("mode") or "dry_run"),
|
||||
incident_id=incident_id,
|
||||
)
|
||||
if event:
|
||||
history["timeline_event_id"] = event.get("id")
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"awooop_recurrence_work_item_timeline_history_failed",
|
||||
incident_id=incident_id,
|
||||
error=str(exc),
|
||||
)
|
||||
else:
|
||||
history["timeline_reason"] = "source_review_not_incident_scoped"
|
||||
|
||||
history["recorded"] = bool(
|
||||
history.get("alert_operation_id") or history.get("timeline_event_id")
|
||||
@@ -940,7 +952,8 @@ async def _record_recurrence_work_item_handoff_history(
|
||||
payload: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
incident_id = str(payload.get("incident_id") or "")
|
||||
if not incident_id:
|
||||
source_review = _is_source_correlation_review_payload(payload)
|
||||
if not incident_id and not source_review:
|
||||
return {"recorded": False, "reason": "missing_incident_id"}
|
||||
|
||||
history: dict[str, Any] = {
|
||||
@@ -958,7 +971,7 @@ async def _record_recurrence_work_item_handoff_history(
|
||||
|
||||
record = await get_alert_operation_log_repository().append(
|
||||
"ESCALATED",
|
||||
incident_id=incident_id,
|
||||
incident_id=incident_id or None,
|
||||
auto_repair_id=str(payload.get("auto_repair_id") or "") or None,
|
||||
actor="awooop_recurrence_work_item_service",
|
||||
action_detail=(
|
||||
@@ -976,26 +989,29 @@ async def _record_recurrence_work_item_handoff_history(
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
try:
|
||||
from src.services.approval_db import get_timeline_service
|
||||
if incident_id:
|
||||
try:
|
||||
from src.services.approval_db import get_timeline_service
|
||||
|
||||
event = await get_timeline_service().add_event(
|
||||
event_type="human",
|
||||
status="warning" if allowed else "error",
|
||||
title="AwoooP recurrence work item handoff",
|
||||
description=_recurrence_handoff_history_description(context),
|
||||
actor="awooop_recurrence_work_item_service",
|
||||
actor_role=str(payload.get("handoff_kind") or "handoff"),
|
||||
incident_id=incident_id,
|
||||
)
|
||||
if event:
|
||||
history["timeline_event_id"] = event.get("id")
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"awooop_recurrence_work_item_handoff_timeline_history_failed",
|
||||
incident_id=incident_id,
|
||||
error=str(exc),
|
||||
)
|
||||
event = await get_timeline_service().add_event(
|
||||
event_type="human",
|
||||
status="warning" if allowed else "error",
|
||||
title="AwoooP recurrence work item handoff",
|
||||
description=_recurrence_handoff_history_description(context),
|
||||
actor="awooop_recurrence_work_item_service",
|
||||
actor_role=str(payload.get("handoff_kind") or "handoff"),
|
||||
incident_id=incident_id,
|
||||
)
|
||||
if event:
|
||||
history["timeline_event_id"] = event.get("id")
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"awooop_recurrence_work_item_handoff_timeline_history_failed",
|
||||
incident_id=incident_id,
|
||||
error=str(exc),
|
||||
)
|
||||
else:
|
||||
history["timeline_reason"] = "source_review_not_incident_scoped"
|
||||
|
||||
history["recorded"] = bool(
|
||||
history.get("alert_operation_id") or history.get("timeline_event_id")
|
||||
|
||||
@@ -555,6 +555,82 @@ def test_build_recurrence_work_item_dry_run_returns_ticket_preview_without_write
|
||||
assert dry_run["read_model_route"]["required_scope"] == "read"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_correlation_dry_run_history_records_without_incident(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
appended: list[dict[str, object]] = []
|
||||
|
||||
class FakeRecord:
|
||||
id = "alert-op-1"
|
||||
|
||||
class FakeRepo:
|
||||
async def append(self, event_type: str, **kwargs): # noqa: ANN001
|
||||
appended.append({"event_type": event_type, **kwargs})
|
||||
return FakeRecord()
|
||||
|
||||
from src.repositories import alert_operation_log_repository
|
||||
|
||||
monkeypatch.setattr(
|
||||
alert_operation_log_repository,
|
||||
"get_alert_operation_log_repository",
|
||||
lambda: FakeRepo(),
|
||||
)
|
||||
|
||||
history = (
|
||||
await channel_event_dossier_service._record_recurrence_work_item_dry_run_history(
|
||||
{
|
||||
"source": "channel_event_dossier.recurrence",
|
||||
"project_id": "awoooi",
|
||||
"work_item_id": (
|
||||
"source-evidence:sentry:upstream_canary:"
|
||||
"awoooi-canary-codex-t115-production"
|
||||
),
|
||||
"incident_id": None,
|
||||
"auto_repair_id": None,
|
||||
"mode": "observe",
|
||||
"requested_mode": "auto",
|
||||
"allowed": True,
|
||||
"executed": True,
|
||||
"safety_level": "read_only",
|
||||
"writes_incident_state": False,
|
||||
"writes_auto_repair_result": False,
|
||||
"writes_ticket": False,
|
||||
"verification_result_preview": "observe_only",
|
||||
"current_state_summary": {
|
||||
"work_item_kind": "source_correlation_review",
|
||||
"work_item_next_step": "review_provider_source_match",
|
||||
"repair_status": "source_correlation_review",
|
||||
"latest_stage": "upstream_canary",
|
||||
"latest_provider_event_id": (
|
||||
"sentry:upstream_canary:"
|
||||
"awoooi-canary-codex-t115-production"
|
||||
),
|
||||
},
|
||||
"ticket_preview": {"would_create": False},
|
||||
"read_model_route": {
|
||||
"agent_id": "awooop_recurrence_coordinator",
|
||||
"tool_name": "channel_event_dossier.recurrence",
|
||||
"required_scope": "read",
|
||||
},
|
||||
"checks": [],
|
||||
"next_step": "review_provider_source_match",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assert history == {
|
||||
"recorded": True,
|
||||
"alert_operation_id": "alert-op-1",
|
||||
"timeline_event_id": None,
|
||||
"timeline_reason": "source_review_not_incident_scoped",
|
||||
}
|
||||
assert appended[0]["event_type"] == "PRE_FLIGHT_PASSED"
|
||||
assert appended[0]["incident_id"] is None
|
||||
assert appended[0]["actor"] == "awooop_recurrence_work_item_service"
|
||||
assert appended[0]["context"]["work_item_id"].startswith("source-evidence:")
|
||||
|
||||
|
||||
def test_build_recurrence_work_item_handoff_records_ticket_proposal_contract() -> None:
|
||||
recurrence = build_dossier_recurrence(
|
||||
[
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- `next_step=review_provider_source_match`
|
||||
- `reason=provider_native_evidence_unlinked`
|
||||
- 不寫入 Incident / AutoRepair / Ticket,只提供 preview / dry-run / handoff read model。
|
||||
- Source review dry-run / handoff 沒有 Incident 時仍會寫入 `alert_operation_log`(`incident_id=null`),避免 operator 看到 `missing_incident_id` 誤判為沒有 DB audit;timeline 仍只在有 Incident 時寫入。
|
||||
- `/api/v1/platform/events/dossier/recurrence` summary 新增 `source_correlation_review_group_total`。
|
||||
- AwoooP Runs 前端「重複告警關聯」新增「來源待審」指標,卡片顯示事件 stage,讓 operator 可看見 provider-native evidence 已進 AwoooP 但仍需配對審核。
|
||||
- AwoooP Work Items 同步顯示 source review count、stage、provider event id、Sentry / SignOz refs,避免從 Runs 點進工作項後掉成 unknown。
|
||||
@@ -23,7 +24,7 @@
|
||||
python -m py_compile apps/api/src/services/channel_event_dossier_service.py apps/api/src/api/v1/platform/events.py
|
||||
-> pass
|
||||
DATABASE_URL=postgresql+asyncpg://test:test@localhost/test pytest -q tests/test_channel_event_dossier_service.py
|
||||
-> 14 passed
|
||||
-> 15 passed
|
||||
pnpm --dir apps/web exec tsc --noEmit
|
||||
-> pass
|
||||
NEXT_PUBLIC_API_URL=https://awoooi.wooo.work pnpm --dir apps/web run build
|
||||
@@ -45,7 +46,8 @@ python -m ruff check src/services/channel_event_dossier_service.py src/api/v1/pl
|
||||
- AwoooP 告警可觀測鏈:99.985% → 99.988%。
|
||||
- 前端 AI 自動化管理介面同步:99.99%(Runs / Work Items recurrence panel 已同步來源待審)。
|
||||
- 完整 AI 自動化管理產品化:99.65% → 99.68%。
|
||||
- 剩餘:推 Gitea main、等待 CI/CD、production API / frontend 驗證。
|
||||
- 第一輪部署:Gitea Actions 1952 Code Review success;1951 CD success;production recurrence schema 已出現 `source_correlation_review_group_total` / `latest_stage` / `stage_counts`。
|
||||
- 剩餘:source review audit 小修再推 Gitea main,等待第二輪 CI/CD、production dry-run history 驗證。
|
||||
|
||||
## 2026-05-20|T115 Provider-native upstream canary 接入
|
||||
|
||||
|
||||
Reference in New Issue
Block a user