diff --git a/apps/api/src/api/v1/ai_governance.py b/apps/api/src/api/v1/ai_governance.py index 92a2be62f..158e734c8 100644 --- a/apps/api/src/api/v1/ai_governance.py +++ b/apps/api/src/api/v1/ai_governance.py @@ -65,6 +65,7 @@ from src.services.governance_query_service import ( query_km_review_draft_dedupe, query_km_stale_candidates, ) +from src.utils.timezone import now_taipei logger = structlog.get_logger(__name__) @@ -164,12 +165,29 @@ async def get_governance_queue( page=page, size=size, ) - return await query_governance_queue( - dispatch_status=dispatch_status, - event_types=event_type, - page=page, - size=size, - ) + try: + return await query_governance_queue( + dispatch_status=dispatch_status, + event_types=event_type, + page=page, + size=size, + ) + except Exception as exc: + logger.warning( + "governance_queue_readback_degraded", + dispatch_status=dispatch_status, + event_type=event_type, + page=page, + size=size, + error_type=type(exc).__name__, + ) + return GovernanceQueueResponse( + items=[], + total=0, + page=page, + size=size, + table_pending=True, + ) # ============================================================================= @@ -190,7 +208,21 @@ async def get_km_review_draft_dedupe( owner_action,不自動 archive、不自動 approve/publish KM。 """ logger.debug("km_review_draft_dedupe_request", limit=limit) - return await query_km_review_draft_dedupe(limit=limit) + try: + return await query_km_review_draft_dedupe(limit=limit) + except Exception as exc: + logger.warning( + "km_review_draft_dedupe_readback_degraded", + limit=limit, + error_type=type(exc).__name__, + ) + return KnowledgeReviewDraftDedupeResponse( + total_review_drafts=0, + event_group_total=0, + duplicate_draft_total=0, + groups=[], + generated_at=now_taipei(), + ) # ============================================================================= diff --git a/apps/api/tests/test_ai_governance_endpoints.py b/apps/api/tests/test_ai_governance_endpoints.py index e1b674953..2094b8ece 100644 --- a/apps/api/tests/test_ai_governance_endpoints.py +++ b/apps/api/tests/test_ai_governance_endpoints.py @@ -401,6 +401,25 @@ class TestQueueEndpoint: assert len(data["items"]) == 1 assert data["items"][0]["dispatch_status"] == "pending" + def test_readback_exception_returns_degraded_queue(self, client): + """Work Items 背景讀取失敗時應 fail-soft,不讓前台收到 500.""" + with patch( + "src.api.v1.ai_governance.query_governance_queue", + new=AsyncMock(side_effect=RuntimeError("temporary readback failure")), + ): + r = client.get( + "/api/v1/ai/governance/queue?dispatch_status=all" + "&event_type=knowledge_degradation&size=20" + ) + + assert r.status_code == 200 + data = r.json() + assert data["items"] == [] + assert data["total"] == 0 + assert data["page"] == 1 + assert data["size"] == 20 + assert data["table_pending"] is True + def test_invalid_dispatch_status_rejected(self, client): """非法 dispatch_status 應被拒絕(422).""" r = client.get("/api/v1/ai/governance/queue?dispatch_status=unknown") @@ -579,6 +598,23 @@ class TestKmReviewDraftDedupe: ) assert data["groups"][0]["archive_history"][0]["archived_count"] == 2 + def test_endpoint_readback_exception_returns_empty_plan(self, client): + """KM dedupe 背景讀取失敗時應 fail-soft,不讓 Work Items 收到 500.""" + with patch( + "src.api.v1.ai_governance.query_km_review_draft_dedupe", + new=AsyncMock(side_effect=RuntimeError("temporary dedupe read failure")), + ): + r = client.get("/api/v1/ai/governance/km-review-drafts/dedupe?limit=100") + + assert r.status_code == 200 + data = r.json() + assert data["schema_version"] == "km_review_draft_dedupe_v1" + assert data["total_review_drafts"] == 0 + assert data["event_group_total"] == 0 + assert data["duplicate_draft_total"] == 0 + assert data["groups"] == [] + assert data["generated_at"] + def test_governance_event_tag_extraction(self): assert _extract_governance_event_id_from_tags([ "agent:Hermes",