feat(awooop): record source correlation review decisions
All checks were successful
Code Review / ai-code-review (push) Successful in 12s
CD Pipeline / tests (push) Successful in 3m59s
CD Pipeline / build-and-deploy (push) Successful in 3m48s
CD Pipeline / post-deploy-checks (push) Successful in 1m55s

This commit is contained in:
Your Name
2026-05-21 09:53:36 +08:00
parent ee5a54ecba
commit 88e7477a7c
8 changed files with 933 additions and 26 deletions

View File

@@ -21,12 +21,14 @@ from src.services.channel_event_dossier_service import (
RecurrenceWorkItemHandoffKind,
RecurrenceWorkItemMode,
RecurrenceWorkItemNotFoundError,
SourceCorrelationReviewDecision,
fetch_channel_event_dossier,
fetch_channel_event_dossier_coverage,
fetch_channel_event_dossier_recurrence,
fetch_recurrence_work_item_dry_run,
fetch_recurrence_work_item_handoff,
fetch_recurrence_work_item_preview,
fetch_source_correlation_review_decision,
)
from src.services.channel_hub import record_external_alert_event
from src.services.platform_operator_service import list_recent_channel_events
@@ -173,6 +175,7 @@ class ChannelEventRecurrenceSummary(BaseModel):
automation_gap_group_total: int = 0
failed_repair_group_total: int = 0
source_correlation_review_group_total: int = 0
source_correlation_decision_recorded_group_total: int = 0
latest_received_at: datetime | None
@@ -195,6 +198,7 @@ class ChannelEventRecurrenceItem(BaseModel):
incident_ids: list[str] = Field(default_factory=list)
repair_summary: dict[str, Any] | None = None
work_item: dict[str, Any] | None = None
source_correlation_review: dict[str, Any] | None = None
occurrence_total: int
duplicate_total: int
linked_run_total: int
@@ -237,6 +241,19 @@ class RecurrenceWorkItemHandoffRequest(BaseModel):
limit: int = Field(default=300, ge=1, le=300)
class SourceCorrelationReviewDecisionRequest(BaseModel):
"""Record-only source evidence review decision."""
project_id: str | None = Field(default=None, min_length=1)
work_item_id: str = Field(min_length=1)
decision: SourceCorrelationReviewDecision
target_incident_id: str | None = Field(default=None, min_length=1, max_length=30)
reviewer_id: str = Field(default="operator_console", min_length=1, max_length=100)
operator_note: str | None = Field(default=None, max_length=500)
provider: str | None = Field(default=None, min_length=1)
limit: int = Field(default=300, ge=1, le=300)
@router.get(
"/events/dossier",
response_model=ChannelEventDossierResponse,
@@ -471,6 +488,36 @@ async def handoff_event_recurrence_work_item(
) from exc
@router.post(
"/events/dossier/recurrence/source-correlation/review",
summary="記錄來源證據與 Incident 配對審核結果",
description=(
"針對 source_correlation_review work item 記錄 operator 審核決定。"
"本 API 僅寫入 alert_operation_log / 可選 timeline_events"
"不修改 Incident 狀態、不回寫 source event、不建立外部 ticket。"
),
)
async def review_source_correlation_work_item(
request: SourceCorrelationReviewDecisionRequest,
) -> dict[str, Any]:
try:
return await fetch_source_correlation_review_decision(
project_id=request.project_id,
work_item_id=request.work_item_id,
decision=request.decision,
target_incident_id=request.target_incident_id,
reviewer_id=request.reviewer_id,
operator_note=request.operator_note,
provider=request.provider,
limit=request.limit,
)
except RecurrenceWorkItemNotFoundError as exc:
raise HTTPException(
status_code=404,
detail="recurrence_work_item_not_found",
) from exc
@router.get(
"/events/recent",
response_model=RecentEventsResponse,

View File

@@ -26,9 +26,18 @@ _MAX_REPAIR_INCIDENTS = 200
_SOURCE_CORRELATION_REVIEW_PROVIDERS = {"sentry", "signoz"}
_SOURCE_CORRELATION_REVIEW_EXCLUDED_STAGES = {"heartbeat"}
_SOURCE_CORRELATION_WORK_ITEM_ID_MAX = 180
_SOURCE_CORRELATION_DECISION_SCHEMA_VERSION = (
"awooop_source_correlation_review_decision_v1"
)
_SOURCE_CORRELATION_REVIEW_ACTOR = "awooop_source_correlation_review_service"
_INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b")
RecurrenceWorkItemMode = Literal["auto", "ticket", "reverify", "approval_review", "observe"]
RecurrenceWorkItemHandoffKind = Literal["ticket_proposal", "manual_review"]
SourceCorrelationReviewDecision = Literal[
"accepted",
"rejected",
"needs_more_evidence",
]
class RecurrenceWorkItemNotFoundError(LookupError):
@@ -42,9 +51,9 @@ def _as_dict(value: Any) -> dict[str, Any]:
def _json_safe(value: Any) -> Any:
if isinstance(value, dict):
return {str(key): _json_safe(item) for key, item in value.items()}
if isinstance(value, (list, tuple, set)):
if isinstance(value, list | tuple | set):
return [_json_safe(item) for item in value]
if value is None or isinstance(value, (str, int, float, bool)):
if value is None or isinstance(value, str | int | float | bool):
return value
return str(value)
@@ -94,6 +103,58 @@ def _source_correlation_work_item_id(group: dict[str, Any]) -> str:
return f"source-evidence:{source_id}"[:_SOURCE_CORRELATION_WORK_ITEM_ID_MAX]
def _source_correlation_review_outcome(decision: str) -> dict[str, str]:
if decision == "accepted":
return {
"status": "source_correlation_accepted",
"next_step": "verify_source_match_in_status_chain",
"reason": "provider_native_evidence_accepted",
"review_status": "accepted",
}
if decision == "rejected":
return {
"status": "source_correlation_rejected",
"next_step": "monitor_for_new_provider_evidence",
"reason": "provider_native_evidence_rejected",
"review_status": "rejected",
}
return {
"status": "source_correlation_review",
"next_step": "collect_more_source_evidence",
"reason": "provider_native_evidence_needs_more_evidence",
"review_status": "needs_more_evidence",
}
def _normalize_source_review_decision(row: dict[str, Any]) -> dict[str, Any] | None:
context = _as_dict(row.get("context"))
if context.get("schema_version") != _SOURCE_CORRELATION_DECISION_SCHEMA_VERSION:
return None
work_item_id = str(context.get("work_item_id") or "").strip()
decision = str(context.get("decision") or "").strip()
if not work_item_id or decision not in {
"accepted",
"rejected",
"needs_more_evidence",
}:
return None
return {
"schema_version": _SOURCE_CORRELATION_DECISION_SCHEMA_VERSION,
"review_id": row.get("id"),
"work_item_id": work_item_id,
"decision": decision,
"review_status": context.get("review_status") or decision,
"target_incident_id": context.get("target_incident_id"),
"reviewer_id": context.get("reviewer_id"),
"operator_note": context.get("operator_note"),
"latest_provider_event_id": context.get("latest_provider_event_id"),
"latest_stage": context.get("latest_stage"),
"recorded_at": row.get("created_at"),
"history": context.get("history") or {},
}
def _append_unique(values: list[str], candidate: Any) -> None:
text_value = str(candidate or "").strip()
if text_value and text_value not in values:
@@ -148,10 +209,12 @@ def build_dossier_recurrence(
project_id: str,
limit: int,
repair_summaries_by_incident: dict[str, dict[str, Any]] | None = None,
source_review_decisions_by_work_item: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
"""Group recent source events into recurrence buckets with linked run state."""
groups: dict[str, dict[str, Any]] = {}
repair_summaries = repair_summaries_by_incident or {}
source_review_decisions = source_review_decisions_by_work_item or {}
for row in rows:
event = build_dossier_event(row)
@@ -249,7 +312,11 @@ def build_dossier_recurrence(
for group in groups.values():
run_ids = group.pop("_run_ids")
group["linked_run_total"] = len(run_ids)
_attach_work_item_summary(group, repair_summaries)
_attach_work_item_summary(
group,
repair_summaries,
source_review_decisions,
)
linked_run_total += len(run_ids)
items.append(group)
@@ -317,6 +384,9 @@ def build_dossier_recurrence(
if _as_dict(item.get("repair_summary")).get("status")
== "source_correlation_review"
),
"source_correlation_decision_recorded_group_total": sum(
1 for item in items if item.get("source_correlation_review")
),
"latest_received_at": latest_received_at,
},
"items": items,
@@ -355,13 +425,21 @@ def _repair_status(
def _work_item_status(repair_status: str) -> str:
if repair_status == "no_incident_link":
return "none"
if repair_status == "auto_repair_verified":
if repair_status in {
"auto_repair_verified",
"source_correlation_accepted",
"source_correlation_rejected",
}:
return "closed"
return "open"
def _work_item_kind(repair_status: str, auto_repair_id: Any) -> str:
if repair_status == "source_correlation_review":
if repair_status in {
"source_correlation_review",
"source_correlation_accepted",
"source_correlation_rejected",
}:
return "source_correlation_review"
if auto_repair_id:
return "verification"
@@ -377,6 +455,8 @@ def _work_item_kind(repair_status: str, auto_repair_id: Any) -> str:
def _work_item_next_step(repair_status: str) -> str:
return {
"source_correlation_review": "review_provider_source_match",
"source_correlation_accepted": "verify_source_match_in_status_chain",
"source_correlation_rejected": "monitor_for_new_provider_evidence",
"auto_repair_succeeded_unverified": "run_post_verification",
"auto_repair_failed": "triage_failed_repair",
"auto_repair_recorded": "review_repair_record",
@@ -390,6 +470,8 @@ def _work_item_next_step(repair_status: str) -> str:
def _work_item_reason(repair_status: str) -> str:
return {
"source_correlation_review": "provider_native_evidence_unlinked",
"source_correlation_accepted": "provider_native_evidence_accepted",
"source_correlation_rejected": "provider_native_evidence_rejected",
"auto_repair_succeeded_unverified": "auto_repair_missing_verification",
"auto_repair_failed": "auto_repair_failed",
"auto_repair_recorded": "auto_repair_record_needs_review",
@@ -403,6 +485,7 @@ def _work_item_reason(repair_status: str) -> str:
def _attach_work_item_summary(
group: dict[str, Any],
repair_summaries_by_incident: dict[str, dict[str, Any]],
source_review_decisions_by_work_item: dict[str, dict[str, Any]],
) -> None:
incident_ids = [
str(incident_id) for incident_id in group.get("incident_ids", []) if incident_id
@@ -450,17 +533,38 @@ def _attach_work_item_summary(
elif status_value == "source_correlation_review" and work_status != "none":
work_item_id = _source_correlation_work_item_id(group)
source_review_decision = (
source_review_decisions_by_work_item.get(work_item_id)
if work_item_id
else None
)
matched_incident_id = None
work_item_next_step = _work_item_next_step(status_value)
work_item_reason = _work_item_reason(status_value)
if source_review_decision:
outcome = _source_correlation_review_outcome(
str(source_review_decision.get("decision") or "")
)
status_value = outcome["status"]
repair_payload["status"] = status_value
work_status = _work_item_status(status_value)
matched_incident_id = source_review_decision.get("target_incident_id")
work_item_next_step = outcome["next_step"]
work_item_reason = outcome["reason"]
group["source_correlation_review"] = source_review_decision
group["latest_incident_id"] = latest_incident_id
group["repair_summary"] = repair_payload
group["work_item"] = {
"schema_version": "awooop_recurrence_work_item_link_v1",
"work_item_id": work_item_id,
"incident_id": latest_incident_id,
"matched_incident_id": matched_incident_id,
"auto_repair_id": auto_repair_id,
"status": work_status,
"kind": _work_item_kind(status_value, auto_repair_id),
"next_step": _work_item_next_step(status_value),
"reason": _work_item_reason(status_value),
"next_step": work_item_next_step,
"reason": work_item_reason,
"needs_human": work_status == "open",
}
@@ -481,6 +585,9 @@ def _recurrence_work_item_target(item: dict[str, Any]) -> dict[str, Any]:
"latest_run_state": item.get("latest_run_state"),
"latest_agent_id": item.get("latest_agent_id"),
"latest_incident_id": item.get("latest_incident_id"),
"matched_incident_id": _as_dict(item.get("work_item")).get(
"matched_incident_id"
),
}
@@ -653,6 +760,8 @@ def _recurrence_current_state_summary(
"latest_provider_event_id": item.get("latest_provider_event_id"),
"latest_run_state": item.get("latest_run_state"),
"latest_run_id": item.get("latest_run_id"),
"matched_incident_id": work_item.get("matched_incident_id"),
"source_correlation_review": item.get("source_correlation_review"),
"repair_status": repair_summary.get("status"),
"latest_auto_repair_id": repair_summary.get("latest_auto_repair_id"),
"latest_verification_result": repair_summary.get("latest_verification_result"),
@@ -1063,6 +1172,243 @@ def _recurrence_handoff_history_description(context: dict[str, Any]) -> str:
)[:500]
def _source_correlation_review_checks(
item: dict[str, Any],
work_item: dict[str, Any],
*,
decision: str,
target_incident_id: str | None,
) -> list[dict[str, Any]]:
source_ref_total = int(item.get("source_ref_total") or 0)
is_source_review = work_item.get("kind") == "source_correlation_review"
checks = [
{
"name": "source_review_work_item",
"passed": is_source_review,
"detail": str(work_item.get("kind") or "unknown"),
},
{
"name": "known_decision",
"passed": decision
in {"accepted", "rejected", "needs_more_evidence"},
"detail": decision or "unknown",
},
{
"name": "source_refs_present",
"passed": source_ref_total > 0,
"detail": str(source_ref_total),
},
{
"name": "provider_event_present",
"passed": bool(item.get("latest_provider_event_id")),
"detail": str(item.get("latest_provider_event_id") or "missing"),
},
{
"name": "review_record_only",
"passed": True,
"detail": "alert_operation_log_only",
},
]
if decision == "accepted":
checks.append(
{
"name": "target_incident_required",
"passed": bool(
target_incident_id and _INCIDENT_ID_RE.search(target_incident_id)
),
"detail": target_incident_id or "missing target_incident_id",
}
)
return checks
def _source_correlation_review_context(payload: dict[str, Any]) -> dict[str, Any]:
return {
"schema_version": _SOURCE_CORRELATION_DECISION_SCHEMA_VERSION,
"source": payload.get("source"),
"project_id": payload.get("project_id"),
"work_item_id": payload.get("work_item_id"),
"decision": payload.get("decision"),
"review_status": payload.get("review_status"),
"target_incident_id": payload.get("target_incident_id"),
"reviewer_id": payload.get("reviewer_id"),
"operator_note": payload.get("operator_note"),
"latest_provider_event_id": payload.get("latest_provider_event_id"),
"latest_stage": payload.get("latest_stage"),
"provider": payload.get("provider"),
"alertname": payload.get("alertname"),
"namespace": payload.get("namespace"),
"target_resource": payload.get("target_resource"),
"safety_level": payload.get("safety_level"),
"writes_incident_state": payload.get("writes_incident_state"),
"writes_source_event": payload.get("writes_source_event"),
"writes_auto_repair_result": payload.get("writes_auto_repair_result"),
"writes_ticket": payload.get("writes_ticket"),
"creates_external_ticket": payload.get("creates_external_ticket"),
"checks": payload.get("checks"),
"current_state_summary": payload.get("current_state_summary"),
"plan": payload.get("plan"),
"read_model_route": payload.get("read_model_route"),
"next_step": payload.get("next_step"),
"history": payload.get("history"),
}
def _source_correlation_review_description(context: dict[str, Any]) -> str:
return (
f"decision={context.get('decision')} "
f"work_item={context.get('work_item_id')} "
f"provider_event={context.get('latest_provider_event_id')} "
f"target_incident={context.get('target_incident_id') or '--'} "
f"writes_incident={context.get('writes_incident_state')} "
f"writes_source_event={context.get('writes_source_event')}"
)[:500]
def build_source_correlation_review_decision(
recurrence: dict[str, Any],
*,
work_item_id: str,
decision: SourceCorrelationReviewDecision,
target_incident_id: str | None = None,
reviewer_id: str = "operator_console",
operator_note: str | None = None,
) -> dict[str, Any]:
"""Build a record-only source-correlation review decision payload."""
item, work_item = _find_recurrence_work_item(recurrence, work_item_id)
safe_decision = str(decision)
target_id = str(target_incident_id or "").strip() or None
checks = _source_correlation_review_checks(
item,
work_item,
decision=safe_decision,
target_incident_id=target_id,
)
allowed = all(check["passed"] for check in checks)
outcome = _source_correlation_review_outcome(safe_decision)
writes_timeline = bool(target_id and safe_decision == "accepted")
payload = {
"schema_version": _SOURCE_CORRELATION_DECISION_SCHEMA_VERSION,
"source": "channel_event_dossier.recurrence",
"project_id": recurrence.get("project_id"),
"work_item_id": work_item.get("work_item_id"),
"incident_id": work_item.get("incident_id"),
"target_incident_id": target_id,
"decision": safe_decision,
"review_status": outcome["review_status"],
"reviewer_id": str(reviewer_id or "operator_console")[:100],
"operator_note": str(operator_note or "").strip()[:500] or None,
"allowed": allowed,
"executed": allowed,
"safety_level": "source_review_record_only",
"writes_incident_state": False,
"writes_source_event": False,
"writes_auto_repair_result": False,
"writes_ticket": False,
"creates_external_ticket": False,
"latest_provider_event_id": item.get("latest_provider_event_id"),
"latest_stage": item.get("latest_stage"),
"provider": item.get("provider"),
"alertname": item.get("alertname"),
"namespace": item.get("namespace"),
"target_resource": item.get("target_resource"),
"checks": checks,
"current_state_summary": _recurrence_current_state_summary(item, work_item),
"plan": {
"step": "record_source_correlation_review_decision",
"flywheel_node": "source_correlation_review",
"agent_id": "awooop_source_correlation_reviewer",
"required_scope": "record_history",
"writes": ["alert_operation_log", *(["timeline_events"] if writes_timeline else [])],
"target_action": outcome["next_step"],
"reason": outcome["reason"],
"target": _recurrence_work_item_target(item),
},
"read_model_route": {
"agent_id": "awooop_source_correlation_reviewer",
"tool_name": "channel_event_dossier.recurrence",
"required_scope": "record_history",
"is_shadow": False,
"flywheel_node": "source_correlation_review",
},
"next_step": outcome["next_step"] if allowed else "fix_preflight_checks",
}
if not allowed:
payload["executed"] = False
return payload
async def _record_source_correlation_review_decision_history(
payload: dict[str, Any],
) -> dict[str, Any]:
if not payload.get("allowed"):
return {"recorded": False, "reason": "preflight_failed"}
incident_id = str(payload.get("target_incident_id") or "") or None
history: dict[str, Any] = {
"recorded": False,
"alert_operation_id": None,
"timeline_event_id": None,
}
context = _source_correlation_review_context(payload)
try:
from src.repositories.alert_operation_log_repository import (
get_alert_operation_log_repository,
)
record = await get_alert_operation_log_repository().append(
"USER_ACTION",
incident_id=incident_id,
actor=_SOURCE_CORRELATION_REVIEW_ACTOR,
action_detail=f"source_correlation_review:{payload.get('decision')}"[:200],
success=True,
context=_json_safe(context),
)
if record is not None:
history["alert_operation_id"] = getattr(record, "id", None)
except Exception as exc:
logger.warning(
"awooop_source_correlation_review_alert_operation_failed",
work_item_id=payload.get("work_item_id"),
error=str(exc),
)
if incident_id and payload.get("decision") == "accepted":
try:
from src.services.approval_db import get_timeline_service
event = await get_timeline_service().add_event(
event_type="human",
status="success",
title="AwoooP source correlation review",
description=_source_correlation_review_description(context),
actor=_SOURCE_CORRELATION_REVIEW_ACTOR,
actor_role="source_correlation_review",
incident_id=incident_id,
)
if event:
history["timeline_event_id"] = event.get("id")
except Exception as exc:
logger.warning(
"awooop_source_correlation_review_timeline_failed",
incident_id=incident_id,
work_item_id=payload.get("work_item_id"),
error=str(exc),
)
else:
history["timeline_reason"] = "review_not_incident_timeline_scoped"
history["recorded"] = bool(
history.get("alert_operation_id") or history.get("timeline_event_id")
)
if not history["recorded"]:
history["reason"] = "history_sink_unavailable"
return history
def build_dossier_coverage(
rows: list[dict[str, Any]],
*,
@@ -1293,6 +1639,48 @@ async def _fetch_auto_repair_summaries_by_incident(
return summaries
async def _fetch_source_review_decisions_by_work_item(
db: Any,
*,
project_id: str,
limit: int,
) -> dict[str, dict[str, Any]]:
"""Fetch latest source-correlation review decisions from event-sourced audit."""
result = await db.execute(
text("""
SELECT DISTINCT ON (context->>'work_item_id')
id,
incident_id,
actor,
action_detail,
success,
context,
created_at
FROM alert_operation_log
WHERE actor = :actor
AND context->>'schema_version' = :schema_version
AND COALESCE(context->>'project_id', :project_id) = :project_id
ORDER BY context->>'work_item_id', created_at DESC
LIMIT :limit
"""),
{
"actor": _SOURCE_CORRELATION_REVIEW_ACTOR,
"schema_version": _SOURCE_CORRELATION_DECISION_SCHEMA_VERSION,
"project_id": project_id,
"limit": max(1, min(limit, _MAX_RECURRENCE_EVENTS)),
},
)
decisions: dict[str, dict[str, Any]] = {}
for row in result.mappings().all():
item = _normalize_source_review_decision(dict(row))
if not item:
continue
decisions[str(item["work_item_id"])] = item
return decisions
async def fetch_channel_event_dossier(
*,
project_id: str | None,
@@ -1479,12 +1867,18 @@ async def fetch_channel_event_dossier_recurrence(
db,
_collect_incident_ids_from_rows(rows),
)
source_review_decisions = await _fetch_source_review_decisions_by_work_item(
db,
project_id=effective_project_id,
limit=safe_limit,
)
return build_dossier_recurrence(
rows,
project_id=effective_project_id,
limit=safe_limit,
repair_summaries_by_incident=repair_summaries,
source_review_decisions_by_work_item=source_review_decisions,
)
@@ -1562,3 +1956,41 @@ async def fetch_recurrence_work_item_handoff(
elif payload.get("allowed"):
payload["handoff_status"] = "record_failed"
return payload
async def fetch_source_correlation_review_decision(
*,
project_id: str | None,
work_item_id: str,
decision: SourceCorrelationReviewDecision,
target_incident_id: str | None = None,
reviewer_id: str = "operator_console",
operator_note: str | None = None,
provider: str | None = None,
limit: int = _MAX_RECURRENCE_EVENTS,
) -> dict[str, Any]:
"""Fetch recurrence state and record an explicit source review decision."""
recurrence = await fetch_channel_event_dossier_recurrence(
project_id=project_id,
provider=provider,
limit=limit,
)
payload = build_source_correlation_review_decision(
recurrence,
work_item_id=work_item_id,
decision=decision,
target_incident_id=target_incident_id,
reviewer_id=reviewer_id,
operator_note=operator_note,
)
payload["history"] = await _record_source_correlation_review_decision_history(
payload
)
if payload["history"].get("recorded"):
payload["review_record_status"] = "recorded"
elif payload.get("allowed"):
payload["review_record_status"] = "record_failed"
else:
payload["review_record_status"] = "blocked"
return payload

View File

@@ -15,6 +15,7 @@ from src.services.channel_event_dossier_service import (
build_recurrence_work_item_dry_run,
build_recurrence_work_item_handoff,
build_recurrence_work_item_preview,
build_source_correlation_review_decision,
fetch_channel_event_dossier,
fetch_channel_event_dossier_coverage,
fetch_channel_event_dossier_recurrence,
@@ -268,6 +269,7 @@ def test_build_dossier_recurrence_groups_events_and_run_state() -> None:
assert recurrence["summary"]["automation_gap_group_total"] == 0
assert recurrence["summary"]["failed_repair_group_total"] == 1
assert recurrence["summary"]["source_correlation_review_group_total"] == 1
assert recurrence["summary"]["source_correlation_decision_recorded_group_total"] == 0
host_group = recurrence["items"][0]
assert host_group["recurrence_key"] == "fingerprint:fp-host-disk"
@@ -287,6 +289,7 @@ def test_build_dossier_recurrence_groups_events_and_run_state() -> None:
"schema_version": "awooop_recurrence_work_item_link_v1",
"work_item_id": "verification:INC-20260513-ABCD:repair-1",
"incident_id": "INC-20260513-ABCD",
"matched_incident_id": None,
"auto_repair_id": "repair-1",
"status": "open",
"kind": "verification",
@@ -304,6 +307,7 @@ def test_build_dossier_recurrence_groups_events_and_run_state() -> None:
"schema_version": "awooop_recurrence_work_item_link_v1",
"work_item_id": "source-evidence:sentry:received:issue-1",
"incident_id": None,
"matched_incident_id": None,
"auto_repair_id": None,
"status": "open",
"kind": "source_correlation_review",
@@ -355,6 +359,7 @@ def test_build_recurrence_work_item_preview_allows_source_correlation_review() -
item = recurrence["items"][0]
work_item_id = "source-evidence:signoz:upstream_canary:canary-1"
assert recurrence["summary"]["source_correlation_review_group_total"] == 1
assert recurrence["summary"]["source_correlation_decision_recorded_group_total"] == 0
assert recurrence["summary"]["open_work_item_group_total"] == 1
assert item["latest_stage"] == "upstream_canary"
assert item["repair_summary"]["status"] == "source_correlation_review"
@@ -386,6 +391,124 @@ def test_build_recurrence_work_item_preview_allows_source_correlation_review() -
)
def test_source_correlation_review_decision_records_accepted_audit_contract() -> None:
recurrence = build_dossier_recurrence(
[
{
"event_id": "event-1",
"project_id": "awoooi",
"channel_type": "internal",
"provider_event_id": "sentry:received:issue-1",
"content_hash": "a" * 64,
"content_preview": "Sentry issue",
"content_redacted": "Sentry issue",
"redaction_version": "audit_sink_v1",
"source_envelope": {
"provider": "sentry",
"stage": "received",
"source_refs": {
"sentry_issue_ids": ["issue-1"],
"alert_ids": ["sentry:received:issue-1"],
},
"log_correlation": {
"alertname": "Sentry Issue",
"severity": "error",
"namespace": "awoooi-prod",
"target_resource": "web",
"fingerprint": "fp-sentry-issue-1",
},
},
"is_duplicate": False,
"provider_ts": None,
"received_at": "2026-05-20T13:10:00",
"run_id": None,
"run_state": None,
"run_agent_id": None,
}
],
project_id="awoooi",
limit=20,
)
decision = build_source_correlation_review_decision(
recurrence,
work_item_id="source-evidence:sentry:received:issue-1",
decision="accepted",
target_incident_id="INC-20260520-ABC123",
reviewer_id="operator_console",
operator_note="matches current frontend incident",
)
assert decision["schema_version"] == "awooop_source_correlation_review_decision_v1"
assert decision["allowed"] is True
assert decision["executed"] is True
assert decision["decision"] == "accepted"
assert decision["review_status"] == "accepted"
assert decision["target_incident_id"] == "INC-20260520-ABC123"
assert decision["writes_incident_state"] is False
assert decision["writes_source_event"] is False
assert decision["writes_auto_repair_result"] is False
assert decision["writes_ticket"] is False
assert decision["creates_external_ticket"] is False
assert decision["plan"]["step"] == "record_source_correlation_review_decision"
assert decision["plan"]["writes"] == ["alert_operation_log", "timeline_events"]
assert decision["next_step"] == "verify_source_match_in_status_chain"
def test_build_dossier_recurrence_closes_source_review_after_decision() -> None:
decision = {
"schema_version": "awooop_source_correlation_review_decision_v1",
"review_id": "review-1",
"work_item_id": "source-evidence:sentry:received:issue-1",
"decision": "accepted",
"review_status": "accepted",
"target_incident_id": "INC-20260520-ABC123",
"reviewer_id": "operator_console",
"latest_provider_event_id": "sentry:received:issue-1",
"recorded_at": "2026-05-20T13:11:00",
}
recurrence = build_dossier_recurrence(
[
{
"event_id": "event-1",
"project_id": "awoooi",
"channel_type": "internal",
"provider_event_id": "sentry:received:issue-1",
"content_hash": "a" * 64,
"content_preview": "Sentry issue",
"content_redacted": "Sentry issue",
"redaction_version": "audit_sink_v1",
"source_envelope": {
"provider": "sentry",
"stage": "received",
"source_refs": {"sentry_issue_ids": ["issue-1"]},
},
"is_duplicate": False,
"provider_ts": None,
"received_at": "2026-05-20T13:10:00",
"run_id": None,
"run_state": None,
"run_agent_id": None,
}
],
project_id="awoooi",
limit=20,
source_review_decisions_by_work_item={
"source-evidence:sentry:received:issue-1": decision,
},
)
item = recurrence["items"][0]
assert recurrence["summary"]["open_work_item_group_total"] == 0
assert recurrence["summary"]["source_correlation_review_group_total"] == 0
assert recurrence["summary"]["source_correlation_decision_recorded_group_total"] == 1
assert item["source_correlation_review"] == decision
assert item["repair_summary"]["status"] == "source_correlation_accepted"
assert item["work_item"]["status"] == "closed"
assert item["work_item"]["matched_incident_id"] == "INC-20260520-ABC123"
assert item["work_item"]["next_step"] == "verify_source_match_in_status_chain"
def test_build_dossier_recurrence_opens_work_item_for_completed_run_without_repair() -> None:
recurrence = build_dossier_recurrence(
[
@@ -433,6 +556,7 @@ def test_build_dossier_recurrence_opens_work_item_for_completed_run_without_repa
"schema_version": "awooop_recurrence_work_item_link_v1",
"work_item_id": "incident:INC-20260517-F25B4A",
"incident_id": "INC-20260517-F25B4A",
"matched_incident_id": None,
"auto_repair_id": None,
"status": "open",
"kind": "automation_gap",
@@ -852,7 +976,7 @@ async def test_fetch_channel_event_dossier_uses_typed_run_filter(monkeypatch) ->
async def test_fetch_channel_event_dossier_coverage_uses_typed_provider_filter(
monkeypatch,
) -> None:
captured: dict[str, object] = {}
captured: list[dict[str, object]] = []
class FakeMappings:
def all(self) -> list[dict[str, object]]:
@@ -864,8 +988,7 @@ async def test_fetch_channel_event_dossier_coverage_uses_typed_provider_filter(
class FakeDb:
async def execute(self, statement, params): # noqa: ANN001
captured["sql"] = str(statement)
captured["params"] = params
captured.append({"sql": str(statement), "params": params})
return FakeResult()
class FakeContext:
@@ -889,8 +1012,9 @@ async def test_fetch_channel_event_dossier_coverage_uses_typed_provider_filter(
assert result["project_id"] == "awoooi"
assert result["limit"] == 200
assert "source_envelope->>'provider'" in str(captured["sql"])
assert captured["params"] == {
coverage_query = captured[0]
assert "source_envelope->>'provider'" in str(coverage_query["sql"])
assert coverage_query["params"] == {
"project_id": "awoooi",
"provider": "sentry",
"limit": 200,
@@ -901,7 +1025,7 @@ async def test_fetch_channel_event_dossier_coverage_uses_typed_provider_filter(
async def test_fetch_channel_event_dossier_recurrence_uses_joined_typed_filter(
monkeypatch,
) -> None:
captured: dict[str, object] = {}
captured: list[dict[str, object]] = []
class FakeMappings:
def all(self) -> list[dict[str, object]]:
@@ -913,8 +1037,7 @@ async def test_fetch_channel_event_dossier_recurrence_uses_joined_typed_filter(
class FakeDb:
async def execute(self, statement, params): # noqa: ANN001
captured["sql"] = str(statement)
captured["params"] = params
captured.append({"sql": str(statement), "params": params})
return FakeResult()
class FakeContext:
@@ -938,10 +1061,11 @@ async def test_fetch_channel_event_dossier_recurrence_uses_joined_typed_filter(
assert result["project_id"] == "awoooi"
assert result["limit"] == 300
assert "LEFT JOIN awooop_run_state r" in str(captured["sql"])
assert "e.source_envelope->>'provider'" in str(captured["sql"])
assert ":provider IS NULL" not in str(captured["sql"])
assert captured["params"] == {
recurrence_query = captured[0]
assert "LEFT JOIN awooop_run_state r" in str(recurrence_query["sql"])
assert "e.source_envelope->>'provider'" in str(recurrence_query["sql"])
assert ":provider IS NULL" not in str(recurrence_query["sql"])
assert recurrence_query["params"] == {
"project_id": "awoooi",
"provider": "alertmanager",
"limit": 300,