From f3c72ec23779a37a1798c73410f3fb16f39aaf1e Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Jul 2026 09:51:15 +0800 Subject: [PATCH] fix(governance): fail soft owner review readbacks --- apps/api/src/api/v1/ai_governance.py | 51 +++++++++++++++++-- .../api/tests/test_ai_governance_endpoints.py | 50 ++++++++++++++++++ 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/apps/api/src/api/v1/ai_governance.py b/apps/api/src/api/v1/ai_governance.py index de5628192..3575a8b63 100644 --- a/apps/api/src/api/v1/ai_governance.py +++ b/apps/api/src/api/v1/ai_governance.py @@ -341,6 +341,24 @@ async def get_km_stale_owner_reviews( ) except KmStaleOwnerReviewError as exc: raise HTTPException(status_code=exc.status_code, detail=exc.detail) from exc + except Exception as exc: + logger.warning( + "km_stale_owner_reviews_readback_degraded", + project_id=project_id, + dispatch_status=dispatch_status, + limit=limit, + error_type=type(exc).__name__, + ) + return KnowledgeStaleOwnerReviewInboxResponse( + project_id=project_id, + dispatch_status=dispatch_status, + total=0, + returned=0, + writes_on_read=False, + manual_review_required=True, + items=[], + generated_at=now_taipei(), + ) # ============================================================================= @@ -366,10 +384,35 @@ async def get_km_stale_owner_review_burndown( project_id=project_id, limit=limit, ) - return await query_km_stale_owner_review_burndown( - project_id=project_id, - limit=limit, - ) + try: + return await query_km_stale_owner_review_burndown( + project_id=project_id, + limit=limit, + ) + except Exception as exc: + logger.warning( + "km_stale_owner_review_burndown_readback_degraded", + project_id=project_id, + limit=limit, + error_type=type(exc).__name__, + ) + return KnowledgeStaleOwnerReviewBurnDownResponse( + project_id=project_id, + burn_down_status="no_data", + current_snapshot=None, + entries_to_threshold=0, + pending_owner_reviews=0, + completed_owner_reviews=0, + completion_audit_total=0, + stale_ratio_recheck_total=0, + latest_stale_count_delta=None, + latest_stale_ratio_delta=None, + writes_on_read=False, + manual_review_required=True, + returned=0, + items=[], + 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 76e868dc0..0e3d00ab5 100644 --- a/apps/api/tests/test_ai_governance_endpoints.py +++ b/apps/api/tests/test_ai_governance_endpoints.py @@ -1028,6 +1028,29 @@ class TestKmReviewDraftDedupe: assert data["items"][0]["workflow_stage"] == "waiting_owner_review" assert data["items"][0]["batch_dispatch_id"] == "dispatch-batch-001" + def test_owner_review_inbox_readback_exception_returns_empty_inbox(self, client): + """owner review inbox 背景讀取失敗時應 fail-soft,不讓 Work Items 收到 500.""" + with patch( + "src.api.v1.ai_governance.query_km_stale_owner_review_inbox", + new=AsyncMock(side_effect=RuntimeError("temporary inbox failure")), + ): + r = client.get( + "/api/v1/ai/governance/km-stale-owner-reviews" + "?project_id=awoooi&dispatch_status=pending&limit=30" + ) + + assert r.status_code == 200 + data = r.json() + assert data["schema_version"] == "km_stale_owner_review_inbox_v1" + assert data["project_id"] == "awoooi" + assert data["dispatch_status"] == "pending" + assert data["total"] == 0 + assert data["returned"] == 0 + assert data["writes_on_read"] is False + assert data["manual_review_required"] is True + assert data["items"] == [] + assert data["generated_at"] + def test_owner_review_inbox_context_keeps_batch_and_priority_visible(self): record = KnowledgeEntryRecord( id="km-001", @@ -1156,6 +1179,33 @@ class TestKmReviewDraftDedupe: assert data["latest_stale_count_delta"] == -1 assert data["items"][0]["recheck_dispatch_id"] == "dispatch-recheck-001" + def test_owner_review_burndown_readback_exception_returns_no_data(self, client): + """burndown 背景讀取失敗時應 fail-soft,不讓 Work Items 收到 500.""" + with patch( + "src.api.v1.ai_governance.query_km_stale_owner_review_burndown", + new=AsyncMock(side_effect=RuntimeError("temporary burndown failure")), + ): + r = client.get( + "/api/v1/ai/governance/km-stale-owner-review-burndown" + "?project_id=awoooi&limit=20" + ) + + assert r.status_code == 200 + data = r.json() + assert data["schema_version"] == "km_stale_owner_review_burndown_v1" + assert data["project_id"] == "awoooi" + assert data["burn_down_status"] == "no_data" + assert data["entries_to_threshold"] == 0 + assert data["pending_owner_reviews"] == 0 + assert data["completed_owner_reviews"] == 0 + assert data["completion_audit_total"] == 0 + assert data["stale_ratio_recheck_total"] == 0 + assert data["writes_on_read"] is False + assert data["manual_review_required"] is True + assert data["returned"] == 0 + assert data["items"] == [] + assert data["generated_at"] + def test_owner_review_burndown_items_compute_chronological_delta(self): older_snapshot = { "stale_count": 119,