feat(awooop): surface telegram callback coverage
All checks were successful
CD Pipeline / tests (push) Successful in 1m21s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 3m59s
CD Pipeline / post-deploy-checks (push) Successful in 1m20s

This commit is contained in:
Your Name
2026-05-25 16:56:28 +08:00
parent b7ee1f47ff
commit 449c4ac807
7 changed files with 524 additions and 0 deletions

View File

@@ -102,11 +102,37 @@ class CallbackReplyItem(BaseModel):
run_detail_href: str | None = None
class CallbackReplyAuditSummary(BaseModel):
schema_version: str
project_id: str
outbound_total: int
outbound_source_envelope_total: int
outbound_source_refs_total: int
outbound_incident_ref_total: int
outbound_failed_total: int
callback_total: int
callback_sent_total: int
callback_fallback_total: int
callback_rescue_total: int
callback_failed_total: int
callback_detail_total: int
callback_history_total: int
callback_snapshot_captured_total: int
callback_snapshot_partial_total: int
callback_snapshot_missing_total: int
callback_incident_total: int
snapshot_status: str
next_action: str
latest_outbound_at: datetime | None = None
latest_callback_at: datetime | None = None
class ListCallbackRepliesResponse(BaseModel):
items: list[CallbackReplyItem]
total: int
page: int
per_page: int
summary: CallbackReplyAuditSummary | None = None
class CicdEventItem(BaseModel):

View File

@@ -110,6 +110,9 @@ _KM_STALE_COMPLETION_CALLBACK_SCHEMA_VERSION = (
"km_stale_owner_review_completion_callback_summary_v1"
)
_CALLBACK_EVIDENCE_CAPTURE_STATUS_SCHEMA_VERSION = "callback_evidence_capture_status_v1"
_CALLBACK_REPLY_AUDIT_SUMMARY_SCHEMA_VERSION = (
"telegram_callback_reply_audit_summary_v1"
)
# =============================================================================
# Tenants
@@ -398,6 +401,10 @@ async def list_callback_replies(
total = count_result.scalar_one()
rows_result = await db.execute(list_sql, params)
rows = list(rows_result.mappings().all())
summary = await _fetch_callback_reply_audit_summary(
db,
project_id=project_id or "awoooi",
)
items = [_callback_reply_event_item(row) for row in rows]
status_chain_cache: dict[tuple[str, str], dict[str, Any]] = {}
@@ -454,6 +461,165 @@ async def list_callback_replies(
"total": total,
"page": page,
"per_page": per_page,
"summary": summary,
}
async def _fetch_callback_reply_audit_summary(
db: Any,
*,
project_id: str,
) -> dict[str, Any]:
"""Summarize Telegram outbound mirror and callback evidence capture coverage."""
result = await db.execute(
text("""
SELECT
COUNT(*) AS outbound_total,
COUNT(*) FILTER (
WHERE source_envelope <> '{}'::jsonb
) AS outbound_source_envelope_total,
COUNT(*) FILTER (
WHERE source_envelope ? 'source_refs'
) AS outbound_source_refs_total,
COUNT(*) FILTER (
WHERE COALESCE(
source_envelope #> '{source_refs,incident_ids}',
'[]'::jsonb
) <> '[]'::jsonb
) AS outbound_incident_ref_total,
COUNT(*) FILTER (
WHERE send_status = 'failed'
) AS outbound_failed_total,
COUNT(*) FILTER (
WHERE source_envelope ? 'callback_reply'
) AS callback_total,
COUNT(*) FILTER (
WHERE source_envelope #>> '{callback_reply,status}'
= 'callback_reply_sent'
) AS callback_sent_total,
COUNT(*) FILTER (
WHERE source_envelope #>> '{callback_reply,status}'
= 'callback_reply_fallback_sent'
) AS callback_fallback_total,
COUNT(*) FILTER (
WHERE source_envelope #>> '{callback_reply,status}'
= 'callback_reply_rescue_sent'
) AS callback_rescue_total,
COUNT(*) FILTER (
WHERE source_envelope #>> '{callback_reply,status}'
= 'callback_reply_failed'
) AS callback_failed_total,
COUNT(*) FILTER (
WHERE LOWER(source_envelope #>> '{callback_reply,action}')
= 'detail'
) AS callback_detail_total,
COUNT(*) FILTER (
WHERE LOWER(source_envelope #>> '{callback_reply,action}')
= 'history'
) AS callback_history_total,
COUNT(*) FILTER (
WHERE source_envelope ? 'callback_reply'
AND source_envelope ? 'awooop_status_chain'
AND source_envelope ? 'km_stale_completion_summary'
) AS callback_snapshot_captured_total,
COUNT(*) FILTER (
WHERE source_envelope ? 'callback_reply'
AND (
source_envelope ? 'awooop_status_chain'
OR source_envelope ? 'km_stale_completion_summary'
)
AND NOT (
source_envelope ? 'awooop_status_chain'
AND source_envelope ? 'km_stale_completion_summary'
)
) AS callback_snapshot_partial_total,
COUNT(*) FILTER (
WHERE source_envelope ? 'callback_reply'
AND NOT (
source_envelope ? 'awooop_status_chain'
OR source_envelope ? 'km_stale_completion_summary'
)
) AS callback_snapshot_missing_total,
COUNT(DISTINCT source_envelope #>> '{callback_reply,incident_id}')
FILTER (
WHERE source_envelope ? 'callback_reply'
AND COALESCE(
source_envelope #>> '{callback_reply,incident_id}',
''
) <> ''
) AS callback_incident_total,
MAX(COALESCE(sent_at, queued_at)) AS latest_outbound_at,
MAX(COALESCE(sent_at, queued_at)) FILTER (
WHERE source_envelope ? 'callback_reply'
) AS latest_callback_at
FROM awooop_outbound_message
WHERE project_id = :project_id
AND channel_type = 'telegram'
"""),
{"project_id": project_id},
)
return _callback_reply_audit_summary_from_row(
result.mappings().one(),
project_id=project_id,
)
def _callback_reply_audit_summary_from_row(
row: Mapping[str, Any],
*,
project_id: str,
) -> dict[str, Any]:
"""Convert aggregate SQL row into the public callback evidence audit summary."""
outbound_total = _safe_int(row.get("outbound_total"))
callback_total = _safe_int(row.get("callback_total"))
captured = _safe_int(row.get("callback_snapshot_captured_total"))
partial = _safe_int(row.get("callback_snapshot_partial_total"))
missing = _safe_int(row.get("callback_snapshot_missing_total"))
outbound_incident_refs = _safe_int(row.get("outbound_incident_ref_total"))
if callback_total <= 0:
snapshot_status = "no_callback"
next_action = "press_telegram_detail_or_history"
elif missing > 0:
snapshot_status = "not_captured"
next_action = "press_telegram_detail_or_history_after_rollout"
elif partial > 0:
snapshot_status = "partial"
next_action = "press_telegram_detail_or_history_after_rollout"
elif outbound_total > 0 and outbound_incident_refs == 0:
snapshot_status = "captured"
next_action = "review_outbound_source_refs"
else:
snapshot_status = "captured"
next_action = "none"
return {
"schema_version": _CALLBACK_REPLY_AUDIT_SUMMARY_SCHEMA_VERSION,
"project_id": project_id,
"outbound_total": outbound_total,
"outbound_source_envelope_total": _safe_int(
row.get("outbound_source_envelope_total")
),
"outbound_source_refs_total": _safe_int(
row.get("outbound_source_refs_total")
),
"outbound_incident_ref_total": outbound_incident_refs,
"outbound_failed_total": _safe_int(row.get("outbound_failed_total")),
"callback_total": callback_total,
"callback_sent_total": _safe_int(row.get("callback_sent_total")),
"callback_fallback_total": _safe_int(row.get("callback_fallback_total")),
"callback_rescue_total": _safe_int(row.get("callback_rescue_total")),
"callback_failed_total": _safe_int(row.get("callback_failed_total")),
"callback_detail_total": _safe_int(row.get("callback_detail_total")),
"callback_history_total": _safe_int(row.get("callback_history_total")),
"callback_snapshot_captured_total": captured,
"callback_snapshot_partial_total": partial,
"callback_snapshot_missing_total": missing,
"callback_incident_total": _safe_int(row.get("callback_incident_total")),
"snapshot_status": snapshot_status,
"next_action": next_action,
"latest_outbound_at": row.get("latest_outbound_at"),
"latest_callback_at": row.get("latest_callback_at"),
}

View File

@@ -23,6 +23,7 @@ from src.services.platform_operator_service import (
_ai_route_policy_order,
_ai_route_repair_evidence_item,
_build_awooop_status_chain,
_callback_reply_audit_summary_from_row,
_callback_reply_event_item,
_callback_reply_summary_matches_status,
_cicd_duration_seconds,
@@ -658,6 +659,30 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
"total": 1,
"page": 1,
"per_page": 20,
"summary": {
"schema_version": "telegram_callback_reply_audit_summary_v1",
"project_id": "awoooi",
"outbound_total": 120,
"outbound_source_envelope_total": 118,
"outbound_source_refs_total": 100,
"outbound_incident_ref_total": 80,
"outbound_failed_total": 1,
"callback_total": 3,
"callback_sent_total": 1,
"callback_fallback_total": 1,
"callback_rescue_total": 0,
"callback_failed_total": 1,
"callback_detail_total": 2,
"callback_history_total": 1,
"callback_snapshot_captured_total": 1,
"callback_snapshot_partial_total": 1,
"callback_snapshot_missing_total": 1,
"callback_incident_total": 2,
"snapshot_status": "not_captured",
"next_action": "press_telegram_detail_or_history_after_rollout",
"latest_outbound_at": datetime(2026, 5, 18, 7, 40, 0),
"latest_callback_at": datetime(2026, 5, 18, 7, 31, 37),
},
})
dumped = response.model_dump(mode="json")
@@ -676,6 +701,42 @@ def test_list_callback_replies_response_preserves_callback_evidence() -> None:
] == "Hermes"
assert dumped["items"][0]["evidence_capture_status"]["status"] == "captured"
assert dumped["items"][0]["run_detail_href"].endswith("project_id=awoooi")
assert dumped["summary"]["outbound_total"] == 120
assert dumped["summary"]["callback_snapshot_missing_total"] == 1
assert dumped["summary"]["snapshot_status"] == "not_captured"
def test_callback_reply_audit_summary_marks_missing_snapshots() -> None:
summary = _callback_reply_audit_summary_from_row(
{
"outbound_total": 5256,
"outbound_source_envelope_total": 5256,
"outbound_source_refs_total": 5000,
"outbound_incident_ref_total": 3200,
"outbound_failed_total": 0,
"callback_total": 2,
"callback_sent_total": 2,
"callback_fallback_total": 0,
"callback_rescue_total": 0,
"callback_failed_total": 0,
"callback_detail_total": 0,
"callback_history_total": 2,
"callback_snapshot_captured_total": 0,
"callback_snapshot_partial_total": 0,
"callback_snapshot_missing_total": 2,
"callback_incident_total": 1,
"latest_outbound_at": datetime(2026, 5, 25, 8, 42, 22),
"latest_callback_at": datetime(2026, 5, 24, 14, 38, 4),
},
project_id="awoooi",
)
assert summary["schema_version"] == "telegram_callback_reply_audit_summary_v1"
assert summary["outbound_total"] == 5256
assert summary["callback_total"] == 2
assert summary["callback_snapshot_missing_total"] == 2
assert summary["snapshot_status"] == "not_captured"
assert summary["next_action"] == "press_telegram_detail_or_history_after_rollout"
@pytest.mark.asyncio