fix(awooop): fail soft readback list endpoints
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m27s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-03 08:21:36 +08:00
parent a7ffac9e1c
commit 0e93566f11
5 changed files with 120 additions and 12 deletions

View File

@@ -108,16 +108,29 @@ async def get_governance_events(
page=page,
size=size,
)
return await query_governance_events(
event_ids=event_id,
event_types=event_type,
from_dt=from_,
to_dt=to,
status=status,
severity=severity,
page=page,
size=size,
)
try:
return await query_governance_events(
event_ids=event_id,
event_types=event_type,
from_dt=from_,
to_dt=to,
status=status,
severity=severity,
page=page,
size=size,
)
except Exception as exc:
logger.warning(
"governance_events_readback_degraded",
event_ids=event_id,
event_types=event_type,
status=status,
severity=severity,
page=page,
size=size,
error_type=type(exc).__name__,
)
return GovernanceEventsResponse(items=[], total=0, page=page, size=size)
# =============================================================================

View File

@@ -12,12 +12,15 @@ from decimal import Decimal
from typing import Any
from uuid import UUID
import structlog
from fastapi import APIRouter
from pydantic import BaseModel
from src.services.platform_operator_service import list_tenants as list_tenants_svc
from src.utils.timezone import now_taipei
router = APIRouter()
logger = structlog.get_logger(__name__)
class TenantItem(BaseModel):
@@ -114,6 +117,48 @@ class ListTenantsResponse(BaseModel):
asset_inventory: TenantAssetInventory
def _tenant_readback_fallback() -> dict[str, Any]:
created_at = now_taipei()
return {
"tenants": [
{
"project_id": "awoooi",
"display_name": "AWOOOI",
"migration_mode": "readback_degraded",
"budget_limit_usd": None,
"is_active": True,
"created_at": created_at,
}
],
"total": 1,
"asset_inventory": {
"schema_version": "tenant_asset_inventory_v1",
"mode": "readback_degraded_ai_controlled_repair",
"evidence_refs": [],
"summary": {
"tenant_table_count": 0,
"product_surface_count": 0,
"public_route_count": 0,
"public_gateway_snapshot_route_count": 0,
"source_candidate_repo_count": 0,
"source_in_scope_repo_count": 0,
"source_primary_ready_count": 0,
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
},
"products": [],
"public_routes": [],
"source_repos": [],
"boundaries": [
"tenant readback degraded; AI controlled retry required",
"fallback is read-only and does not modify tenants, routes, repos, workflows, or runtime",
],
},
}
@router.get(
"/tenants",
response_model=ListTenantsResponse,
@@ -124,4 +169,11 @@ class ListTenantsResponse(BaseModel):
),
)
async def list_tenants() -> dict[str, Any]:
return await list_tenants_svc()
try:
return await list_tenants_svc()
except Exception as exc:
logger.warning(
"platform_tenants_readback_degraded",
error_type=type(exc).__name__,
)
return _tenant_readback_fallback()

View File

@@ -233,6 +233,19 @@ class TestEventsEndpoint:
assert captured["event_ids"] == ["evt-001", "evt-002"]
assert captured["status"] == "unresolved"
def test_events_readback_failure_returns_empty_page(self, client):
"""readback exception 應降級成 empty page避免治理頁看到 500."""
async def mock_query(**_kwargs):
raise RuntimeError("simulated governance readback failure")
with patch("src.api.v1.ai_governance.query_governance_events", new=mock_query):
r = client.get("/api/v1/ai/governance/events?page=2&size=10")
assert r.status_code == 200
data = r.json()
assert data == {"items": [], "total": 0, "page": 2, "size": 10}
def test_invalid_severity_rejected(self, client):
"""非法 severity 值應被拒絕422."""
r = client.get("/api/v1/ai/governance/events?severity=bad_value")

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import json
import re
from src.api.v1.platform.tenants import ListTenantsResponse
from src.api.v1.platform.tenants import ListTenantsResponse, _tenant_readback_fallback
from src.services.platform_operator_service import build_tenant_asset_inventory
@@ -145,3 +145,16 @@ def test_tenant_response_model_keeps_asset_inventory_contract() -> None:
assert "nexu-io" not in response_payload
assert all(marker not in response_payload for marker in FORBIDDEN_PUBLIC_MARKERS)
assert response.asset_inventory.source_repos[0].source_namespace_redacted is True
def test_tenant_readback_fallback_keeps_response_model_no_write_contract() -> None:
response = ListTenantsResponse.model_validate(_tenant_readback_fallback())
assert response.tenants[0].project_id == "awoooi"
assert response.tenants[0].migration_mode == "readback_degraded"
assert response.asset_inventory.mode == "readback_degraded_ai_controlled_repair"
assert response.asset_inventory.summary.tenant_table_count == 0
assert response.asset_inventory.products == []
assert response.asset_inventory.public_routes == []
assert response.asset_inventory.source_repos == []
assert "does not modify" in " ".join(response.asset_inventory.boundaries)