#!/usr/bin/env python3 """内容级快照泄露审计的纯函数测试。""" from __future__ import annotations import json import pathlib import sys import unittest PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3] SCRIPT_DIR = PROJECT_ROOT / ".claude" / "skills" / "freeze-context" / "scripts" sys.path.insert(0, str(SCRIPT_DIR)) from audit_leakage import audit_snapshot # noqa: E402 def target_facts(): """提供只供审计侧使用的目标章事实,不进入规划器上下文。""" return { "targetChapter": 489, "forbiddenFacts": [ { "id": "betrayal-489", "firstChapter": 489, "text": "阿铎反水击毙加特朗", } ], } class LeakageAuditTest(unittest.TestCase): def test_safe_snapshot_is_ready(self): result = audit_snapshot( {"events": [{"chapter": 488, "fact": "环星防御仍在"}]}, target_facts(), as_of=488, target=489, ) self.assertTrue(result["ok"]) self.assertEqual(result["status"], "ready") self.assertEqual(result["findingCount"], 0) def test_future_nested_record_blocks_snapshot(self): result = audit_snapshot( {"events": [{"chapter": 489, "fact": "未来事件"}]}, target_facts(), as_of=488, target=489, ) self.assertFalse(result["ok"]) self.assertEqual(result["status"], "invalid_snapshot") self.assertEqual(result["findings"][0]["reason"], "future_or_crosses_as_of") def test_mislabelled_target_fact_blocks_snapshot_without_echoing_text(self): result = audit_snapshot( {"events": [{"chapter": 488, "fact": "阿铎反水击毙加特朗"}]}, target_facts(), as_of=488, target=489, ) encoded = json.dumps(result, ensure_ascii=False) self.assertFalse(result["ok"]) self.assertEqual(result["status"], "invalid_snapshot") self.assertNotIn("阿铎反水击毙加特朗", encoded) self.assertEqual(result["findings"][0]["reason"], "target_fact_match") self.assertIn("factHash", result["findings"][0]) self.assertIn("valueHash", result["findings"][0]) def test_paraphrased_real_target_event_is_blocked(self): """真实错标卡不会逐字复制目标细纲,仍须识别同一事件的多个强锚点。""" facts = { "targetChapter": 489, "forbiddenFacts": [ { "id": "key-events-489", "firstChapter": 489, "text": "关键事件:苏铭投两枚核雷撕开缺口、安若雪同步率100%强攻加特朗、铎叔反水贯杀加特朗", } ], } result = audit_snapshot( { "cards": [ { "chapter": 488, "fact": "与加特朗激战中深陷压力,转交主攻位置后用生命进发让同步率飙至100%,击碎加特朗能量屏障", } ] }, facts, as_of=488, target=489, ) encoded = json.dumps(result, ensure_ascii=False) self.assertFalse(result["ok"]) self.assertEqual(result["findings"][0]["reason"], "target_fact_partial_match") self.assertNotIn("加特朗", encoded) self.assertNotIn("同步率", encoded) def test_related_but_distinct_history_does_not_false_positive(self): """只共享旧角色和体系术语、但缺少目标事件关键锚点时不得误报。""" facts = { "targetChapter": 489, "forbiddenFacts": [ { "id": "key-events-489", "firstChapter": 489, "text": "安若雪同步率100%强攻加特朗", } ], } result = audit_snapshot( {"events": [{"chapter": 387, "fact": "安若雪开启拘束解除,同步率突破95%重创死亡收割者"}]}, facts, as_of=488, target=489, ) self.assertTrue(result["ok"]) self.assertEqual(result["findingCount"], 0) def test_entities_scattered_across_long_history_do_not_form_one_event(self): """多个实体分散在旧细纲的不同事件里,不能仅凭名字凑成目标事件。""" facts = { "targetChapter": 489, "forbiddenFacts": [ { "id": "network-attack-489", "firstChapter": 489, "text": "赛莉丝入虚拟城外圈夺控防御闸门并坑杀凯多萨康里弗两方", } ], } historical_outline = ( "佛罗科密授康里弗追求安若雪以图谋王位;" "赛莉丝入侵要塞惊见囚禁的高等级灵械后退出;" "另一场景中凯多萨与康里弗争执,搅乱苏铭和安若雪的会面。" ) result = audit_snapshot( {"chapters": [{"chapter": 484, "outline": historical_outline}]}, facts, as_of=488, target=489, ) self.assertTrue(result["ok"]) self.assertEqual(result["findingCount"], 0) def test_target_and_as_of_must_form_next_chapter_pair(self): result = audit_snapshot({}, target_facts(), as_of=487, target=489) self.assertFalse(result["ok"]) self.assertEqual(result["status"], "invalid_audit_input") if __name__ == "__main__": unittest.main()