Files
codex-app-server/cloud/tests/test_v2_evidence.py
host 773945d136 v2 evidence: bind release evidence with ts/nonce, pin actual-only quota, ledger onboarding checklist, ledger failure outbox, read-only gate verifier (no live wiring)
- cloud/evidence_binding.py: bind/validate/nonce single-use store;
  release endpoint consumes nonce on first validated submission,
  replay -> 409 evidence_replayed, stale/future -> 409
  evidence_stale, partial -> 400, absent -> old path + label.
  Binding blocks exact replay and stale evidence, NOT restamping
  (unsigned evidence cannot prove content freshness).
- cloud/quota.py docstring: actual-only is the v2 integration
  position (already converged in v1 base via fef09ae, NOT in
  main); original PR semantics (#86 all-rows vs #90 actual-only)
  still pending coordinator confirmation at merge time.
- cloud/ledger_onboarding.py: DDL checklist referencing
  PG_DDL/PG_MARKER_DDL constants, read-only verify_ledger_ready
  and ledger_setup_report; default stays OFF, PG DDL stays an
  operator action (never claimed as executed).
- cloud/ledger_outbox.py + worker hook: ledger write failures
  go to a JSONL outbox in addition to the log and can be
  replayed; task outcome still unaffected (rule 7 kept),
  audit fail-closed untouched -- asymmetry documented, not
  silently changed.
- cloud/gate_verification.py: read-only verifier over an
  injected reader (match/mismatch/skipped); live wiring in
  merge/release is NOT possible on the current contract (no
  repo/ref in body or task row, no guaranteed trusted token),
  proven in the module header; no fake verification claimed.
- cloud/tests/test_v2_evidence.py: 18 new tests, all passing.

(cherry picked from commit bd85e33cd5e846fa6ee5d7e2440182dbd9c758b0)
2026-11-03 02:49:26 +00:00

488 lines
20 KiB
Python
Raw Permalink 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.
"""集成 v2 证据与口径硬伤定向测试(本轨新增,5 项全覆盖)。
1. 证据重放绑定(cloud/evidence_binding + release 端点):
原样重交被拒、正常证据通过、陈旧/未来被拒、只带其一
400、无绑定走旧路径并标注 absent;另钉住诚实边界:
绑定不鉴别内容(改 ci_status 后仍通过校验——防重盖
戳须签名或只读求证,不在本模块冒充)。
2. 无价 token 口径统一(cloud/quota.monthly_usage 为
server/worker/automations 三处共用 canonical 源):
actual 无价行计入、estimated 无价行不计入,并与
main 旧 SQL(计全部 NULL 行)做前后对比。
3. 台账钩子开箱(cloud/ledger_onboarding):表缺时
not ready、表齐且钩子开时 ready、表齐但钩子关时
仍 not ready;DDL 清单引用常量本身,不复制文本。
4. 台账写失败 outbox(cloud/ledger_outbox + worker
_record_ledger_attempt 接线):失败进 outbox、重放
补写行、重放后 outbox 清空、二次重放 0 行;任务
侧返回值仍为 None(不挡任务,规则 7 不变)。
5. 只读核对器(cloud/gate_verification):假 reader
下一致通过、谎报 CI/保护被 mismatch、无 reader /
无目标 / reader 失败一律 checked=False 且 verified
恒 False(skipped 绝不当 pass);实时接线不可实现
的三点见 gate_verification 模块头,服务端 docstring
已同步该边界(本文件不测不存在的实时求证)。
"""
import http.client
import json
import logging
import os
import sqlite3
import tempfile
import threading
import time
import unittest
from pathlib import Path
try:
from cloud import ledger_onboarding
from cloud import ledger_outbox
from cloud.attempt_hook import ensure_marker_table
from cloud.attempt_ledger import PG_DDL as LEDGER_PG_DDL
from cloud.attempt_ledger import record_attempt
from cloud.attempt_hook import PG_MARKER_DDL as MARKER_PG_DDL
from cloud.cost import CostStore
from cloud.evidence_binding import EvidenceNonceStore
from cloud.evidence_binding import bind_evidence
from cloud.evidence_binding import binding_presence
from cloud.evidence_binding import validate_bound_evidence
from cloud.gate_verification import verify_gate_evidence
from cloud.quota import decide as quota_decide
from cloud.quota import monthly_usage
except ImportError:
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud import ledger_onboarding
from cloud import ledger_outbox
from cloud.attempt_hook import ensure_marker_table
from cloud.attempt_ledger import PG_DDL as LEDGER_PG_DDL
from cloud.attempt_ledger import record_attempt
from cloud.attempt_hook import PG_MARKER_DDL as MARKER_PG_DDL
from cloud.cost import CostStore
from cloud.evidence_binding import EvidenceNonceStore
from cloud.evidence_binding import bind_evidence
from cloud.evidence_binding import binding_presence
from cloud.evidence_binding import validate_bound_evidence
from cloud.gate_verification import verify_gate_evidence
from cloud.quota import decide as quota_decide
from cloud.quota import monthly_usage
def _hard_body():
return {
"files_changed": ["apps/web/a.py"],
"owned_dir": "apps/web",
"branch_protection": "protected",
"ci_status": "success",
"ci_checks": [{"name": "ci", "status": "success"}],
"required_checks": ["ci"],
"contract_version": "v2",
"contract_frozen": True,
"coordinator_approved": True,
"unresolved_review_comments": 0,
}
class EvidenceBindingTestCase(unittest.TestCase):
def test_bind_and_validate_fresh(self):
stamped = bind_evidence(_hard_body())
self.assertEqual(binding_presence(stamped), "bound")
ok, reason = validate_bound_evidence(stamped)
self.assertTrue(ok)
self.assertIsNone(reason)
def test_partial_binding_is_not_bound(self):
body = _hard_body()
body["evidence_ts"] = time.time()
self.assertEqual(binding_presence(body), "partial")
ok, reason = validate_bound_evidence(body)
self.assertFalse(ok)
self.assertEqual(reason, "evidence_binding_incomplete")
def test_stale_and_future_rejected(self):
now = time.time()
old = bind_evidence(_hard_body(), now=now - 7200)
ok, reason = validate_bound_evidence(old, now=now)
self.assertFalse(ok)
self.assertEqual(reason, "evidence_too_old")
future = bind_evidence(_hard_body(), now=now + 3600)
ok, reason = validate_bound_evidence(future, now=now)
self.assertFalse(ok)
self.assertEqual(reason, "evidence_from_future")
def test_nonce_single_use_via_store(self):
conn = sqlite3.connect(":memory:")
store = EvidenceNonceStore(conn)
stamped = bind_evidence(_hard_body(), nonce="nonce-1")
self.assertTrue(store.consume("t1", stamped["evidence_nonce"],
stamped["evidence_ts"]))
conn.commit()
# 原样重交:同一 nonce 第二次消费即重放。
self.assertFalse(store.consume("t1", stamped["evidence_nonce"],
stamped["evidence_ts"]))
def test_binding_does_not_authenticate_content(self):
"""诚实边界:绑定防原样重放,不防重盖戳/篡改内容。
盖戳后改 ci_status,校验仍通过——因为没有签名。
本项钉住该边界,防止后来人把 binding 误读成防篡改。
"""
stamped = bind_evidence(_hard_body())
stamped["ci_status"] = "failure"
ok, _reason = validate_bound_evidence(stamped)
self.assertTrue(ok)
class QuotaActualOnlyTestCase(unittest.TestCase):
def test_estimated_unpriced_rows_do_not_trip_cap(self):
tmp = tempfile.TemporaryDirectory()
try:
db_path = os.path.join(tmp.name, "control.db")
store = CostStore(db_path)
conn = sqlite3.connect(db_path)
try:
conn.execute("CREATE TABLE tasks(id TEXT PRIMARY KEY,"
" created_by TEXT)")
conn.execute("INSERT INTO tasks(id, created_by)"
" VALUES(?, ?)", ("task-1", "user-1"))
conn.commit()
finally:
conn.close()
# actual 无价 100(60+40),estimated 无价 5000,
# actual 有价 1 行(usd 非 NULL,不进无价计数)。
store.record("task-1", "proj", "terra-model",
input_tokens=60, output_tokens=40,
usd_micros=None, source="actual")
store.record("task-1", "proj", "terra-model",
input_tokens=5000, output_tokens=0,
usd_micros=None, source="estimated")
store.record("task-1", "proj", "gpt-priced",
input_tokens=10, output_tokens=10,
usd_micros=12345, source="actual")
month = time.strftime("%Y-%m", time.gmtime())
conn = sqlite3.connect(db_path)
try:
used, unpriced = monthly_usage(conn, "user-1", month)
# 前后对比:main 旧 SQL(计全部 NULL 行)会得
# 5100;actual-only 口径只得 100。
old_row = conn.execute(
"SELECT COALESCE(SUM(c.input_tokens"
" + c.output_tokens), 0) FROM cost_records c"
" JOIN tasks t ON c.task_id = t.id"
" WHERE t.created_by = ? AND c.day LIKE ?"
" AND c.usd_micros IS NULL",
("user-1", month + "%")).fetchone()
self.assertEqual(int(old_row[0]), 5100)
self.assertEqual(unpriced, 100)
self.assertEqual(used, 12345)
# 双侧同一口径:CostStore 报表 unpriced 亦为 100。
report = store.report_by_task("task-1")
self.assertEqual(report["unpriced_actual_tokens"], 100)
# 上限判定:100 未超 1000 放行;5100(旧口径)
# 会误拦——这正是统一前服务端与执行侧的分歧点。
self.assertIsNone(quota_decide(10000000, used, unpriced,
1000, month))
self.assertIsNotNone(quota_decide(
10000000, used, 5100, 1000, month))
finally:
conn.close()
finally:
tmp.cleanup()
class LedgerOnboardingTestCase(unittest.TestCase):
def test_missing_tables_not_ready(self):
conn = sqlite3.connect(":memory:")
report = ledger_onboarding.ledger_setup_report(
conn, hook_enabled=True)
self.assertFalse(report["ready"])
self.assertFalse(report["tables_ready"])
self.assertIn("task_attempts table missing",
report["missing"])
self.assertIn("task_attempt_origin_markers table missing",
report["missing"])
def test_tables_ready_but_hook_off_not_ready(self):
conn = sqlite3.connect(":memory:")
record_attempt(conn, "task-1", "success", is_defect=False)
conn.commit()
ensure_marker_table(conn)
conn.commit()
report = ledger_onboarding.ledger_setup_report(
conn, hook_enabled=False)
self.assertTrue(report["tables_ready"])
self.assertFalse(report["ready"])
self.assertTrue(any("enable the hook" in s
for s in report["next_steps"]))
report_on = ledger_onboarding.ledger_setup_report(
conn, hook_enabled=True)
self.assertTrue(report_on["ready"])
def test_ddl_checklist_references_constants_not_copies(self):
sources = ledger_onboarding.ledger_ddl_sources()
self.assertEqual(sources["task_attempts_pg_ddl"],
LEDGER_PG_DDL)
self.assertEqual(sources["marker_pg_ddl"], MARKER_PG_DDL)
self.assertIn("task_attempts", LEDGER_PG_DDL)
self.assertIn("corrects_attempt_no", LEDGER_PG_DDL)
self.assertIn("task_attempt_origin_markers", MARKER_PG_DDL)
steps = [item["step"] for item in
ledger_onboarding.LEDGER_DDL_CHECKLIST]
self.assertEqual(steps, [1, 2, 3, 4, 5])
class LedgerOutboxTestCase(unittest.TestCase):
def test_append_and_replay(self):
tmp = tempfile.TemporaryDirectory()
try:
path = os.path.join(tmp.name, "failures.jsonl")
self.assertTrue(ledger_outbox.append_failure(
path, "task-1", "success", "boom"))
records, bad = ledger_outbox.read_failures(path)
self.assertEqual(len(records), 1)
self.assertEqual(bad, 0)
conn = sqlite3.connect(":memory:")
stats = ledger_outbox.replay_failures(path, conn)
conn.commit()
self.assertEqual(stats["replayed"], 1)
self.assertEqual(stats["remaining"], 0)
rows = conn.execute(
"SELECT outcome FROM task_attempts"
" WHERE task_id = ?", ("task-1",)).fetchall()
self.assertEqual([row[0] for row in rows], ["success"])
# 二次重放:outbox 已空,0 行,不会多写一行。
stats2 = ledger_outbox.replay_failures(path, conn)
self.assertEqual(stats2["replayed"], 0)
rows = conn.execute(
"SELECT COUNT(*) FROM task_attempts"
" WHERE task_id = ?", ("task-1",)).fetchone()
self.assertEqual(rows[0], 1)
finally:
tmp.cleanup()
def test_append_failure_never_raises(self):
# 路径父级是个文件时 makedirs 失败:返回 False,不抛。
tmp = tempfile.TemporaryDirectory()
try:
blocker = os.path.join(tmp.name, "blocker")
with open(blocker, "w") as handle:
handle.write("x")
bad_path = os.path.join(blocker, "failures.jsonl")
self.assertFalse(ledger_outbox.append_failure(
bad_path, "task-1", "success", "boom"))
finally:
tmp.cleanup()
def test_worker_record_failure_goes_to_outbox(self):
"""worker 接线:_record_ledger_attempt 写失败时,
除日志外 outbox 里留一行,且返回 None(不挡任务)。"""
import cloud.worker as worker_mod
tmp = tempfile.TemporaryDirectory()
try:
db_path = os.path.join(tmp.name, "control.db")
worker = worker_mod.Worker.__new__(worker_mod.Worker)
worker.db_path = db_path
worker.attempt_ledger_hook = True
worker.logger = logging.getLogger("test-v2-evidence")
original_connect = worker_mod._db_connect
def _boom(_path):
raise RuntimeError("db down")
worker_mod._db_connect = _boom
try:
result = worker._record_ledger_attempt(
"task-9", "success", True)
finally:
worker_mod._db_connect = original_connect
self.assertIsNone(result)
outbox = ledger_outbox.outbox_path_for(db_path)
records, bad = ledger_outbox.read_failures(outbox)
self.assertEqual(len(records), 1)
self.assertEqual(records[0]["task_id"], "task-9")
self.assertEqual(records[0]["outcome"], "success")
self.assertEqual(bad, 0)
finally:
tmp.cleanup()
class GateVerificationTestCase(unittest.TestCase):
def _live(self, checks, protection):
def reader(repo, ref):
self.assertEqual(repo, "o/r")
self.assertEqual(ref, "sha1")
return {"ci_checks": checks,
"branch_protection": protection}
return reader
def test_matching_evidence_verified(self):
result = verify_gate_evidence(
_hard_body(), reader=self._live(
{"ci": "success"}, "protected"),
repo="o/r", ref="sha1")
self.assertTrue(result["checked"])
self.assertTrue(result["verified"])
self.assertEqual(result["mismatches"], [])
def test_lied_ci_and_protection_mismatch(self):
result = verify_gate_evidence(
_hard_body(), reader=self._live(
{"ci": "failure"}, "unprotected"),
repo="o/r", ref="sha1")
self.assertTrue(result["checked"])
self.assertFalse(result["verified"])
self.assertTrue(any("ci_not_green_live" in m
for m in result["mismatches"]))
self.assertTrue(any("branch_protection" in m
for m in result["mismatches"]))
def test_no_reader_is_not_a_pass(self):
result = verify_gate_evidence(_hard_body(), reader=None,
repo="o/r", ref="sha1")
self.assertFalse(result["checked"])
self.assertFalse(result["verified"])
self.assertEqual(result["reason"], "no_trusted_reader")
def test_no_target_or_reader_failure_is_not_a_pass(self):
result = verify_gate_evidence(
_hard_body(), reader=self._live({}, "protected"),
repo=None, ref=None)
self.assertFalse(result["checked"])
self.assertFalse(result["verified"])
def _boom(_repo, _ref):
raise RuntimeError("github down")
result = verify_gate_evidence(_hard_body(), reader=_boom,
repo="o/r", ref="sha1")
self.assertFalse(result["checked"])
self.assertFalse(result["verified"])
self.assertTrue(result["reason"].startswith("reader_failed"))
class ReleaseEvidenceBindingServerTestCase(unittest.TestCase):
def setUp(self):
from cloud.auth import AuthStore
from cloud.server import create_server
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self._saved_env = {
k: os.environ.get(k) for k in (
"CLOUD_MERGE_GATE_MODE", "CLOUD_BRANCH_PROTECTION",
"CODEX_GLOBAL_ADMINS")}
os.environ.pop("CLOUD_MERGE_GATE_MODE", None)
os.environ.pop("CLOUD_BRANCH_PROTECTION", None)
self.auth = AuthStore(self.db_path)
self.owner_id, self.owner_key = self.auth.create_user("owner")
os.environ["CODEX_GLOBAL_ADMINS"] = self.owner_id
self.server = create_server(self.db_path, "127.0.0.1", 0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
status, self.project = self._req(
"POST", "/projects", {"name": "demo"}, self.owner_key)
self.assertEqual(status, 201)
status, task = self._req(
"POST", "/tasks",
{"project_id": self.project["id"], "prompt": "x"},
self.owner_key)
self.assertEqual(status, 201)
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"UPDATE tasks SET status = 'completed' WHERE id = ?",
(task["id"],))
conn.commit()
finally:
conn.close()
status, _merged = self._req(
"POST", "/tasks/%s/merge" % task["id"], _hard_body(),
self.owner_key)
self.assertEqual(status, 200)
self.task_id = task["id"]
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
for key, value in self._saved_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
def _req(self, method, path, body=None, key=None):
conn = http.client.HTTPConnection(
"127.0.0.1", self.port, timeout=10)
hdrs = {}
payload = None
if body is not None:
payload = json.dumps(body).encode("utf-8")
hdrs["Content-Type"] = "application/json"
if key is not None:
hdrs["X-API-Key"] = key
try:
conn.request(method, path, body=payload, 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
def test_replay_rejected_fresh_passes(self):
bound = bind_evidence(_hard_body())
status, first = self._req(
"POST", "/tasks/%s/release" % self.task_id, bound,
self.owner_key)
self.assertEqual(status, 200)
self.assertEqual(first["evidence_binding"], "bound")
# 原样重交同一份证据体:nonce 已消费,409 重放。
status, replay = self._req(
"POST", "/tasks/%s/release" % self.task_id, bound,
self.owner_key)
self.assertEqual(status, 409)
self.assertEqual(replay["release_gate"]["code"],
"release_blocked_evidence_replayed")
# 新 nonce 的正常证据仍通过(其余字段完全相同)。
fresh = bind_evidence(_hard_body())
status, second = self._req(
"POST", "/tasks/%s/release" % self.task_id, fresh,
self.owner_key)
self.assertEqual(status, 200)
self.assertEqual(second["evidence_binding"], "bound")
def test_stale_partial_and_absent(self):
stale = bind_evidence(_hard_body(), now=time.time() - 7200)
status, resp = self._req(
"POST", "/tasks/%s/release" % self.task_id, stale,
self.owner_key)
self.assertEqual(status, 409)
self.assertEqual(resp["release_gate"]["code"],
"release_blocked_evidence_stale")
partial = _hard_body()
partial["evidence_ts"] = time.time()
status, _resp = self._req(
"POST", "/tasks/%s/release" % self.task_id, partial,
self.owner_key)
self.assertEqual(status, 400)
status, plain = self._req(
"POST", "/tasks/%s/release" % self.task_id, _hard_body(),
self.owner_key)
self.assertEqual(status, 200)
self.assertEqual(plain["evidence_binding"], "absent")
if __name__ == "__main__":
unittest.main()