feat(api): 新增復原演練批准包模板
All checks were successful
CD Pipeline / tests (push) Successful in 1m24s
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / build-and-deploy (push) Successful in 3m59s
CD Pipeline / post-deploy-checks (push) Successful in 2m23s

This commit is contained in:
Your Name
2026-06-05 01:20:50 +08:00
parent 2857da80b4
commit a367227d3a
12 changed files with 1609 additions and 34 deletions

View File

@@ -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"])

View File

@@ -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"]
)

View File

@@ -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"],
}

View File

@@ -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"]
)