Files
codex-app-server/cloud/tests/test_scorecard_mech_live.py
host 3978dbbbf2 v7 mech-6: production-evidence fail-closed live wiring seam (no real reader, no endpoint wiring yet)
- LiveEvidenceReader protocol: get_checks(repo, ref) /
  get_branch_protection(repo, branch), results carry status
  plus raw evidence; production code has no HTTP call, no
  token read, no real GitHub API call (AST-asserted).
- verify_live_evidence: no reader / no repo-ref / reader
  failure / mismatch all verified=False; comparison shared
  with verify_gate_evidence via _compare_claimed_vs_live.
- live_gate_decision: verified=False -> allowed=False,
  http_status=409, status=blocked; verified=True -> 200.
- Endpoints _merge_task/_release_task NOT wired yet (no
  production reader exists; wiring None would block all
  merges/releases); real live verification still pending
  B6 real App install and B1 user-side items.

(cherry picked from commit 7e8bfdee1f22c28329602314f3e166cde34eb6cf)
2026-11-03 02:49:27 +00:00

312 lines
13 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.
"""v7 机制第 6 项:生产证据 fail-closed live 接线定向测试。
只测这一项,不碰其它 7 项,不改阈值(阈值仍
test_default_unapproved,见 cloud/audit_scorecard.py,
本文件不测阈值)、不改 #238 auto_apply、不改 #247
“纠正为 pass 本身不解封”、不改 v1.1 契约。
被测对象(cloud/gate_verification.py):
- LiveEvidenceReader 协议:get_checks(repo, ref) /
get_branch_protection(repo, branch),返回结构含状态
与原始证据;生产代码里绝无真实 HTTP 调用、真实
token 读取、真实 GitHub API 调用(本文件以 AST 断
言钉住这一点)。
- verify_live_evidence(claimed, reader, repo, ref):无
reader、无 repo/ref、reader 失败、reader 返回与声称
不一致时 verified 一律 False;声称 CI 全绿但假 reader
返回失败/缺失时记 mismatch。
- live_gate_decision:verified=False 时返回 409 语义
(allowed=False、http_status=409、status="blocked"、
blocked=True),verified=True 时返回 200 语义。
诚实边界:本文件测的是判定函数,不冒充 HTTP 端点
已接线——_merge_task / _release_task 本批未接
live_gate_decision(无生产 reader 可注入;见
gate_verification 模块头残留),真实 live 验证仍待
B6 真实 App 安装与 B1 等用户侧事项。
"""
import ast
import unittest
from pathlib import Path
try:
from cloud.gate_verification import LiveChecksResult
from cloud.gate_verification import LiveEvidenceReader
from cloud.gate_verification import LiveProtectionResult
from cloud.gate_verification import live_gate_decision
from cloud.gate_verification import verify_live_evidence
except ImportError:
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.gate_verification import LiveChecksResult
from cloud.gate_verification import LiveEvidenceReader
from cloud.gate_verification import LiveProtectionResult
from cloud.gate_verification import live_gate_decision
from cloud.gate_verification import verify_live_evidence
def _claimed_green():
return {
"ci_status": "success",
"ci_checks": [{"name": "ci", "status": "success"}],
"required_checks": ["ci"],
"branch_protection": "protected",
}
class FakeReader(LiveEvidenceReader):
"""实现协议的假 reader:不发网络请求,只返回预设值。
同时记录收到的 repo/ref/branch,断言接线把求证目标
原样传给 reader(不猜目标)。
"""
def __init__(self, checks, protection, checks_raw=None,
protection_raw=None):
self.checks = checks
self.protection = protection
self.checks_raw = checks_raw
self.protection_raw = protection_raw
self.seen = []
def get_checks(self, repo, ref):
self.seen.append(("checks", repo, ref))
return LiveChecksResult(checks=dict(self.checks),
raw=self.checks_raw)
def get_branch_protection(self, repo, branch):
self.seen.append(("protection", repo, branch))
return LiveProtectionResult(status=self.protection,
raw=self.protection_raw)
class FailingReader(LiveEvidenceReader):
def __init__(self, fail_on):
self.fail_on = fail_on
def get_checks(self, repo, ref):
if self.fail_on == "checks":
raise RuntimeError("github down")
return LiveChecksResult(checks={"ci": "success"})
def get_branch_protection(self, repo, branch):
if self.fail_on == "protection":
raise RuntimeError("github down")
return LiveProtectionResult(status="protected")
class LiveEvidenceTestCase(unittest.TestCase):
def test_matching_fake_reader_verified(self):
reader = FakeReader({"ci": "success"}, "protected",
checks_raw={"total_count": 1},
protection_raw={"required": True})
result = verify_live_evidence(
_claimed_green(), reader, "o/r", "sha1",
branch="main")
self.assertTrue(result["checked"])
self.assertTrue(result["verified"])
self.assertEqual(result["mismatches"], [])
self.assertIsNone(result["reason"])
self.assertEqual(result["live"]["ci_checks"],
{"ci": "success"})
self.assertEqual(result["live"]["branch_protection"],
"protected")
self.assertEqual(result["live"]["checks_raw"],
{"total_count": 1})
self.assertEqual(result["live"]["protection_raw"],
{"required": True})
self.assertIn(("checks", "o/r", "sha1"), reader.seen)
self.assertIn(("protection", "o/r", "main"), reader.seen)
def test_lied_all_green_vs_failing_live_mismatch_to_409(self):
# 谎报全绿:声称 ci success + required ci,但实时数据
# 里 ci 是 failure、且 required 的 lint 查无此项。
claimed = _claimed_green()
claimed["required_checks"] = ["ci", "lint"]
reader = FakeReader({"ci": "failure"}, "protected")
result = verify_live_evidence(claimed, reader, "o/r", "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("check_not_green_live: ci" in m
for m in result["mismatches"]))
self.assertTrue(any("required_check_not_green_live: lint" in m
for m in result["mismatches"]))
decision = live_gate_decision(claimed, reader, "o/r", "sha1")
self.assertFalse(decision["allowed"])
self.assertTrue(decision["blocked"])
self.assertEqual(decision["http_status"], 409)
self.assertEqual(decision["status"], "blocked")
self.assertEqual(decision["code"], "live_evidence_mismatch")
self.assertFalse(decision["live"]["verified"])
def test_protection_mismatch_blocks(self):
reader = FakeReader({"ci": "success"}, "unprotected")
result = verify_live_evidence(
_claimed_green(), reader, "o/r", "sha1")
self.assertTrue(result["checked"])
self.assertFalse(result["verified"])
self.assertTrue(any("branch_protection" in m
for m in result["mismatches"]))
decision = live_gate_decision(
_claimed_green(), reader, "o/r", "sha1")
self.assertEqual(decision["http_status"], 409)
self.assertEqual(decision["status"], "blocked")
def test_no_reader_is_not_a_pass(self):
result = verify_live_evidence(
_claimed_green(), None, "o/r", "sha1")
self.assertFalse(result["checked"])
self.assertFalse(result["verified"])
self.assertEqual(result["reason"], "no_trusted_reader")
self.assertIsNone(result["live"])
decision = live_gate_decision(
_claimed_green(), None, "o/r", "sha1")
self.assertFalse(decision["allowed"])
self.assertEqual(decision["http_status"], 409)
self.assertEqual(decision["status"], "blocked")
self.assertEqual(decision["code"], "live_evidence_unverified")
def test_no_repo_or_ref_is_not_a_pass(self):
reader = FakeReader({"ci": "success"}, "protected")
for repo, ref in ((None, "sha1"), ("o/r", None),
("o/r", ""), ("", "sha1")):
result = verify_live_evidence(
_claimed_green(), reader, repo, ref)
self.assertFalse(result["checked"])
self.assertFalse(result["verified"])
self.assertEqual(result["reason"],
"no_verification_target")
decision = live_gate_decision(
_claimed_green(), reader, repo, ref)
self.assertEqual(decision["http_status"], 409)
def test_reader_failure_is_not_a_pass(self):
for fail_on in ("checks", "protection"):
result = verify_live_evidence(
_claimed_green(), FailingReader(fail_on),
"o/r", "sha1")
self.assertFalse(result["checked"])
self.assertFalse(result["verified"])
self.assertTrue(
result["reason"].startswith("reader_failed"))
decision = live_gate_decision(
_claimed_green(), FailingReader(fail_on),
"o/r", "sha1")
self.assertEqual(decision["http_status"], 409)
self.assertEqual(decision["code"],
"live_evidence_unverified")
def test_malformed_reader_result_is_not_a_pass(self):
class BadChecks(LiveEvidenceReader):
def get_checks(self, repo, ref):
return 42
def get_branch_protection(self, repo, branch):
return LiveProtectionResult(status="protected")
class BadStatus(LiveEvidenceReader):
def get_checks(self, repo, ref):
return LiveChecksResult(checks={"ci": 1})
def get_branch_protection(self, repo, branch):
return LiveProtectionResult(status="protected")
for reader in (BadChecks(), BadStatus()):
result = verify_live_evidence(
_claimed_green(), reader, "o/r", "sha1")
self.assertFalse(result["checked"])
self.assertFalse(result["verified"])
self.assertTrue(
result["reason"].startswith("reader_failed"))
def test_matching_fake_reader_decision_allows(self):
reader = FakeReader({"ci": "success"}, "protected")
decision = live_gate_decision(
_claimed_green(), reader, "o/r", "sha1",
branch="main")
self.assertTrue(decision["allowed"])
self.assertFalse(decision["blocked"])
self.assertEqual(decision["http_status"], 200)
self.assertEqual(decision["status"], "allowed")
self.assertEqual(decision["code"], "live_evidence_verified")
def test_branch_defaults_to_ref_and_explicit_branch_wins(self):
reader = FakeReader({"ci": "success"}, "protected")
verify_live_evidence(_claimed_green(), reader, "o/r", "sha1")
self.assertIn(("protection", "o/r", "sha1"), reader.seen)
reader2 = FakeReader({"ci": "success"}, "protected")
verify_live_evidence(_claimed_green(), reader2, "o/r", "sha1",
branch="main")
self.assertIn(("protection", "o/r", "main"), reader2.seen)
def test_callable_legacy_reader_still_works(self):
def live_ok(repo, ref):
self.assertEqual((repo, ref), ("o/r", "sha1"))
return {"ci_checks": {"ci": "success"},
"branch_protection": "protected"}
result = verify_live_evidence(
_claimed_green(), live_ok, "o/r", "sha1")
self.assertTrue(result["verified"])
def live_bad(repo, ref):
return {"ci_checks": {"ci": "failure"},
"branch_protection": "protected"}
result = verify_live_evidence(
_claimed_green(), live_bad, "o/r", "sha1")
self.assertFalse(result["verified"])
self.assertTrue(any("ci_not_green_live" in m
for m in result["mismatches"]))
decision = live_gate_decision(
_claimed_green(), live_bad, "o/r", "sha1")
self.assertEqual(decision["http_status"], 409)
def test_no_real_github_call_in_production_module(self):
# 注意:旧模块头 docstring 本来就以文字描述过“真实
# reader 用 github_app 的 _github_api_request”(未来
# 设想,非代码),所以断言须区分文字与代码:代码
# 位置(import / 调用 / 非 docstring 字符串)里不
# 得出现真实调用,docstring 文字不算。
path = Path(__file__).resolve().parents[1] / "gate_verification.py"
tree = ast.parse(path.read_text())
imported = set()
docstring_nodes = set()
for node in ast.walk(tree):
if isinstance(node, (ast.Module, ast.ClassDef,
ast.FunctionDef,
ast.AsyncFunctionDef)):
body = node.body
if body and isinstance(body[0], ast.Expr) \
and isinstance(body[0].value, ast.Constant) \
and isinstance(body[0].value.value, str):
docstring_nodes.add(id(body[0].value))
if isinstance(node, ast.Import):
imported.update(a.name.split(".")[0]
for a in node.names)
elif isinstance(node, ast.ImportFrom):
if node.module:
imported.add(node.module.split(".")[0])
for banned in ("urllib", "requests", "http", "socket"):
self.assertNotIn(banned, imported,
"production module must not import %s"
% banned)
code_idents = set()
for node in ast.walk(tree):
if isinstance(node, ast.Attribute):
code_idents.add(node.attr)
elif isinstance(node, ast.Name):
code_idents.add(node.id)
elif isinstance(node, ast.Constant) \
and isinstance(node.value, str) \
and id(node) not in docstring_nodes:
self.assertNotIn("api.github.com", node.value)
self.assertNotIn("_github_api_request", code_idents)
if __name__ == "__main__":
unittest.main()