feat(governance): 顯示異地 escrow 準備度
This commit is contained in:
@@ -56,6 +56,9 @@ from src.services.backup_notification_policy import (
|
||||
from src.services.backup_restore_drill_approval_package_template import (
|
||||
load_latest_backup_restore_drill_approval_package_template,
|
||||
)
|
||||
from src.services.offsite_escrow_readiness_status import (
|
||||
load_latest_offsite_escrow_readiness_status,
|
||||
)
|
||||
from src.services.package_supply_chain_inventory import (
|
||||
load_latest_package_supply_chain_inventory,
|
||||
)
|
||||
@@ -583,6 +586,34 @@ async def get_backup_restore_drill_approval_package_template() -> dict[str, Any]
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/offsite-escrow-readiness-status",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得異地 / Escrow 準備度狀態",
|
||||
description=(
|
||||
"讀取最新已提交的異地備份、credential escrow 與 K8s resource offsite readiness 狀態;"
|
||||
"此端點只回傳 read-only status,不執行 backup、restore、offsite sync、"
|
||||
"不寫 credential marker、不讀 credential、不輸出 secret 明文、不改排程、不寫 workflow、"
|
||||
"不送 Telegram 測試通知、不做破壞性 prune、不改生產路由。"
|
||||
),
|
||||
)
|
||||
async def get_offsite_escrow_readiness_status() -> dict[str, Any]:
|
||||
"""Return the latest read-only offsite / escrow readiness status."""
|
||||
try:
|
||||
return await asyncio.to_thread(load_latest_offsite_escrow_readiness_status)
|
||||
except FileNotFoundError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
logger.error("offsite_escrow_readiness_status_invalid", error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="異地 / Escrow 準備度狀態快照無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/package-supply-chain-inventory",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
160
apps/api/src/services/offsite_escrow_readiness_status.py
Normal file
160
apps/api/src/services/offsite_escrow_readiness_status.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Offsite / escrow readiness status snapshot.
|
||||
|
||||
Loads the latest committed, read-only offsite / escrow readiness status. The
|
||||
status view never runs backups, restores, offsite sync, credential marker
|
||||
writes, credential reads, schedule changes, workflow writes, Telegram test
|
||||
notifications, destructive prune, or production routing changes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from src.services.snapshot_paths import default_evaluations_dir
|
||||
|
||||
_DEFAULT_EVALUATIONS_DIR = default_evaluations_dir(Path(__file__))
|
||||
_SNAPSHOT_PATTERN = "offsite_escrow_readiness_status_*.json"
|
||||
_SCHEMA_VERSION = "offsite_escrow_readiness_status_v1"
|
||||
|
||||
|
||||
def load_latest_offsite_escrow_readiness_status(
|
||||
evaluations_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Load the newest committed offsite / escrow readiness status snapshot."""
|
||||
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
|
||||
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
|
||||
if not candidates:
|
||||
raise FileNotFoundError(f"no offsite / escrow readiness status snapshots found in {directory}")
|
||||
|
||||
latest = candidates[-1]
|
||||
with latest.open(encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError(f"{latest}: expected JSON object")
|
||||
_require_schema(payload, _SCHEMA_VERSION, str(latest))
|
||||
_require_read_only_boundaries(payload, str(latest))
|
||||
_require_operation_boundaries(payload, str(latest))
|
||||
_require_rollup_consistency(payload, str(latest))
|
||||
_require_redacted_cards(payload, str(latest))
|
||||
return payload
|
||||
|
||||
|
||||
def _require_schema(payload: dict[str, Any], expected: str, label: str) -> None:
|
||||
actual = payload.get("schema_version")
|
||||
if actual != expected:
|
||||
raise ValueError(f"{label}: expected schema_version={expected}, got {actual!r}")
|
||||
|
||||
|
||||
def _require_read_only_boundaries(payload: dict[str, Any], label: str) -> None:
|
||||
program_status = payload.get("program_status") or {}
|
||||
if program_status.get("read_only_mode") is not True:
|
||||
raise ValueError(f"{label}: program_status.read_only_mode must be true")
|
||||
|
||||
boundaries = payload.get("approval_boundaries") or {}
|
||||
blocked_flags = {
|
||||
"sdk_installation_allowed",
|
||||
"paid_api_call_allowed",
|
||||
"shadow_or_canary_allowed",
|
||||
"production_routing_allowed",
|
||||
"destructive_operation_allowed",
|
||||
"restore_execution_allowed",
|
||||
"offsite_sync_execution_allowed",
|
||||
"credential_marker_write_allowed",
|
||||
}
|
||||
allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False)
|
||||
if allowed:
|
||||
raise ValueError(f"{label}: approval boundaries must remain false: {allowed}")
|
||||
|
||||
|
||||
def _require_operation_boundaries(payload: dict[str, Any], label: str) -> None:
|
||||
boundaries = payload.get("operation_boundaries") or {}
|
||||
if boundaries.get("read_only_status_allowed") is not True:
|
||||
raise ValueError(f"{label}: read_only_status_allowed must be true")
|
||||
|
||||
blocked_flags = {
|
||||
"backup_execution_allowed",
|
||||
"restore_execution_allowed",
|
||||
"offsite_sync_execution_allowed",
|
||||
"credential_marker_write_allowed",
|
||||
"credential_read_allowed",
|
||||
"secret_plaintext_allowed",
|
||||
"schedule_change_allowed",
|
||||
"workflow_write_allowed",
|
||||
"telegram_test_notification_allowed",
|
||||
"destructive_prune_allowed",
|
||||
"production_routing_allowed",
|
||||
}
|
||||
allowed = sorted(flag for flag in blocked_flags if boundaries.get(flag) is not False)
|
||||
if allowed:
|
||||
raise ValueError(f"{label}: operation boundaries must remain false: {allowed}")
|
||||
|
||||
|
||||
def _require_rollup_consistency(payload: dict[str, Any], label: str) -> None:
|
||||
cards = payload.get("readiness_cards") or []
|
||||
rollups = payload.get("rollups") or {}
|
||||
if rollups.get("total_cards") != len(cards):
|
||||
raise ValueError(f"{label}: rollups.total_cards must match readiness_cards")
|
||||
|
||||
verified_offsite = {
|
||||
card.get("card_id")
|
||||
for card in cards
|
||||
if card.get("kind") == "offsite_mirror" and card.get("readiness") == "verified"
|
||||
}
|
||||
if set(rollups.get("verified_offsite_card_ids") or []) != verified_offsite:
|
||||
raise ValueError(f"{label}: rollups.verified_offsite_card_ids must match verified offsite cards")
|
||||
|
||||
blocked_escrow = {
|
||||
card.get("card_id")
|
||||
for card in cards
|
||||
if card.get("kind") == "credential_escrow" and card.get("readiness") == "blocked"
|
||||
}
|
||||
if set(rollups.get("blocked_escrow_card_ids") or []) != blocked_escrow:
|
||||
raise ValueError(f"{label}: rollups.blocked_escrow_card_ids must match blocked escrow cards")
|
||||
|
||||
action_required = {
|
||||
card.get("card_id")
|
||||
for card in cards
|
||||
if card.get("readiness") == "action_required"
|
||||
}
|
||||
if set(rollups.get("action_required_card_ids") or []) != action_required:
|
||||
raise ValueError(f"{label}: rollups.action_required_card_ids must match action_required cards")
|
||||
|
||||
execution_blocked = {
|
||||
card.get("card_id")
|
||||
for card in cards
|
||||
if any(operation.endswith("_execution") or operation == "credential_marker_write" for operation in card.get("blocked_operations", []))
|
||||
}
|
||||
if set(rollups.get("execution_blocked_card_ids") or []) != execution_blocked:
|
||||
raise ValueError(f"{label}: rollups.execution_blocked_card_ids must match cards with blocked execution operations")
|
||||
|
||||
|
||||
def _require_redacted_cards(payload: dict[str, Any], label: str) -> None:
|
||||
cards = payload.get("readiness_cards") or []
|
||||
forbidden_exposure = {
|
||||
"plaintext",
|
||||
"secret_plaintext",
|
||||
"credential_plaintext",
|
||||
"token_visible",
|
||||
}
|
||||
exposed = sorted(
|
||||
card.get("card_id")
|
||||
for card in cards
|
||||
if card.get("credential_exposure_status") in forbidden_exposure
|
||||
)
|
||||
if exposed:
|
||||
raise ValueError(f"{label}: credential exposure must stay redacted: {exposed}")
|
||||
|
||||
contract = payload.get("operator_contract") or {}
|
||||
must_not_interpret_as = set(contract.get("must_not_interpret_as") or [])
|
||||
required_denials = {
|
||||
"復原批准",
|
||||
"異地同步批准",
|
||||
"credential marker 寫入批准",
|
||||
"完整 DR 綠燈",
|
||||
}
|
||||
if not required_denials.issubset(must_not_interpret_as):
|
||||
raise ValueError(f"{label}: operator_contract.must_not_interpret_as is missing required denials")
|
||||
@@ -18,12 +18,12 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho
|
||||
assert data["schema_version"] == "ai_agent_automation_backlog_v1"
|
||||
assert data["program_status"]["overall_completion_percent"] == 100
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["program_status"]["current_task_id"] == "P1-105"
|
||||
assert data["program_status"]["next_task_id"] == "P1-106"
|
||||
assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 20
|
||||
assert data["rollups"]["by_priority"]["P1"] == 18
|
||||
assert data["rollups"]["by_status"]["done"] == 13
|
||||
assert data["rollups"]["by_gate_status"]["read_only_allowed"] == 17
|
||||
assert data["program_status"]["current_task_id"] == "P1-106"
|
||||
assert data["program_status"]["next_task_id"] == "P1-305"
|
||||
assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 21
|
||||
assert data["rollups"]["by_priority"]["P1"] == 19
|
||||
assert data["rollups"]["by_status"]["done"] == 14
|
||||
assert data["rollups"]["by_gate_status"]["read_only_allowed"] == 18
|
||||
assert data["approval_boundaries"]["sdk_installation_allowed"] is False
|
||||
assert data["approval_boundaries"]["paid_api_call_allowed"] is False
|
||||
assert data["approval_boundaries"]["production_routing_allowed"] is False
|
||||
@@ -33,4 +33,5 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho
|
||||
assert any(item["item_id"] == "AUTO-P1-103" for item in data["backlog_items"])
|
||||
assert any(item["item_id"] == "AUTO-P1-104" for item in data["backlog_items"])
|
||||
assert any(item["item_id"] == "AUTO-P1-105" for item in data["backlog_items"])
|
||||
assert any(item["item_id"] == "AUTO-P1-106" for item in data["backlog_items"])
|
||||
assert any(item["item_id"] == "AUTO-P3-001" for item in data["backlog_items"])
|
||||
|
||||
@@ -18,8 +18,8 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
assert data["schema_version"] == "ai_agent_automation_inventory_snapshot_v1"
|
||||
assert data["program_status"]["overall_completion_percent"] == 100
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["program_status"]["current_task_id"] == "P1-105"
|
||||
assert data["program_status"]["next_task_id"] == "P1-106"
|
||||
assert data["program_status"]["current_task_id"] == "P1-106"
|
||||
assert data["program_status"]["next_task_id"] == "P1-305"
|
||||
assert data["approval_boundaries"]["sdk_installation_allowed"] is False
|
||||
assert data["approval_boundaries"]["paid_api_call_allowed"] is False
|
||||
assert data["approval_boundaries"]["production_routing_allowed"] is False
|
||||
@@ -30,6 +30,7 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
assert any(task["task_id"] == "P1-103" for task in data["tasks"])
|
||||
assert any(task["task_id"] == "P1-104" for task in data["tasks"])
|
||||
assert any(task["task_id"] == "P1-105" for task in data["tasks"])
|
||||
assert any(task["task_id"] == "P1-106" for task in data["tasks"])
|
||||
assert any(evidence["evidence_id"] == "dependency_risk_policy_api" for evidence in data["evidence"])
|
||||
assert any(evidence["evidence_id"] == "dependency_drift_check_plan_api" for evidence in data["evidence"])
|
||||
assert any(
|
||||
@@ -42,3 +43,4 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
evidence["evidence_id"] == "backup_restore_drill_approval_package_template_api"
|
||||
for evidence in data["evidence"]
|
||||
)
|
||||
assert any(evidence["evidence_id"] == "offsite_escrow_readiness_status_api" for evidence in data["evidence"])
|
||||
|
||||
213
apps/api/tests/test_offsite_escrow_readiness_status.py
Normal file
213
apps/api/tests/test_offsite_escrow_readiness_status.py
Normal file
@@ -0,0 +1,213 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.offsite_escrow_readiness_status import (
|
||||
load_latest_offsite_escrow_readiness_status,
|
||||
)
|
||||
|
||||
|
||||
def test_load_latest_offsite_escrow_readiness_status_reads_newest_file(tmp_path):
|
||||
older = _snapshot(generated_at="2026-06-04T00:00:00+08:00", completion=40)
|
||||
newer = _snapshot(generated_at="2026-06-05T00:00:00+08:00", completion=100)
|
||||
(tmp_path / "offsite_escrow_readiness_status_2026-06-04.json").write_text(
|
||||
json.dumps(older),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
|
||||
json.dumps(newer),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
loaded = load_latest_offsite_escrow_readiness_status(tmp_path)
|
||||
|
||||
assert loaded["generated_at"] == "2026-06-05T00:00:00+08:00"
|
||||
assert loaded["program_status"]["overall_completion_percent"] == 100
|
||||
assert loaded["rollups"]["total_cards"] == 3
|
||||
|
||||
|
||||
def test_offsite_escrow_readiness_status_requires_read_only_mode(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["program_status"]["read_only_mode"] = False
|
||||
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="read_only_mode"):
|
||||
load_latest_offsite_escrow_readiness_status(tmp_path)
|
||||
|
||||
|
||||
def test_offsite_escrow_readiness_status_requires_blocked_approval_boundaries(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["approval_boundaries"]["credential_marker_write_allowed"] = True
|
||||
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="approval boundaries"):
|
||||
load_latest_offsite_escrow_readiness_status(tmp_path)
|
||||
|
||||
|
||||
def test_offsite_escrow_readiness_status_requires_blocked_operation_boundaries(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["operation_boundaries"]["offsite_sync_execution_allowed"] = True
|
||||
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="operation boundaries"):
|
||||
load_latest_offsite_escrow_readiness_status(tmp_path)
|
||||
|
||||
|
||||
def test_offsite_escrow_readiness_status_requires_total_consistency(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["rollups"]["total_cards"] = 999
|
||||
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="total_cards"):
|
||||
load_latest_offsite_escrow_readiness_status(tmp_path)
|
||||
|
||||
|
||||
def test_offsite_escrow_readiness_status_requires_escrow_blocked_consistency(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["rollups"]["blocked_escrow_card_ids"] = []
|
||||
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="blocked_escrow_card_ids"):
|
||||
load_latest_offsite_escrow_readiness_status(tmp_path)
|
||||
|
||||
|
||||
def test_offsite_escrow_readiness_status_rejects_credential_plaintext(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["readiness_cards"][1]["credential_exposure_status"] = "credential_plaintext"
|
||||
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="credential exposure"):
|
||||
load_latest_offsite_escrow_readiness_status(tmp_path)
|
||||
|
||||
|
||||
def test_offsite_escrow_readiness_status_requires_operator_denials(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["operator_contract"]["must_not_interpret_as"] = ["復原批准"]
|
||||
(tmp_path / "offsite_escrow_readiness_status_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="must_not_interpret_as"):
|
||||
load_latest_offsite_escrow_readiness_status(tmp_path)
|
||||
|
||||
|
||||
def test_offsite_escrow_readiness_status_fails_when_missing(tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_latest_offsite_escrow_readiness_status(tmp_path)
|
||||
|
||||
|
||||
def _snapshot(
|
||||
*,
|
||||
generated_at: str = "2026-06-05T00:00:00+08:00",
|
||||
completion: int = 100,
|
||||
) -> dict:
|
||||
return {
|
||||
"schema_version": "offsite_escrow_readiness_status_v1",
|
||||
"generated_at": generated_at,
|
||||
"source_refs": ["docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json"],
|
||||
"program_status": {
|
||||
"overall_completion_percent": completion,
|
||||
"current_priority": "P1",
|
||||
"current_task_id": "P1-106",
|
||||
"next_task_id": "P1-305",
|
||||
"read_only_mode": True,
|
||||
},
|
||||
"rollups": {
|
||||
"total_cards": 3,
|
||||
"by_readiness": {"verified": 1, "action_required": 1, "blocked": 1},
|
||||
"by_kind": {
|
||||
"offsite_mirror": 1,
|
||||
"credential_escrow": 1,
|
||||
"k8s_resource_offsite": 1,
|
||||
},
|
||||
"verified_offsite_card_ids": ["offsite_rclone_full_sync"],
|
||||
"blocked_escrow_card_ids": ["credential_escrow_markers"],
|
||||
"action_required_card_ids": ["velero_k8s_resources"],
|
||||
"execution_blocked_card_ids": [
|
||||
"offsite_rclone_full_sync",
|
||||
"credential_escrow_markers",
|
||||
"velero_k8s_resources",
|
||||
],
|
||||
},
|
||||
"readiness_cards": [
|
||||
_card("offsite_rclone_full_sync", "offsite_mirror", "verified"),
|
||||
_card("credential_escrow_markers", "credential_escrow", "blocked"),
|
||||
_card("velero_k8s_resources", "k8s_resource_offsite", "action_required"),
|
||||
],
|
||||
"operator_contract": {
|
||||
"display_mode": "read_only_status",
|
||||
"success_notification_policy": "成功狀態不得即時通知洗版。",
|
||||
"failure_notification_policy": "失敗或阻擋維持 action-required。",
|
||||
"credential_display_policy": "只顯示 redacted metadata。",
|
||||
"must_not_interpret_as": [
|
||||
"復原批准",
|
||||
"異地同步批准",
|
||||
"credential marker 寫入批准",
|
||||
"完整 DR 綠燈",
|
||||
],
|
||||
},
|
||||
"operation_boundaries": {
|
||||
"read_only_status_allowed": True,
|
||||
"backup_execution_allowed": False,
|
||||
"restore_execution_allowed": False,
|
||||
"offsite_sync_execution_allowed": False,
|
||||
"credential_marker_write_allowed": False,
|
||||
"credential_read_allowed": False,
|
||||
"secret_plaintext_allowed": False,
|
||||
"schedule_change_allowed": False,
|
||||
"workflow_write_allowed": False,
|
||||
"telegram_test_notification_allowed": False,
|
||||
"destructive_prune_allowed": False,
|
||||
"production_routing_allowed": False,
|
||||
},
|
||||
"approval_boundaries": {
|
||||
"sdk_installation_allowed": False,
|
||||
"paid_api_call_allowed": False,
|
||||
"shadow_or_canary_allowed": False,
|
||||
"production_routing_allowed": False,
|
||||
"destructive_operation_allowed": False,
|
||||
"restore_execution_allowed": False,
|
||||
"offsite_sync_execution_allowed": False,
|
||||
"credential_marker_write_allowed": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _card(card_id: str, kind: str, readiness: str) -> dict:
|
||||
return {
|
||||
"card_id": card_id,
|
||||
"target_id": card_id,
|
||||
"display_name": card_id,
|
||||
"kind": kind,
|
||||
"readiness": readiness,
|
||||
"offsite_status": "verified" if readiness == "verified" else "not_applicable",
|
||||
"escrow_status": "missing_markers" if kind == "credential_escrow" else "not_applicable",
|
||||
"restore_drill_status": "approval_required",
|
||||
"credential_exposure_status": "redacted_only",
|
||||
"automation_gate_status": "read_only_allowed",
|
||||
"operator_summary": "只讀狀態。",
|
||||
"next_action": "維持只讀。",
|
||||
"evidence_refs": ["docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json"],
|
||||
"blocked_operations": ["offsite_sync_execution", "credential_marker_write"],
|
||||
}
|
||||
43
apps/api/tests/test_offsite_escrow_readiness_status_api.py
Normal file
43
apps/api/tests/test_offsite_escrow_readiness_status_api.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.v1.agents import router
|
||||
|
||||
|
||||
def test_offsite_escrow_readiness_status_endpoint_returns_committed_snapshot():
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/v1/agents/offsite-escrow-readiness-status")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "offsite_escrow_readiness_status_v1"
|
||||
assert data["program_status"]["overall_completion_percent"] == 100
|
||||
assert data["program_status"]["read_only_mode"] is True
|
||||
assert data["program_status"]["current_task_id"] == "P1-106"
|
||||
assert data["program_status"]["next_task_id"] == "P1-305"
|
||||
assert data["rollups"]["total_cards"] == len(data["readiness_cards"]) == 3
|
||||
assert data["rollups"]["verified_offsite_card_ids"] == ["offsite_rclone_full_sync"]
|
||||
assert data["rollups"]["blocked_escrow_card_ids"] == ["credential_escrow_markers"]
|
||||
assert data["rollups"]["action_required_card_ids"] == ["velero_k8s_resources"]
|
||||
assert data["operation_boundaries"]["read_only_status_allowed"] is True
|
||||
assert data["operation_boundaries"]["restore_execution_allowed"] is False
|
||||
assert data["operation_boundaries"]["offsite_sync_execution_allowed"] is False
|
||||
assert data["operation_boundaries"]["credential_marker_write_allowed"] is False
|
||||
assert data["operation_boundaries"]["credential_read_allowed"] is False
|
||||
assert data["operation_boundaries"]["secret_plaintext_allowed"] is False
|
||||
assert data["operation_boundaries"]["telegram_test_notification_allowed"] is False
|
||||
assert data["approval_boundaries"]["credential_marker_write_allowed"] is False
|
||||
assert any(
|
||||
card["card_id"] == "credential_escrow_markers" and card["readiness"] == "blocked"
|
||||
for card in data["readiness_cards"]
|
||||
)
|
||||
assert any(
|
||||
card["card_id"] == "velero_k8s_resources" and card["readiness"] == "action_required"
|
||||
for card in data["readiness_cards"]
|
||||
)
|
||||
assert "完整 DR 綠燈" in data["operator_contract"]["must_not_interpret_as"]
|
||||
Reference in New Issue
Block a user