aps-agent/tests/golden/test_audit_appendonly_guard.py

92 lines
3.4 KiB
Python
Raw Normal View History

# ============================================================
# 审计 append-only 守护黄金测试(plan.md §3.6 / 矩阵 116 行剩余项)
# 覆盖:跨进程锁并发写不丢行、清理策略(保留窗口归档不删除证据)。
# ============================================================
from __future__ import annotations
import json
import threading
from pathlib import Path
from server.agent_core.audit_filelock import locked_append
from server.agent_core.audit_ledger import AnchorLedger
from server.agent_core.audit_mirror import AuditMirror
def _event(i: int) -> dict:
return {"id": i, "at": "2026-08-01T00:00:00+08:00", "actor": "tester",
"category": "TEST", "action": f"test.{i}", "target": {"type": "TEST"},
"power": "P0", "rationale": {}, "evidenceRefs": [], "beforeSnapshot": None,
"result": "SUCCESS", "prevHash": "GENESIS", "hash": "0" * 64}
def test_locked_append_concurrent_no_lost_lines(tmp_path: Path):
"""跨进程锁:4 线程×50 次并发 append 同一 JSONL,200 行全保留、无交错损坏。"""
target = tmp_path / "shared.jsonl"
def worker(n: int) -> None:
for i in range(50):
with locked_append(target) as fh:
fh.write(json.dumps({"n": n, "i": i}, sort_keys=True) + "\n")
threads = [threading.Thread(target=worker, args=(n,)) for n in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
lines = target.read_text(encoding="utf-8").splitlines()
assert len(lines) == 200, f"expected 200 lines, got {len(lines)}"
for ln in lines:
assert json.loads(ln), "行应为合法 JSON(无交错损坏)"
def test_ledger_archive_old_keeps_window_and_evidence(tmp_path: Path):
"""清理策略:超过保留窗口的旧锚定记录移入归档目录,主文件保留窗口;证据不丢失。"""
ledger = AnchorLedger("tenant-a", "world-1", ledger_dir=str(tmp_path))
evs = []
for i in range(5):
evs.append(_event(i + 1))
ledger.append(evs, reason="test")
assert ledger.status(evs)["records"] == 5
r = ledger.archive_old(keep_last=2)
assert r["archived"] == 3
assert r["kept"] == 2
assert len(ledger.read()) == 2
archive = Path(r["archive"])
assert archive.exists()
archived_lines = archive.read_text(encoding="utf-8").splitlines()
assert len(archived_lines) == 3
# 归档记录仍是合法 JSON(分级存储,非删除)
for ln in archived_lines:
rec = json.loads(ln)
assert rec.get("root") and rec.get("count")
def test_ledger_archive_under_window_is_noop(tmp_path: Path):
"""保留窗口内不归档(no-op),主文件不变。"""
ledger = AnchorLedger("tenant-a", "world-1", ledger_dir=str(tmp_path))
evs = []
for i in range(3):
evs.append(_event(i + 1))
ledger.append(evs, reason="test")
r = ledger.archive_old(keep_last=10)
assert r["archived"] == 0
assert r["kept"] == 3
assert len(ledger.read()) == 3
def test_mirror_append_uses_cross_process_lock(tmp_path: Path):
"""镜像 append 经 locked_append 落盘:并发下仍完整(回归保护)。"""
m = AuditMirror("tenant-a", "world-1", mirror_dir=str(tmp_path))
threads = [
threading.Thread(target=m.append_event, args=(_event(i + 1),))
for i in range(5)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert m.count() == 5