feat(ai): 新增 P2-408 中低風險白名單
Some checks failed
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m33s
CD Pipeline / build-and-deploy (push) Successful in 5m2s
CD Pipeline / post-deploy-checks (push) Successful in 1m32s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-18 19:00:45 +08:00
parent 63a75f7784
commit b36f4b97fe
13 changed files with 1959 additions and 5 deletions

View File

@@ -0,0 +1,142 @@
from __future__ import annotations
import copy
import json
from pathlib import Path
import pytest
from src.services.ai_agent_low_medium_risk_whitelist import (
load_latest_ai_agent_low_medium_risk_whitelist,
)
_REPO_ROOT = Path(__file__).resolve().parents[3]
_COMMITTED_SNAPSHOT = (
_REPO_ROOT
/ "docs"
/ "evaluations"
/ "ai_agent_low_medium_risk_whitelist_2026-06-18.json"
)
def test_load_latest_ai_agent_low_medium_risk_whitelist_reads_newest_file(tmp_path):
older = _snapshot(generated_at="2026-06-17T00:00:00+08:00")
newer = _snapshot(generated_at="2026-06-18T18:42:00+08:00")
(tmp_path / "ai_agent_low_medium_risk_whitelist_2026-06-17.json").write_text(
json.dumps(older),
encoding="utf-8",
)
(tmp_path / "ai_agent_low_medium_risk_whitelist_2026-06-18.json").write_text(
json.dumps(newer),
encoding="utf-8",
)
loaded = load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
assert loaded["generated_at"] == "2026-06-18T18:42:00+08:00"
assert loaded["program_status"]["current_task_id"] == "P2-408"
assert loaded["program_status"]["next_task_id"] == "P2-409"
assert loaded["program_status"]["read_only_mode"] is True
assert loaded["rollups"]["low_risk_candidate_count"] == 3
assert loaded["rollups"]["medium_risk_candidate_count"] == 3
assert loaded["rollups"]["auto_worker_run_count"] == 0
def test_ai_agent_low_medium_risk_whitelist_requires_read_only_mode(tmp_path):
snapshot = _snapshot()
snapshot["program_status"]["read_only_mode"] = False
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="program_status"):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def test_ai_agent_low_medium_risk_whitelist_blocks_live_execution(tmp_path):
snapshot = _snapshot()
snapshot["whitelist_candidates"][0]["live_execution_allowed"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="live_execution_allowed"):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def test_ai_agent_low_medium_risk_whitelist_blocks_gateway_queue_write(tmp_path):
snapshot = _snapshot()
snapshot["activation_boundaries"]["gateway_queue_write_enabled"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="activation boundaries"):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def test_ai_agent_low_medium_risk_whitelist_requires_dry_run_verifier(tmp_path):
snapshot = _snapshot()
snapshot["whitelist_candidates"][0]["dry_run_verifier_id"] = "missing_verifier"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="missing dry-run verifiers"):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def test_ai_agent_low_medium_risk_whitelist_requires_rollback_proof(tmp_path):
snapshot = _snapshot()
snapshot["whitelist_candidates"][0]["rollback_proof_id"] = "missing_rollback"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="missing rollback proofs"):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def test_ai_agent_low_medium_risk_whitelist_requires_audit_reason_template(tmp_path):
snapshot = _snapshot()
snapshot["whitelist_candidates"][0]["audit_reason_template_id"] = "missing_audit_template"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="missing audit reason templates"):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def test_ai_agent_low_medium_risk_whitelist_rejects_high_risk_candidate(tmp_path):
snapshot = _snapshot()
snapshot["whitelist_candidates"][0]["risk_tier"] = "high"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="must be low or medium"):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def test_ai_agent_low_medium_risk_whitelist_requires_rollup_consistency(tmp_path):
snapshot = _snapshot()
snapshot["rollups"]["whitelist_candidate_count"] = 99
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="rollup counts"):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def test_ai_agent_low_medium_risk_whitelist_rejects_private_terms(tmp_path):
snapshot = _snapshot()
snapshot["whitelist_candidates"][0]["allowed_no_write_outputs"] = ["請把 In app browser 內容放進前端"]
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="forbidden public terms"):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def test_ai_agent_low_medium_risk_whitelist_fails_when_missing(tmp_path):
with pytest.raises(FileNotFoundError):
load_latest_ai_agent_low_medium_risk_whitelist(tmp_path)
def _snapshot(*, generated_at: str = "2026-06-18T18:42:00+08:00") -> dict:
payload = json.loads(_COMMITTED_SNAPSHOT.read_text(encoding="utf-8"))
cloned = copy.deepcopy(payload)
cloned["generated_at"] = generated_at
return cloned
def _write_snapshot(tmp_path, payload: dict) -> None:
(tmp_path / "ai_agent_low_medium_risk_whitelist_2026-06-18.json").write_text(
json.dumps(payload),
encoding="utf-8",
)

View File

@@ -0,0 +1,58 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.agents import router
def test_ai_agent_low_medium_risk_whitelist_endpoint_returns_committed_snapshot():
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get("/api/v1/agents/agent-low-medium-risk-whitelist")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "ai_agent_low_medium_risk_whitelist_v1"
assert data["program_status"]["current_task_id"] == "P2-408"
assert data["program_status"]["next_task_id"] == "P2-409"
assert data["program_status"]["read_only_mode"] is True
assert (
data["program_status"]["runtime_authority"]
== "low_medium_risk_whitelist_no_live_execution_committed_snapshot"
)
assert data["rollups"]["source_readback_count"] == len(data["source_readbacks"]) == 5
assert data["rollups"]["whitelist_candidate_count"] == len(data["whitelist_candidates"]) == 6
assert data["rollups"]["low_risk_candidate_count"] == 3
assert data["rollups"]["medium_risk_candidate_count"] == 3
assert data["rollups"]["dry_run_verifier_count"] == len(data["dry_run_verifiers"]) == 5
assert data["rollups"]["rollback_proof_count"] == len(data["rollback_proofs"]) == 5
assert data["rollups"]["audit_reason_template_count"] == len(data["audit_reason_templates"]) == 6
assert data["rollups"]["high_risk_redirect_count"] == len(data["high_risk_redirects"]) == 3
assert data["rollups"]["owner_review_gate_count"] == len(data["owner_review_gates"]) == 3
assert data["rollups"]["live_execution_approval_required_count"] == 6
assert data["rollups"]["blocked_runtime_action_count"] == 27
assert data["rollups"]["auto_worker_run_count"] == 0
assert data["rollups"]["low_risk_execution_count"] == 0
assert data["rollups"]["medium_risk_execution_count"] == 0
assert data["rollups"]["gateway_queue_write_count"] == 0
assert data["rollups"]["telegram_send_count"] == 0
assert data["rollups"]["bot_api_call_count"] == 0
assert data["rollups"]["receipt_production_write_count"] == 0
assert data["rollups"]["production_write_count"] == 0
assert data["rollups"]["secret_read_count"] == 0
assert data["rollups"]["paid_api_call_count"] == 0
assert data["rollups"]["host_write_count"] == 0
assert data["rollups"]["kubectl_action_count"] == 0
assert data["rollups"]["destructive_operation_count"] == 0
assert data["telegram_policy"]["canonical_room"] == "AwoooI SRE 戰情室"
assert data["telegram_policy"]["canonical_room_env"] == "SRE_GROUP_CHAT_ID"
assert data["telegram_policy"]["telegram_send_allowed"] is False
assert data["telegram_policy"]["gateway_queue_write_allowed"] is False
assert data["telegram_policy"]["direct_bot_api_allowed"] is False
assert data["activation_boundaries"]["auto_worker_enabled"] is False
assert data["activation_boundaries"]["low_risk_live_execution_enabled"] is False
assert data["activation_boundaries"]["medium_risk_live_execution_enabled"] is False
assert data["activation_boundaries"]["openclaw_replacement_allowed"] is False