"""防篡改账本(哈希链)+ 成本双列视图单测。 不联网、不跑真实模型:账本走临时 SQLite 文件,篡改/删除 用 SQL 直写模拟(先 DROP 触发器,模拟有 DDL 权限的攻击者; 触发器本身另有单测),服务端点走本地临时端口。 """ import http.client import json import os import sqlite3 import sys import tempfile import threading import unittest from pathlib import Path try: from cloud.auth import AuthStore from cloud.cost import CostStore, dual_column_view, price_usd_micros from cloud.ledger import ( GENESIS_HASH, LedgerStore, append_pushback_round, audit_timeline, export_chain, list_pushback_rounds, module_failure_counts, verify_chain, verify_export) from cloud.server import create_server, init_db except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.auth import AuthStore from cloud.cost import CostStore, dual_column_view, price_usd_micros from cloud.ledger import ( GENESIS_HASH, LedgerStore, append_pushback_round, audit_timeline, export_chain, list_pushback_rounds, module_failure_counts, verify_chain, verify_export) from cloud.server import create_server, init_db def _req(port, method, path, key=None): conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10) hdrs = {} if key is not None: hdrs["X-API-Key"] = key try: conn.request(method, path, headers=hdrs) resp = conn.getresponse() data = resp.read() finally: conn.close() try: parsed = json.loads(data.decode("utf-8")) if data else None except ValueError: parsed = None return resp.status, parsed class TestLedgerChain(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.store = LedgerStore(os.path.join(self.tmp.name, "led.db")) def tearDown(self): self.store.close() self.tmp.cleanup() def test_append_and_verify_ok(self): self.store.append("u1", "p1", "create_task", "t1") self.store.append("u1", "p1", "approve_merge", "t1") result = self.store.verify() self.assertTrue(result["ok"]) self.assertEqual(result["count"], 2) self.assertEqual(result["failures"], []) self.assertNotEqual(result["head_hash"], GENESIS_HASH) def test_append_only_interface(self): # 接口层面即只追加:没有修改/删除方法(test_auth 同款断言)。 self.assertFalse(hasattr(LedgerStore, "delete_entry")) self.assertFalse(hasattr(LedgerStore, "update_entry")) self.assertFalse(hasattr(LedgerStore, "delete")) self.assertFalse(hasattr(LedgerStore, "update")) def test_sqlite_triggers_block_update_and_delete(self): self.store.append("u1", "p1", "create_task", "t1") with self.assertRaises(Exception): self.store.conn.execute( "UPDATE audit_ledger SET detail='x' WHERE id=1") with self.assertRaises(Exception): self.store.conn.execute( "DELETE FROM audit_ledger WHERE id=1") self.store.conn.rollback() self.assertTrue(self.store.verify()["ok"]) def test_tamper_detail_fails_verify(self): self.store.append("u1", "p1", "create_task", "t1") self.store.append("u1", "p1", "approve_merge", "t1") # 模拟有 DDL 权限的攻击者:先卸触发器再改历史。 self.store.conn.execute( "DROP TRIGGER IF EXISTS audit_ledger_no_update") self.store.conn.execute( "UPDATE audit_ledger SET detail='forged' WHERE id=1") self.store.conn.commit() result = self.store.verify() self.assertFalse(result["ok"]) self.assertTrue(any("tampered" in f for f in result["failures"])) def test_delete_middle_breaks_chain(self): for i in range(3): self.store.append("u1", "p1", "act-%d" % i, "d%d" % i) self.store.conn.execute( "DROP TRIGGER IF EXISTS audit_ledger_no_delete") self.store.conn.execute("DELETE FROM audit_ledger WHERE id=2") self.store.conn.commit() result = self.store.verify() self.assertFalse(result["ok"]) self.assertTrue(any("broken" in f for f in result["failures"])) def test_delete_tail_detected_by_old_head(self): for i in range(3): self.store.append("u1", "p1", "act-%d" % i, "d%d" % i) old_export = self.store.export() old_head = old_export["head_hash"] self.store.conn.execute( "DROP TRIGGER IF EXISTS audit_ledger_no_delete") self.store.conn.execute( "DELETE FROM audit_ledger WHERE id=3") self.store.conn.commit() # 截断后的链自洽(固有局限,如实断言),但旧头对不上。 self.assertTrue(self.store.verify()["ok"]) self.assertFalse( self.store.verify(expected_head=old_head)["ok"]) self.assertFalse( verify_export(self.store.export(), expected_head=old_head)["ok"]) def test_export_round_trip_and_tampered_export(self): self.store.append("u1", "p1", "create_task", "t1") self.store.append("u1", "p1", "approve_merge", "t1") data = self.store.export() self.assertTrue(verify_export(data)["ok"]) self.assertEqual(data["count"], 2) data["entries"][0]["detail"] = "forged" self.assertFalse(verify_export(data)["ok"]) data2 = self.store.export() data2["head_hash"] = "0" * 64 self.assertFalse(verify_export(data2)["ok"]) def test_legacy_migration_marks_pre_chain_without_rewrite(self): # 老 audit_log 行(直接 INSERT,模拟链上线前的历史)。 conn = sqlite3.connect(os.path.join(self.tmp.name, "led.db")) try: conn.execute( "CREATE TABLE IF NOT EXISTS audit_log(" "id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT," " project_id TEXT, action TEXT, detail TEXT," " created_at REAL)") conn.execute( "INSERT INTO audit_log(user_id, project_id, action," " detail, created_at) VALUES(?,?,?,?,?)", ("u1", "p1", "create_task", "t0", 1000.0)) conn.execute( "INSERT INTO audit_log(user_id, project_id, action," " detail, created_at) VALUES(?,?,?,?,?)", ("u1", "p1", "approve_merge", "t0", 1001.0)) conn.commit() finally: conn.close() # 链已空但老表有行:verify 必须 fail-closed,不认账。 fresh = LedgerStore(os.path.join(self.tmp.name, "led.db")) try: result = fresh.verify() self.assertFalse(result["ok"]) self.assertTrue(any("legacy_not_migrated" in f for f in result["failures"])) migrated = fresh.migrate_legacy() self.assertTrue(migrated["migrated"]) self.assertEqual( migrated["marker"]["action"], "ledger_genesis") after = fresh.verify() self.assertTrue(after["ok"]) self.assertIsNotNone(after["genesis_marker"]) self.assertEqual( after["genesis_marker"]["legacy_audit_rows"], 2) # 迁移后链已启动,再跑迁移必须拒绝,不补不改。 self.assertFalse(fresh.migrate_legacy()["migrated"]) # 老行原样未动:仍 2 行、无哈希列。 raw = sqlite3.connect(os.path.join(self.tmp.name, "led.db")) try: rows = raw.execute( "SELECT COUNT(*) FROM audit_log").fetchone() cols = [c[1] for c in raw.execute( "PRAGMA table_info(audit_log)").fetchall()] finally: raw.close() self.assertEqual(rows[0], 2) self.assertNotIn("entry_hash", cols) finally: fresh.close() def test_auth_audit_writes_chain_same_transaction(self): db_path = os.path.join(self.tmp.name, "auth.db") auth = AuthStore(db_path) try: owner_id, _key = auth.create_user("owner") auth.create_project("p1", owner_id) auth.audit(owner_id, "p1", "create_task", "task-1") auth.audit(owner_id, "p1", "approve_merge", "task-1") self.assertEqual(len(auth.list_audit("p1")), 2) finally: auth.conn.close() led = LedgerStore(db_path) try: result = led.verify() self.assertTrue(result["ok"]) self.assertEqual(result["count"], 2) actions = [e["action"] for e in led.export()["entries"]] self.assertEqual(actions, ["create_task", "approve_merge"]) finally: led.close() class TestPushbackAndRebuild(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.store = LedgerStore(os.path.join(self.tmp.name, "led.db")) def tearDown(self): self.store.close() self.tmp.cleanup() def _verdict(self, verdict, source, task="task-1"): return self.store.append_audit_verdict( task, "p1", "mod-a", "v1", verdict, source, failures=["f1"] if verdict == "fail" else []) def test_failure_counts_split_sources_and_reset_on_pass(self): self._verdict("fail", "audit") self._verdict("fail", "acceptance") counts = self.store.module_failure_counts("mod-a", "v1") self.assertEqual(counts["audit_fails"], 1) self.assertEqual(counts["acceptance_fails"], 1) self.assertEqual(counts["total_fails"], 2) self.assertFalse(counts["rebuild_triggered"]) self._verdict("pass", "audit") counts = self.store.module_failure_counts("mod-a", "v1") self.assertEqual(counts["total_fails"], 0) def test_contract_version_change_restarts_count(self): self._verdict("fail", "audit") self._verdict("fail", "audit") self.assertEqual( self.store.module_failure_counts("mod-a", "v1")["total_fails"], 2) self.assertEqual( self.store.module_failure_counts("mod-a", "v2")["total_fails"], 0) def test_rebuild_trigger_and_rebuild_resets_baseline(self): for source in ("audit", "acceptance", "audit"): self._verdict("fail", source) counts = self.store.module_failure_counts("mod-a", "v1") self.assertEqual(counts["total_fails"], 3) self.assertFalse(counts["rebuild_triggered"]) self._verdict("fail", "audit") # 第 4 次:推倒触发点 counts = self.store.module_failure_counts("mod-a", "v1") self.assertTrue(counts["rebuild_triggered"]) self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 1, {"commit": "abc123"}, root_cause="implementation", status="decided", prior_failure_total=4) with self.assertRaises(ValueError): # rebuilt 必须带新实现引用。 self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 1, {"commit": "abc123"}, root_cause="implementation", status="rebuilt") self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 1, {"commit": "abc123"}, new_impl_ref={"commit": "def456"}, root_cause="implementation", status="rebuilt", prior_failure_total=4) counts = self.store.module_failure_counts("mod-a", "v1") self.assertEqual(counts["total_fails"], 0) # 重做后再超 3 次:同样触发(不再自动重开是升级语义, # 账本只负责计数与触发信号,不替第 3 轨做升级决策)。 for _ in range(4): self._verdict("fail", "audit") self.assertTrue( self.store.module_failure_counts( "mod-a", "v1")["rebuild_triggered"]) def test_contract_design_root_cause_recorded_not_rebuild(self): entry = self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 1, {"tag": "v1-old"}, root_cause="contract_design", status="decided") self.assertEqual(entry["action"], "audit_rebuild") with self.assertRaises(ValueError): self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 2, {"commit": "x"}, root_cause="something_else") def test_timeline_reconstructs_full_path_without_erasure(self): # fail → 推回 1-3 轮 → 第 4 次 fail → 推倒 → 重做 → 再审 pass self._verdict("fail", "audit") for round_no in (1, 2, 3): self.store.append_pushback_round( "task-1", "p1", round_no, ["f1"], {"failures": ["f1"]}, module_id="mod-a", contract_version="v1", failure_source="audit") self._verdict("fail", "audit") # 此时累计:首 fail + 3 轮后各 1 fail = 4 次,已触发推倒。 self.assertTrue( self.store.module_failure_counts( "mod-a", "v1")["rebuild_triggered"]) self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 1, {"commit": "old"}, new_impl_ref={"commit": "new"}, root_cause="implementation", status="rebuilt", prior_failure_total=4) self._verdict("pass", "audit") timeline = self.store.audit_timeline(task_id="task-1") actions = [e["action"] for e in timeline["entries"]] self.assertEqual(actions.count("audit_verdict"), 5) self.assertEqual(actions.count("audit_pushback_round"), 3) self.assertEqual(actions.count("audit_rebuild"), 1) # 最终 pass 在末尾,前面 4 次 fail 一个不少。 self.assertEqual(timeline["entries"][-1]["parsed"]["verdict"], "pass") self.assertEqual(timeline["summary"]["verdict_fail"], 4) self.assertEqual(timeline["summary"]["verdict_pass"], 1) self.assertEqual(timeline["summary"]["rebuilds_completed"], 1) self.assertTrue(self.store.verify()["ok"]) def test_pushback_round_over_3_flagged_in_timeline(self): self.store.append_pushback_round( "task-1", "p1", 4, ["f1"], {"failures": ["f1"]}, module_id="mod-a", contract_version="v1") timeline = self.store.audit_timeline(task_id="task-1") self.assertEqual(len(timeline["summary"]["anomalies"]), 1) def test_pushback_rounds_list_and_validation(self): append_pushback_round( self.store.conn, "task-1", "p1", 1, ["f1"], {"failures": ["f1"], "evidence": "e"}) self.store.conn.commit() rounds = list_pushback_rounds(self.store.conn, task_id="task-1") self.assertEqual(len(rounds), 1) self.assertEqual(rounds[0]["pushback"]["round_no"], 1) with self.assertRaises(ValueError): append_pushback_round( self.store.conn, "task-1", "p1", 1, [], {"failures": []}) class TestTightenedCaliber(unittest.TestCase): """三条收紧:第三次失败判定、保留资产、测试单审、 审计的审计、重开前用户确认。""" def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.store = LedgerStore(os.path.join(self.tmp.name, "led.db")) def tearDown(self): self.store.close() self.tmp.cleanup() def _verdict(self, verdict, source="audit", task="task-1"): return self.store.append_audit_verdict( task, "p1", "mod-a", "v1", verdict, source, failures=["f1"] if verdict == "fail" else []) def test_third_failure_assessment_kinds(self): entry = self.store.append_third_failure_assessment( "task-1", "p1", "mod-a", "v1", "early_rebuild", {"audit_conclusion": "structural", "failure_items": ["f1"], "diff_scope": "mod-a/*", "passed_items": ["a1"], "continuation_counting": "extra rounds count as fails"}) self.assertEqual(entry["action"], "audit_third_failure_assessment") cont = self.store.append_third_failure_assessment( "task-1", "p1", "mod-a", "v1", "continuation_round", {"audit_conclusion": "nearly done"}) parsed = json.loads(cont["detail"]) self.assertEqual(parsed["max_extra_rounds"], 1) with self.assertRaises(ValueError): self.store.append_third_failure_assessment( "task-1", "p1", "mod-a", "v1", "early_rebuild", {"audit_conclusion": "x"}, max_extra_rounds=1) with self.assertRaises(ValueError): self.store.append_third_failure_assessment( "task-1", "p1", "mod-a", "v1", "continuation_round", {}) timeline = self.store.audit_timeline(task_id="task-1") self.assertEqual(timeline["summary"]["third_failure_assessments"], 2) def test_rebuild_retained_assets(self): entry = self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 1, {"commit": "old"}, new_impl_ref={"commit": "new"}, root_cause="implementation", status="rebuilt", retained_assets={ "old_test_set_ref": "tests/mod-a@old", "passed_acceptance_items": ["a1", "a2"], "old_new_impl_mapping": {"old": "new"}, "old_tests_status": "pass"}) parsed = json.loads(entry["detail"]) self.assertEqual(parsed["retained_assets"]["old_tests_status"], "pass") with self.assertRaises(ValueError): self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 2, {"commit": "old"}, retained_assets={ "old_tests_status": "mostly"}) def test_test_change_audit_gate(self): # 纯新增不走重新冻结门。 self.store.append_test_change_audit( "task-1", "p1", "mod-a", "v1", {"added": ["test_new"]}, auditor="aud-1", contract_refrozen=False) # 删改不走重新冻结:拒绝写入。 with self.assertRaises(ValueError): self.store.append_test_change_audit( "task-1", "p1", "mod-a", "v1", {"deleted": ["test_old"]}, auditor="aud-1", contract_refrozen=False) # 删改 + 重新冻结 + 引用:通过。 entry = self.store.append_test_change_audit( "task-1", "p1", "mod-a", "v1", {"modified": ["test_m"], "deleted": ["test_old"]}, auditor="aud-1", contract_refrozen=True, refreeze_ref="v2", reason="contract change") self.assertEqual(entry["action"], "audit_test_change") with self.assertRaises(ValueError): # refrozen 时引用必填。 self.store.append_test_change_audit( "task-1", "p1", "mod-a", "v1", {"modified": ["test_m"]}, auditor="aud-1", contract_refrozen=True) def test_meta_review_correction_changes_effective_count(self): first = self._verdict("fail") self._verdict("fail") third = self._verdict("fail") self.assertEqual( self.store.module_failure_counts( "mod-a", "v1")["total_fails"], 3) # 纠正最新一条 fail → pass:计数清零(pass 即基线)。 self.store.append_audit_of_audit( "task-1", "p1", "mod-a", "v1", third["id"], "fail", "pass", True, reviewer="meta-1", findings=["evidence misread"], triggered_process="none") self.assertEqual( self.store.module_failure_counts( "mod-a", "v1")["total_fails"], 0) # 但链上原 fail 与纠正都保留,时间线不抹。 timeline = self.store.audit_timeline(task_id="task-1") self.assertEqual(timeline["summary"]["verdict_fail"], 3) self.assertEqual(timeline["summary"]["meta_reviews"], 1) self.assertTrue(self.store.verify()["ok"]) def test_meta_review_first_fail_correction_semantics(self): first = self._verdict("fail") self._verdict("fail") self._verdict("fail") # 纠正第一条 fail → pass:pass 清零后,后两条 fail 重计为 2。 self.store.append_audit_of_audit( "task-1", "p1", "mod-a", "v1", first["id"], "fail", "pass", True, reviewer="meta-1", findings=["wrong item"], triggered_process="re-review") self.assertEqual( self.store.module_failure_counts( "mod-a", "v1")["total_fails"], 2) def test_acceptance_rebuild_requires_confirmed_notice(self): # 无通知:拒绝。 with self.assertRaises(ValueError): self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 1, {"commit": "old"}, root_cause="implementation", trigger_source="acceptance") # 仅 pending 通知:仍拒绝。 self.store.append_rebuild_notice( "task-1", "p1", "mod-a", "v1", 1, "new direction: rebuild mod-a", notified_party="user-1") with self.assertRaises(ValueError): self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 1, {"commit": "old"}, root_cause="implementation", trigger_source="acceptance") # confirmed 后:通过;audit 触发本来就不走此门。 self.store.append_rebuild_notice( "task-1", "p1", "mod-a", "v1", 1, "new direction: rebuild mod-a", notified_party="user-1", confirmation_status="confirmed", confirmed_by="user-1") entry = self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 1, {"commit": "old"}, root_cause="implementation", trigger_source="acceptance") self.assertEqual(entry["action"], "audit_rebuild") self.store.append_rebuild_decision( "task-1", "p1", "mod-a", "v1", 2, {"commit": "old"}, root_cause="implementation", trigger_source="audit") timeline = self.store.audit_timeline(task_id="task-1") self.assertEqual(timeline["summary"]["rebuild_notices"], 2) class TestCostDualColumn(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.store = CostStore(os.path.join(self.tmp.name, "cost.db")) def tearDown(self): try: self.store._conn.close() except Exception: pass self.tmp.cleanup() def test_unpriced_never_priced_as_zero(self): priced = price_usd_micros("gpt-6.1-sol", 1000000, 0, 0) self.assertIsNotNone(priced) self.store.record("t1", "p1", "gpt-6.1-sol", input_tokens=1000000, output_tokens=0, usd_micros=priced, source="actual") # 无价模型:usd_micros=None,绝不写成 0。 self.store.record("t1", "p1", "unpriced-model", input_tokens=5000, output_tokens=0, usd_micros=None, source="actual") view = dual_column_view(self.store.report_by_task("t1")) self.assertEqual(view["priced_usd_micros"], priced) self.assertEqual(view["usd_micros"], priced) self.assertEqual(view["unpriced_actual_tokens"], 5000) self.assertIn("billing_note", view) self.assertIn("不等于免费", view["billing_note"]) class TestLedgerServerEndpoints(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.db_path = os.path.join(self.tmp.name, "srv.db") auth = AuthStore(self.db_path) try: self.admin_id, self.admin_key = auth.create_user("admin") self.user_id, self.user_key = auth.create_user("plain") auth.create_project("p1", self.admin_id) finally: auth.conn.close() init_db(self.db_path) self._old_admins = os.environ.get("CODEX_GLOBAL_ADMINS") os.environ["CODEX_GLOBAL_ADMINS"] = self.admin_id self.server = create_server(self.db_path, port=0) self.port = self.server.server_address[1] self.thread = threading.Thread( target=self.server.serve_forever, daemon=True) self.thread.start() def tearDown(self): try: self.server.shutdown() self.server.server_close() except Exception: pass if self._old_admins is None: os.environ.pop("CODEX_GLOBAL_ADMINS", None) else: os.environ["CODEX_GLOBAL_ADMINS"] = self._old_admins self.tmp.cleanup() def test_verify_and_export_admin_only(self): status, body = _req(self.port, "GET", "/admin/audit-ledger/verify", key=self.admin_key) self.assertEqual(status, 200) self.assertTrue(body["ok"]) status, body = _req(self.port, "GET", "/admin/audit-ledger/export?limit=10", key=self.admin_key) self.assertEqual(status, 200) self.assertEqual(body["version"], 1) status, _body = _req(self.port, "GET", "/admin/audit-ledger/verify", key=self.user_key) self.assertEqual(status, 403) status, _body = _req(self.port, "GET", "/admin/audit-ledger/export?limit=xx", key=self.admin_key) self.assertEqual(status, 400) if __name__ == "__main__": unittest.main()