fix(awooop): fail soft drift fingerprint readback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 2m3s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 2m3s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
This commit is contained in:
@@ -15,6 +15,7 @@ leWOOOgo 積木化原則:
|
||||
|
||||
from typing import Literal
|
||||
|
||||
import structlog
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
@@ -32,6 +33,7 @@ from src.services.drift_analyzer import get_drift_analyzer
|
||||
from src.services.drift_detector import get_drift_detector
|
||||
from src.services.drift_fingerprint_state_service import (
|
||||
DriftFingerprintStateNotFoundError,
|
||||
build_drift_fingerprint_unavailable_state,
|
||||
get_drift_fingerprint_state_service,
|
||||
)
|
||||
from src.services.drift_interpreter import get_drift_interpreter
|
||||
@@ -39,6 +41,7 @@ from src.services.drift_remediator import get_drift_remediator
|
||||
from src.utils.timezone import now_taipei
|
||||
|
||||
router = APIRouter(prefix="/drift", tags=["drift"])
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
# 2026-04-09 Claude Sonnet 4.6: B4 drift_reports 持久化 — 改用 DB repository
|
||||
|
||||
@@ -157,6 +160,18 @@ async def get_drift_fingerprint_state(
|
||||
return await svc.get_state(report_id=report_id, namespace=namespace)
|
||||
except DriftFingerprintStateNotFoundError as exc:
|
||||
raise HTTPException(status_code=404, detail="drift_report_not_found") from exc
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"drift_fingerprint_state_readback_degraded",
|
||||
report_id=report_id,
|
||||
namespace=namespace,
|
||||
error_type=type(exc).__name__,
|
||||
)
|
||||
return build_drift_fingerprint_unavailable_state(
|
||||
namespace=namespace,
|
||||
report_id=report_id,
|
||||
reason="internal_readback_error",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/fingerprints/handoff", summary="記錄 Config Drift fingerprint 交接")
|
||||
|
||||
@@ -233,6 +233,67 @@ def build_drift_fingerprint_state(
|
||||
}
|
||||
|
||||
|
||||
def build_drift_fingerprint_unavailable_state(
|
||||
*,
|
||||
namespace: str | None = None,
|
||||
report_id: str | None = None,
|
||||
reason: str = "readback_exception",
|
||||
) -> dict[str, Any]:
|
||||
"""Build a no-write degraded payload when the read model cannot load."""
|
||||
|
||||
selected_namespace = namespace or DEFAULT_NAMESPACE
|
||||
return {
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"namespace": selected_namespace,
|
||||
"fingerprint": None,
|
||||
"latest_report_id": report_id,
|
||||
"latest_status": "readback_unavailable",
|
||||
"latest_scanned_at": None,
|
||||
"latest_created_at": None,
|
||||
"summary": "Drift fingerprint readback unavailable; AI controlled retry required",
|
||||
"high_count": 0,
|
||||
"medium_count": 0,
|
||||
"info_count": 0,
|
||||
"interpretation": None,
|
||||
"repeat_state": {
|
||||
"fingerprint": None,
|
||||
"occurrences_12h": 0,
|
||||
"matching_strategy": "readback_degraded_fallback_v1",
|
||||
},
|
||||
"occurrences_12h": 0,
|
||||
"matching_strategy": "readback_degraded_fallback_v1",
|
||||
"operator_stage": "readback_degraded_ai_controlled_repair",
|
||||
"fsm_state": "pending_human",
|
||||
"next_step": "manual_investigation_or_ansible_check_mode",
|
||||
"open_pr": None,
|
||||
"latest_handoff": None,
|
||||
"latest_remediation": None,
|
||||
"strict_fingerprint": None,
|
||||
"p0_escalation": {
|
||||
"suppresses_repeated_p0": False,
|
||||
"dedup_key_strategy": "readback_degraded_no_fingerprint",
|
||||
"dedup_window_hours": 24,
|
||||
},
|
||||
"read_model_route": {
|
||||
"agent_id": "openclaw",
|
||||
"tool_name": "drift_fingerprint_state",
|
||||
"required_scope": "read:drift read:gitea",
|
||||
"flywheel_node": (
|
||||
"drift_scanned>ai_analyzed>fingerprint_fsm>"
|
||||
"ai_controlled_readback_repair"
|
||||
),
|
||||
},
|
||||
"readback_status": "degraded",
|
||||
"readback_error": reason,
|
||||
"writes_incident_state": False,
|
||||
"writes_auto_repair_result": False,
|
||||
"writes_drift_status": False,
|
||||
"writes_ticket": False,
|
||||
"writes_remediation_record": False,
|
||||
"creates_external_ticket": False,
|
||||
}
|
||||
|
||||
|
||||
def _handoff_context(
|
||||
state: dict[str, Any],
|
||||
*,
|
||||
|
||||
31
apps/api/tests/test_drift_fingerprint_state_api.py
Normal file
31
apps/api/tests/test_drift_fingerprint_state_api.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
|
||||
from src.api.v1 import drift as drift_api
|
||||
|
||||
|
||||
class _BrokenDriftFingerprintStateService:
|
||||
async def get_state(self, *, report_id=None, namespace=None):
|
||||
raise RuntimeError("simulated drift readback failure")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_drift_fingerprint_state_endpoint_fails_soft_on_readback_error(monkeypatch) -> None:
|
||||
monkeypatch.setattr(
|
||||
drift_api,
|
||||
"get_drift_fingerprint_state_service",
|
||||
lambda: _BrokenDriftFingerprintStateService(),
|
||||
)
|
||||
|
||||
payload = await drift_api.get_drift_fingerprint_state(
|
||||
report_id=None,
|
||||
namespace="awoooi-prod",
|
||||
)
|
||||
|
||||
assert payload["schema_version"] == "drift_fingerprint_state_v1"
|
||||
assert payload["namespace"] == "awoooi-prod"
|
||||
assert payload["readback_status"] == "degraded"
|
||||
assert payload["readback_error"] == "internal_readback_error"
|
||||
assert payload["operator_stage"] == "readback_degraded_ai_controlled_repair"
|
||||
assert payload["writes_incident_state"] is False
|
||||
assert payload["writes_auto_repair_result"] is False
|
||||
assert payload["writes_drift_status"] is False
|
||||
@@ -1,6 +1,7 @@
|
||||
from src.models.drift import DriftItem, DriftLevel, DriftReport, DriftStatus
|
||||
from src.services.drift_fingerprint_state_service import (
|
||||
build_drift_fingerprint_state,
|
||||
build_drift_fingerprint_unavailable_state,
|
||||
)
|
||||
from src.services.drift_repeat_state import build_drift_repeat_state
|
||||
|
||||
@@ -128,3 +129,25 @@ def test_build_state_marks_remediation_executed_unverified() -> None:
|
||||
|
||||
assert state["fsm_state"] == "remediation_executed_unverified"
|
||||
assert state["next_step"] == "run_verification_scan_then_record_result"
|
||||
|
||||
|
||||
def test_build_unavailable_state_is_no_write_ai_controlled_retry() -> None:
|
||||
state = build_drift_fingerprint_unavailable_state(
|
||||
namespace="awoooi-prod",
|
||||
report_id="drift-readback-gap",
|
||||
reason="internal_readback_error",
|
||||
)
|
||||
|
||||
assert state["schema_version"] == "drift_fingerprint_state_v1"
|
||||
assert state["namespace"] == "awoooi-prod"
|
||||
assert state["latest_report_id"] == "drift-readback-gap"
|
||||
assert state["readback_status"] == "degraded"
|
||||
assert state["readback_error"] == "internal_readback_error"
|
||||
assert state["operator_stage"] == "readback_degraded_ai_controlled_repair"
|
||||
assert state["fsm_state"] == "pending_human"
|
||||
assert state["next_step"] == "manual_investigation_or_ansible_check_mode"
|
||||
assert state["writes_incident_state"] is False
|
||||
assert state["writes_auto_repair_result"] is False
|
||||
assert state["writes_drift_status"] is False
|
||||
assert state["writes_ticket"] is False
|
||||
assert state["creates_external_ticket"] is False
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
## 2026-07-03 — 07:55 Drift fingerprint readback degraded fallback
|
||||
|
||||
**完成內容**:
|
||||
- Runs AI Loop Agent 正式 deploy 後,production mobile smoke 擴查 `/zh-TW/awooop/work-items` 時發現 `GET /api/v1/drift/fingerprints/state?namespace=awoooi-prod&project_id=awoooi` 回 HTTP 500,導致相關頁 smoke 出現 console resource error。
|
||||
- `/api/v1/drift/fingerprints/state` 保留 `DriftFingerprintStateNotFoundError` → 404;其他 readback exception 改回 `drift_fingerprint_state_v1` degraded payload,標示 `readback_status=degraded`、`operator_stage=readback_degraded_ai_controlled_repair`、`next_step=manual_investigation_or_ansible_check_mode`,讓 Work Items / AI Agent 顯示 AI 受控補齊路徑,不再把 readback gap 變成 Internal Error。
|
||||
- degraded payload 固定 no-write:不寫 incident、auto-repair、drift status、ticket、remediation record,也不建立外部 ticket。
|
||||
|
||||
**已跑驗證**:
|
||||
- `DATABASE_URL=postgresql+asyncpg://test:test@localhost/test PYTHONPATH=apps/api python3.11 -m pytest apps/api/tests/test_drift_fingerprint_state_service.py apps/api/tests/test_drift_fingerprint_state_api.py -q -p no:cacheprovider`:`7 passed`。
|
||||
- `python3.11 -m py_compile apps/api/src/api/v1/drift.py apps/api/src/services/drift_fingerprint_state_service.py apps/api/tests/test_drift_fingerprint_state_service.py apps/api/tests/test_drift_fingerprint_state_api.py`:通過。
|
||||
- `python3.11 ops/runner/guard-gitea-runner-pressure.py --root .`:通過。
|
||||
- `git diff --check`:通過。
|
||||
- production Work Items desktop/mobile smoke:待本 commit deploy marker 後重跑。
|
||||
|
||||
**仍維持**:
|
||||
- 本輪只修 read-only API fallback;不讀 secret / token / `.env` / raw sessions / SQLite / auth;不使用 GitHub / gh;不觸發 drift scan、不寫 DB、不發 Telegram、不做 runtime apply;critical / secret / destructive / reboot / firewall / active scan / paid provider / force push 仍維持 break-glass。
|
||||
|
||||
## 2026-07-03 — 05:05 AwoooP Runs AI Loop Agent 處置鏈首屏
|
||||
|
||||
**完成內容**:
|
||||
|
||||
Reference in New Issue
Block a user