aps-agent/tests/golden/test_trace_break_visual.py

242 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ============================================================
# round-39 黄金测试:trace 断链检测 + 可视化暴露(矩阵 114 剩余项)
# 覆盖:
# 1) verify_audit_trace:构造断链(哈希篡改/计数不一致/条目非法/格式非法)→ 检出 expected/actual
# 2) 端到端:schedule.run 真实审计 → 校验通过;篡改 traceChainHash → 检出
# 3) API:/api/gov/audit traceAlerts(断链列表/空数组)+ /api/gov/trace/verify 汇总
# ============================================================
from __future__ import annotations
import asyncio
import hashlib
import json
import pytest
from fastapi.testclient import TestClient
from server.agent_core.evidence import EvidenceItem, trace_chain, verify_audit_trace
from server.aps_domain.workflow import handle_intent
from server.contracts import IntentResult
from server.state.seed import seed_world
class _MemStore:
def __init__(self, data):
self.data = data
def next_id(self, kind: str) -> int:
key = "_c_" + kind
self.data[key] = self.data.get(key, 0) + 1
return self.data[key]
def save(self):
pass
def _mk_items():
return [
EvidenceItem(kind="schedule-version", ref="1", version="1"),
EvidenceItem(kind="run", ref="run-1", runId="1", engine="RULE",
meta={"strategy": "COMPREHENSIVE", "solveStatus": "OPTIMAL", "gap": 0.0}),
EvidenceItem(kind="algorithm", ref="RULE", version="RULE", inputsHash="abc123"),
]
def _trace_rationale(items, **overrides):
chain = trace_chain(items)
rat = {
"traceChainHash": chain["chainHash"],
"traceCount": chain["count"],
"traceSummary": [{"kind": it["kind"], "ref": it["ref"]} for it in chain["items"]],
"traceItems": chain["items"],
}
rat.update(overrides)
return rat
def _algo_event(eid: int, rationale: dict, **overrides) -> dict:
ev = {"id": eid, "at": "2026-08-02T10:00:00+08:00", "ts": "2026-08-02T10:00:00+08:00",
"actor": "tester", "category": "ALGO_RUN", "action": "schedule.run",
"target": {"type": "SCHEDULE_VERSION", "id": eid}, "power": "P1",
"rationale": rationale, "prevHash": "GENESIS", "hash": "0" * 64}
ev.update(overrides)
return ev
# ---------------- 1) verify_audit_trace 纯函数 ----------------
def test_verify_audit_trace_ok_on_self_describing_record():
ev = _algo_event(1, _trace_rationale(_mk_items()))
result = verify_audit_trace([ev])
assert result["ok"] is True
assert result["checked"] == 1
assert result["broken"] == []
def test_verify_audit_trace_detects_hash_tamper():
items = _mk_items()
ev = _algo_event(1, _trace_rationale(items, traceChainHash="f" * 64))
result = verify_audit_trace([ev])
assert result["ok"] is False
(b,) = result["broken"]
assert b["eventId"] == 1
assert b["expected"] == "f" * 64
assert b["actual"] == trace_chain(items)["chainHash"] # 重算出的真实链哈希
assert b["reason"] == "chain-hash-mismatch"
def test_verify_audit_trace_detects_count_mismatch():
ev = _algo_event(2, _trace_rationale(_mk_items(), traceCount=2))
result = verify_audit_trace([ev])
assert result["ok"] is False
b = result["broken"][0]
assert b["eventId"] == 2
assert b["reason"] == "count-mismatch"
assert b["expected"] == "2"
assert b["actual"] == "3"
def test_verify_audit_trace_detects_bad_summary_entry():
rat = _trace_rationale(_mk_items())
rat["traceSummary"][1] = {"kind": "hack", "ref": "x"}
ev = _algo_event(3, rat)
result = verify_audit_trace([ev])
assert result["ok"] is False
assert result["broken"][0]["reason"] == "bad-summary-entry"
def test_verify_audit_trace_detects_bad_hash_format():
ev = _algo_event(4, _trace_rationale(_mk_items(), traceChainHash="zzz"))
result = verify_audit_trace([ev])
assert result["ok"] is False
assert result["broken"][0]["reason"] == "bad-hash-format"
def test_verify_audit_trace_legacy_record_no_false_positive():
"""旧记录(无 traceItems,只有 traceChainHash/count/summary)仅结构校验,不误报。"""
rat = _trace_rationale(_mk_items())
rat.pop("traceItems")
ev = _algo_event(5, rat)
result = verify_audit_trace([ev])
assert result["ok"] is True
assert result["broken"] == []
# ---------------- 2) 端到端:schedule.run 真实审计 ----------------
def test_schedule_run_audit_verifies_then_tamper_detected():
store = _MemStore(seed_world())
async def _run():
await handle_intent(store, "t", IntentResult(intent="schedule.run",
params={"strategy": "COMPREHENSIVE", "engine": "RULE"},
confidence=1.0, source="RULE_FAST"))
asyncio.run(_run())
runs = [e for e in store.data["auditEvents"]
if e["category"] == "ALGO_RUN" and e["action"] == "schedule.run"]
assert runs
ev = runs[-1]
assert isinstance(ev["rationale"].get("traceItems"), list), "traceItems 应落盘以便精确重算"
result = verify_audit_trace(store.data["auditEvents"])
assert result["ok"] is True
tampered = json.loads(json.dumps(ev))
tampered["rationale"]["traceChainHash"] = "0" * 64
result2 = verify_audit_trace([tampered])
assert result2["ok"] is False
(b,) = result2["broken"]
assert b["eventId"] == ev["id"]
assert b["expected"] == "0" * 64
assert b["actual"] == ev["rationale"]["traceChainHash"]
assert b["reason"] == "chain-hash-mismatch"
# ---------------- 3) API:/api/gov/audit traceAlerts + /api/gov/trace/verify ----------------
class _ApiStore:
def __init__(self, events):
self.tenant_uuid = "platform"
self.world_key = "default"
self.data = {"auditEvents": events}
def next_id(self, _kind: str) -> int:
return 999
def save(self) -> None:
pass
def _signed_events(events):
"""把测试事件签名成哈希链一致(避免链告警干扰 traceAlerts 断言)。"""
prev = "GENESIS"
for ev in events:
body = {k: v for k, v in ev.items() if k != "hash"}
body["prevHash"] = prev
ev["prevHash"] = prev
ev["hash"] = hashlib.sha256(
(prev + json.dumps(body, ensure_ascii=False, sort_keys=True)).encode("utf-8")
).hexdigest()
prev = ev["hash"]
return events
@pytest.fixture
def gov_client(monkeypatch, tmp_path):
import os
import server.gateway.app as gateway_module
from tests.auth_provider import install_test_auth
install_test_auth(monkeypatch, "tenant-trace-visual")
os.environ["APS_AUDIT_LEDGER_DIR"] = str(tmp_path / "ledger")
def _make(events):
store = _ApiStore(_signed_events(events))
monkeypatch.setattr(gateway_module, "get_store", lambda: store)
c = TestClient(gateway_module.create_app())
login = c.post("/api/auth/login", json={"username": "planner", "password": "test"})
assert login.status_code == 200, login.text
return c
yield _make
os.environ.pop("APS_AUDIT_LEDGER_DIR", None)
def test_gov_audit_trace_alerts_empty_when_clean(gov_client):
c = gov_client([_algo_event(1, _trace_rationale(_mk_items()))])
r = c.get("/api/gov/audit")
assert r.status_code == 200, r.text
body = r.json()
assert body["traceAlerts"] == [], "无断链时 traceAlerts 应为空数组"
assert body["traceOk"] is True
assert body["traceChecked"] == 1
r2 = c.get("/api/gov/trace/verify")
assert r2.status_code == 200, r2.text
v = r2.json()
assert v["ok"] is True and v["checked"] == 1 and v["broken"] == []
assert v["summary"]["broken"] == 0
def test_gov_audit_trace_alerts_nonempty_when_broken(gov_client):
items = _mk_items()
ev = _algo_event(1, _trace_rationale(items, traceChainHash="f" * 64))
c = gov_client([ev])
r = c.get("/api/gov/audit")
assert r.status_code == 200, r.text
body = r.json()
assert body["traceOk"] is False
assert len(body["traceAlerts"]) == 1
b = body["traceAlerts"][0]
assert b["eventId"] == 1
assert b["expected"] == "f" * 64
assert b["actual"] == trace_chain(items)["chainHash"]
assert b["reason"] == "chain-hash-mismatch"
r2 = c.get("/api/gov/trace/verify")
assert r2.status_code == 200, r2.text
v = r2.json()
assert v["ok"] is False
assert v["summary"]["broken"] == 1