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
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:
142
apps/api/tests/test_ai_agent_low_medium_risk_whitelist.py
Normal file
142
apps/api/tests/test_ai_agent_low_medium_risk_whitelist.py
Normal 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",
|
||||
)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user