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