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

This commit is contained in:
Your Name
2026-07-03 07:53:18 +08:00
parent b338a7ab54
commit 468eba526e
5 changed files with 147 additions and 0 deletions

View File

@@ -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 交接")

View File

@@ -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],
*,

View 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

View File

@@ -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

View File

@@ -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 applycritical / secret / destructive / reboot / firewall / active scan / paid provider / force push 仍維持 break-glass。
## 2026-07-03 — 05:05 AwoooP Runs AI Loop Agent 處置鏈首屏
**完成內容**