feat(api): 新增復原演練批准包模板
This commit is contained in:
@@ -53,6 +53,9 @@ from src.services.backup_dr_readiness_matrix import (
|
||||
from src.services.backup_notification_policy import (
|
||||
load_latest_backup_notification_policy,
|
||||
)
|
||||
from src.services.backup_restore_drill_approval_package_template import (
|
||||
load_latest_backup_restore_drill_approval_package_template,
|
||||
)
|
||||
from src.services.package_supply_chain_inventory import (
|
||||
load_latest_package_supply_chain_inventory,
|
||||
)
|
||||
@@ -551,6 +554,35 @@ async def get_backup_notification_policy() -> dict[str, Any]:
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/backup-restore-drill-approval-package-template",
|
||||
response_model=dict[str, Any],
|
||||
summary="取得 Backup / DR 復原演練批准包模板",
|
||||
description=(
|
||||
"讀取最新已提交的 Backup / DR restore drill、credential escrow review、"
|
||||
"K8s resource recovery、observability recovery 與 route reconstruction 批准包模板;"
|
||||
"此端點只回傳 read-only template,不執行 backup、restore、offsite sync、"
|
||||
"不寫 credential marker、不改排程、不寫 workflow、不送 Telegram 測試通知、"
|
||||
"不輸出 secret 明文、不做破壞性 prune、不呼叫付費 API、不建立 shadow/canary、不改生產路由。"
|
||||
),
|
||||
)
|
||||
async def get_backup_restore_drill_approval_package_template() -> dict[str, Any]:
|
||||
"""Return the latest read-only Backup / DR restore drill approval package template."""
|
||||
try:
|
||||
return await asyncio.to_thread(load_latest_backup_restore_drill_approval_package_template)
|
||||
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("backup_restore_drill_approval_package_template_invalid", error=str(exc))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Backup / DR 復原演練批准包模板快照無效",
|
||||
) from exc
|
||||
|
||||
|
||||
@router.get(
|
||||
"/package-supply-chain-inventory",
|
||||
response_model=dict[str, Any],
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Backup / DR restore drill approval package template snapshot.
|
||||
|
||||
Loads the latest committed, read-only approval package template for restore
|
||||
drills, credential escrow review, K8s resource recovery, observability
|
||||
recovery, and route reconstruction. The template never runs backups, restores,
|
||||
offsite sync, credential marker writes, schedule changes, workflow writes,
|
||||
Telegram test notifications, destructive prune, secret plaintext export, paid
|
||||
API calls, shadow/canary, 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 = "backup_restore_drill_approval_package_template_*.json"
|
||||
_SCHEMA_VERSION = "backup_restore_drill_approval_package_template_v1"
|
||||
|
||||
|
||||
def load_latest_backup_restore_drill_approval_package_template(
|
||||
evaluations_dir: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Load the newest committed Backup / DR restore drill approval package template."""
|
||||
directory = evaluations_dir or _DEFAULT_EVALUATIONS_DIR
|
||||
candidates = sorted(directory.glob(_SNAPSHOT_PATTERN))
|
||||
if not candidates:
|
||||
raise FileNotFoundError(
|
||||
f"no Backup / DR restore drill approval package template 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))
|
||||
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_template_allowed") is not True:
|
||||
raise ValueError(f"{label}: read_only_template_allowed must be true")
|
||||
|
||||
blocked_flags = {
|
||||
"backup_execution_allowed",
|
||||
"restore_execution_allowed",
|
||||
"offsite_sync_execution_allowed",
|
||||
"credential_marker_write_allowed",
|
||||
"schedule_change_allowed",
|
||||
"workflow_write_allowed",
|
||||
"telegram_test_notification_allowed",
|
||||
"destructive_prune_allowed",
|
||||
"secret_plaintext_allowed",
|
||||
"production_routing_allowed",
|
||||
"sdk_installation_allowed",
|
||||
"paid_api_call_allowed",
|
||||
"shadow_or_canary_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:
|
||||
templates = payload.get("package_templates") or []
|
||||
rollups = payload.get("rollups") or {}
|
||||
if rollups.get("total_templates") != len(templates):
|
||||
raise ValueError(f"{label}: rollups.total_templates must match package_templates")
|
||||
|
||||
ready_ids = {
|
||||
template.get("template_id")
|
||||
for template in templates
|
||||
if template.get("status") == "template_ready"
|
||||
}
|
||||
if set(rollups.get("template_ready_ids") or []) != ready_ids:
|
||||
raise ValueError(f"{label}: rollups.template_ready_ids must match template_ready templates")
|
||||
|
||||
hitl_ids = {
|
||||
template.get("template_id")
|
||||
for template in templates
|
||||
if "HITL approval" in (template.get("manual_approvals") or [])
|
||||
}
|
||||
if set(rollups.get("hitl_required_template_ids") or []) != hitl_ids:
|
||||
raise ValueError(f"{label}: rollups.hitl_required_template_ids must match HITL templates")
|
||||
|
||||
blocked_target_ids = _target_ids_by_readiness(templates, "blocked")
|
||||
if set(rollups.get("blocked_source_target_ids") or []) != blocked_target_ids:
|
||||
raise ValueError(
|
||||
f"{label}: rollups.blocked_source_target_ids must match blocked source targets"
|
||||
)
|
||||
|
||||
action_required_target_ids = _target_ids_by_readiness(templates, "action_required")
|
||||
if set(rollups.get("action_required_source_target_ids") or []) != action_required_target_ids:
|
||||
raise ValueError(
|
||||
f"{label}: rollups.action_required_source_target_ids must match action_required source targets"
|
||||
)
|
||||
|
||||
if (payload.get("decision_gate_contract") or {}).get("hitl_required") is not True:
|
||||
raise ValueError(f"{label}: decision_gate_contract.hitl_required must be true")
|
||||
|
||||
|
||||
def _target_ids_by_readiness(templates: list[dict[str, Any]], readiness: str) -> set[str]:
|
||||
return {
|
||||
target.get("target_id")
|
||||
for template in templates
|
||||
for target in template.get("source_target_statuses", [])
|
||||
if target.get("readiness") == readiness
|
||||
}
|
||||
@@ -18,11 +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-104"
|
||||
assert data["program_status"]["next_task_id"] == "P1-105"
|
||||
assert data["rollups"]["total_items"] == len(data["backlog_items"]) == 19
|
||||
assert data["rollups"]["by_priority"]["P1"] == 17
|
||||
assert data["rollups"]["by_status"]["done"] == 12
|
||||
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["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
|
||||
@@ -31,4 +32,5 @@ def test_ai_agent_automation_backlog_snapshot_endpoint_returns_committed_snapsho
|
||||
assert any(item["item_id"] == "AUTO-P1-206" for item in data["backlog_items"])
|
||||
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-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-104"
|
||||
assert data["program_status"]["next_task_id"] == "P1-105"
|
||||
assert data["program_status"]["current_task_id"] == "P1-105"
|
||||
assert data["program_status"]["next_task_id"] == "P1-106"
|
||||
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
|
||||
@@ -29,6 +29,7 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
assert any(task["task_id"] == "P1-206" for task in data["tasks"])
|
||||
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(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(
|
||||
@@ -37,3 +38,7 @@ def test_ai_agent_automation_inventory_snapshot_endpoint_returns_committed_snaps
|
||||
)
|
||||
assert any(evidence["evidence_id"] == "backup_notification_policy_api" for evidence in data["evidence"])
|
||||
assert any(evidence["evidence_id"] == "backup_dr_evidence_ui" for evidence in data["evidence"])
|
||||
assert any(
|
||||
evidence["evidence_id"] == "backup_restore_drill_approval_package_template_api"
|
||||
for evidence in data["evidence"]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.backup_restore_drill_approval_package_template import (
|
||||
load_latest_backup_restore_drill_approval_package_template,
|
||||
)
|
||||
|
||||
|
||||
def test_load_latest_backup_restore_drill_approval_package_template_reads_newest_file(tmp_path):
|
||||
older = _snapshot(generated_at="2026-06-04T00:00:00+08:00", completion=75)
|
||||
newer = _snapshot(generated_at="2026-06-05T00:00:00+08:00", completion=100)
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-04.json").write_text(
|
||||
json.dumps(older),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text(
|
||||
json.dumps(newer),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
loaded = load_latest_backup_restore_drill_approval_package_template(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_templates"] == 2
|
||||
assert loaded["operation_boundaries"]["restore_execution_allowed"] is False
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_requires_read_only_mode(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["program_status"]["read_only_mode"] = False
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="read_only_mode"):
|
||||
load_latest_backup_restore_drill_approval_package_template(tmp_path)
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_requires_blocked_operations(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["operation_boundaries"]["restore_execution_allowed"] = True
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="operation boundaries"):
|
||||
load_latest_backup_restore_drill_approval_package_template(tmp_path)
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_requires_blocked_approval_boundaries(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["approval_boundaries"]["credential_marker_write_allowed"] = True
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="approval boundaries"):
|
||||
load_latest_backup_restore_drill_approval_package_template(tmp_path)
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_requires_total_consistency(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["rollups"]["total_templates"] = 999
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="total_templates"):
|
||||
load_latest_backup_restore_drill_approval_package_template(tmp_path)
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_requires_ready_id_consistency(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["rollups"]["template_ready_ids"] = []
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="template_ready_ids"):
|
||||
load_latest_backup_restore_drill_approval_package_template(tmp_path)
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_requires_hitl_consistency(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["rollups"]["hitl_required_template_ids"] = []
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="hitl_required_template_ids"):
|
||||
load_latest_backup_restore_drill_approval_package_template(tmp_path)
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_requires_source_target_rollups(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["rollups"]["blocked_source_target_ids"] = []
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="blocked_source_target_ids"):
|
||||
load_latest_backup_restore_drill_approval_package_template(tmp_path)
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_requires_hitl_gate(tmp_path):
|
||||
snapshot = _snapshot()
|
||||
snapshot["decision_gate_contract"]["hitl_required"] = False
|
||||
(tmp_path / "backup_restore_drill_approval_package_template_2026-06-05.json").write_text(
|
||||
json.dumps(snapshot),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="hitl_required"):
|
||||
load_latest_backup_restore_drill_approval_package_template(tmp_path)
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_fails_when_missing(tmp_path):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
load_latest_backup_restore_drill_approval_package_template(tmp_path)
|
||||
|
||||
|
||||
def _snapshot(
|
||||
*,
|
||||
generated_at: str = "2026-06-05T00:00:00+08:00",
|
||||
completion: int = 100,
|
||||
) -> dict:
|
||||
return {
|
||||
"schema_version": "backup_restore_drill_approval_package_template_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-105",
|
||||
"next_task_id": "P1-106",
|
||||
"read_only_mode": True,
|
||||
},
|
||||
"rollups": {
|
||||
"total_templates": 2,
|
||||
"by_domain": {"database_restore": 1, "k8s_resource_restore": 1},
|
||||
"template_ready_ids": [
|
||||
"database_restore_drill_approval_package",
|
||||
"velero_k8s_restore_drill_package",
|
||||
],
|
||||
"hitl_required_template_ids": [
|
||||
"database_restore_drill_approval_package",
|
||||
"velero_k8s_restore_drill_package",
|
||||
],
|
||||
"blocked_source_target_ids": ["configs_capture"],
|
||||
"action_required_source_target_ids": ["velero_k8s_resources"],
|
||||
},
|
||||
"approval_fields": [
|
||||
{
|
||||
"field_id": "manual_approval",
|
||||
"required": True,
|
||||
"description": "approval",
|
||||
}
|
||||
],
|
||||
"package_templates": [
|
||||
_template(
|
||||
"database_restore_drill_approval_package",
|
||||
"database_restore",
|
||||
[{"target_id": "configs_capture", "readiness": "blocked"}],
|
||||
),
|
||||
_template(
|
||||
"velero_k8s_restore_drill_package",
|
||||
"k8s_resource_restore",
|
||||
[{"target_id": "velero_k8s_resources", "readiness": "action_required"}],
|
||||
),
|
||||
],
|
||||
"decision_gate_contract": {
|
||||
"openclaw_role": "arbitrate",
|
||||
"hermes_role": "summarize",
|
||||
"nemotron_role": "offline review",
|
||||
"hitl_required": True,
|
||||
"expires_after": "7 days",
|
||||
"invalidated_by": ["snapshot change"],
|
||||
},
|
||||
"operation_boundaries": {
|
||||
"read_only_template_allowed": True,
|
||||
"backup_execution_allowed": False,
|
||||
"restore_execution_allowed": False,
|
||||
"offsite_sync_execution_allowed": False,
|
||||
"credential_marker_write_allowed": False,
|
||||
"schedule_change_allowed": False,
|
||||
"workflow_write_allowed": False,
|
||||
"telegram_test_notification_allowed": False,
|
||||
"destructive_prune_allowed": False,
|
||||
"secret_plaintext_allowed": False,
|
||||
"production_routing_allowed": False,
|
||||
"sdk_installation_allowed": False,
|
||||
"paid_api_call_allowed": False,
|
||||
"shadow_or_canary_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 _template(template_id: str, domain: str, source_target_statuses: list[dict]) -> dict:
|
||||
return {
|
||||
"template_id": template_id,
|
||||
"domain": domain,
|
||||
"status": "template_ready",
|
||||
"owner_agent": "openclaw",
|
||||
"purpose": "approval package",
|
||||
"source_target_statuses": source_target_statuses,
|
||||
"required_evidence": ["docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json"],
|
||||
"required_decisions": ["approve or reject"],
|
||||
"required_prechecks": ["precheck"],
|
||||
"required_tests": ["schema validation"],
|
||||
"rollback_requirements": ["rollback plan"],
|
||||
"abort_criteria": ["abort"],
|
||||
"manual_approvals": ["OpenClaw arbitration", "HITL approval"],
|
||||
"prohibited_without_approval": ["restore execution"],
|
||||
"evidence_refs": ["docs/evaluations/backup_dr_readiness_matrix_2026-06-04.json"],
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from src.api.v1.agents import router
|
||||
|
||||
|
||||
def test_backup_restore_drill_approval_package_template_endpoint_returns_committed_snapshot():
|
||||
app = FastAPI()
|
||||
app.include_router(router, prefix="/api/v1")
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/v1/agents/backup-restore-drill-approval-package-template")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["schema_version"] == "backup_restore_drill_approval_package_template_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_templates"] == len(data["package_templates"]) == 6
|
||||
assert len(data["rollups"]["hitl_required_template_ids"]) == 6
|
||||
assert data["rollups"]["blocked_source_target_ids"] == [
|
||||
"configs_capture",
|
||||
"credential_escrow_markers",
|
||||
]
|
||||
assert data["rollups"]["action_required_source_target_ids"] == [
|
||||
"signoz",
|
||||
"velero_k8s_resources",
|
||||
]
|
||||
assert data["operation_boundaries"]["read_only_template_allowed"] is True
|
||||
assert data["operation_boundaries"]["backup_execution_allowed"] is False
|
||||
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"]["workflow_write_allowed"] is False
|
||||
assert data["operation_boundaries"]["telegram_test_notification_allowed"] is False
|
||||
assert data["operation_boundaries"]["secret_plaintext_allowed"] is False
|
||||
assert data["operation_boundaries"]["production_routing_allowed"] is False
|
||||
assert data["decision_gate_contract"]["hitl_required"] is True
|
||||
assert any(
|
||||
template["template_id"] == "credential_escrow_review_package"
|
||||
for template in data["package_templates"]
|
||||
)
|
||||
assert any(
|
||||
template["template_id"] == "velero_k8s_restore_drill_package"
|
||||
for template in data["package_templates"]
|
||||
)
|
||||
Reference in New Issue
Block a user