feat(agents): 新增 AI action audit ledger
Some checks failed
Code Review / ai-code-review (push) Successful in 12s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / tests (push) Successful in 1m54s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-19 00:50:43 +08:00
parent f390cddb4d
commit e13f716c00
9 changed files with 1306 additions and 136 deletions

View File

@@ -0,0 +1,133 @@
from __future__ import annotations
import copy
import json
from pathlib import Path
import pytest
from src.services.ai_agent_action_audit_ledger import (
load_latest_ai_agent_action_audit_ledger,
)
_REPO_ROOT = Path(__file__).resolve().parents[3]
_COMMITTED_SNAPSHOT = (
_REPO_ROOT
/ "docs"
/ "evaluations"
/ "ai_agent_action_audit_ledger_2026-06-19.json"
)
def test_load_latest_ai_agent_action_audit_ledger_reads_newest_file(tmp_path):
older = _snapshot(generated_at="2026-06-18T23:00:00+08:00")
newer = _snapshot(generated_at="2026-06-19T04:10:00+08:00")
(tmp_path / "ai_agent_action_audit_ledger_2026-06-18.json").write_text(
json.dumps(older),
encoding="utf-8",
)
(tmp_path / "ai_agent_action_audit_ledger_2026-06-19.json").write_text(
json.dumps(newer),
encoding="utf-8",
)
loaded = load_latest_ai_agent_action_audit_ledger(tmp_path)
assert loaded["generated_at"] == "2026-06-19T04:10:00+08:00"
assert loaded["program_status"]["current_task_id"] == "P2-410"
assert loaded["program_status"]["next_task_id"] == "P2-411"
assert loaded["rollups"]["audit_event_template_count"] == 8
assert loaded["rollups"]["verifier_receipt_gate_count"] == 5
assert loaded["rollups"]["telegram_send_count"] == 0
def test_ai_agent_action_audit_ledger_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_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_blocks_audit_db_write(tmp_path):
snapshot = _snapshot()
snapshot["audit_truth"]["audit_db_write_enabled"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="audit_db_write_enabled"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_blocks_event_gateway_write(tmp_path):
snapshot = _snapshot()
snapshot["audit_event_templates"][0]["gateway_queue_write_allowed"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="gateway_queue_write_allowed"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_requires_all_risk_tiers(tmp_path):
snapshot = _snapshot()
for event in snapshot["audit_event_templates"]:
event["risk_tier"] = "medium"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="low, medium, high, and critical"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_rejects_missing_source_ref(tmp_path):
snapshot = _snapshot()
snapshot["audit_event_templates"][0]["source_readback_ids"] = ["missing_source"]
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="missing source readbacks"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_blocks_verifier_live_mode(tmp_path):
snapshot = _snapshot()
snapshot["verifier_receipt_gates"][0]["live_verifier_allowed"] = True
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="live_verifier_allowed"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_requires_rollup_consistency(tmp_path):
snapshot = _snapshot()
snapshot["rollups"]["audit_event_template_count"] = 99
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="rollup counts"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_rejects_private_terms(tmp_path):
snapshot = _snapshot()
snapshot["audit_event_templates"][0]["display_name"] = "請把 " + "In app " + "browser" + " 內容放進前端"
_write_snapshot(tmp_path, snapshot)
with pytest.raises(ValueError, match="forbidden public terms"):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def test_ai_agent_action_audit_ledger_fails_when_missing(tmp_path):
with pytest.raises(FileNotFoundError):
load_latest_ai_agent_action_audit_ledger(tmp_path)
def _snapshot(*, generated_at: str = "2026-06-19T04:10: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_action_audit_ledger_2026-06-19.json").write_text(
json.dumps(payload),
encoding="utf-8",
)

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.agents import router
def test_ai_agent_action_audit_ledger_endpoint_returns_committed_snapshot():
app = FastAPI()
app.include_router(router, prefix="/api/v1")
client = TestClient(app)
response = client.get("/api/v1/agents/agent-action-audit-ledger")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "ai_agent_action_audit_ledger_v1"
assert data["program_status"]["current_task_id"] == "P2-410"
assert data["program_status"]["next_task_id"] == "P2-411"
assert data["program_status"]["read_only_mode"] is True
assert (
data["program_status"]["runtime_authority"]
== "agent_action_audit_ledger_no_live_write_committed_snapshot"
)
assert data["rollups"]["source_readback_count"] == len(data["source_readbacks"]) == 7
assert data["rollups"]["audit_event_template_count"] == len(data["audit_event_templates"]) == 8
assert data["rollups"]["verifier_receipt_gate_count"] == len(data["verifier_receipt_gates"]) == 5
assert data["rollups"]["low_medium_event_count"] == 4
assert data["rollups"]["high_risk_event_count"] == 3
assert data["rollups"]["critical_event_count"] == 1
assert data["rollups"]["report_gap_event_count"] == 2
assert data["rollups"]["telegram_event_count"] == 2
assert data["rollups"]["blocked_runtime_action_count"] == 23
assert data["rollups"]["audit_db_write_count"] == 0
assert data["rollups"]["timeline_write_count"] == 0
assert data["rollups"]["km_write_count"] == 0
assert data["rollups"]["playbook_trust_write_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"]["production_write_count"] == 0
assert data["activation_boundaries"]["committed_snapshot_read_allowed"] is True
assert data["activation_boundaries"]["audit_db_write_enabled"] is False
assert data["activation_boundaries"]["telegram_send_enabled"] is False
assert data["display_redaction_contract"]["redaction_required"] is True
assert data["display_redaction_contract"]["secret_value_display_allowed"] is False