feat(api): 新增復原演練批准包模板
This commit is contained in:
@@ -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