"""三批基线闸门测试:fresh25 阈值边界 + historical11 固定预期表 + shadow20 达标断言(确定性,零模型)。 阈值严格按设计档 §5.3/§5.2/§5.4,本测试固化边界(19/25 拒 vs 20/25 过、品类 2/5 拒、成本 ¥1.5/¥15 临界、 假阳放行 0、needs_human 不自动 accept、tester_error=0、inconclusive≤1、固定六项零漂移、fresh25 硬前置)。 """ import asyncio import json import sys from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from baseline_gates import ( # noqa: E402 FRESH25_GENRES, build_shadow_plan, evaluate_fresh25_gate, evaluate_historical_gate, evaluate_shadow20_gate, load_historical_expectations, run_shadow_batch, ) # ────────────────────────── fresh25 ────────────────────────── def _fresh_rows(accepted_per_genre=(4, 4, 4, 4, 4), *, first_pass=18, repair=0, rescued=0, run_cost=1.0, chain_cost=5.0, drop=0, dup_gid=False): """按品类构造 25 局批结果行(gid/genre 与 hard_genre_batch 同 schema)。""" rows = [] idx = 0 for gi, genre in enumerate(FRESH25_GENRES): for rnd in range(1, 6): rows.append({ "gid": f"{genre}-r{rnd}", "genre": genre, "round": rnd, "accepted": rnd <= accepted_per_genre[gi], "firstPassAccepted": idx < first_pass, "repairAttempted": idx < repair, "rescuedByRoll": 2 if idx < rescued else None, "acceptedAfterRepair": False, "outcome": "accept" if rnd <= accepted_per_genre[gi] else "reject", "acceptanceCostRmb": run_cost, "parentChainCostRmb": chain_cost, }) idx += 1 if drop: rows = rows[:-drop] if dup_gid: rows[-1]["gid"] = rows[0]["gid"] return rows def test_fresh25_all_green_passes(): result = evaluate_fresh25_gate(_fresh_rows()) assert result["pass"] is True assert result["metrics"]["accepted"] == 20 assert all(c["pass"] for c in result["checks"]) def test_fresh25_boundary_19_of_25_rejected(): """accepted 19/25(4,4,4,4,3)→ 总数闸拒;品类 3/5 仍达标,只挂总数一项。""" result = evaluate_fresh25_gate(_fresh_rows(accepted_per_genre=(4, 4, 4, 4, 3))) assert result["pass"] is False by_name = {c["name"]: c for c in result["checks"]} assert by_name["acceptedTotal"]["pass"] is False assert by_name["acceptedTotal"]["actual"] == "19/25" assert by_name["perGenreMin"]["pass"] is True def test_fresh25_boundary_20_of_25_passes(): result = evaluate_fresh25_gate(_fresh_rows(accepted_per_genre=(4, 4, 4, 4, 4))) by_name = {c["name"]: c for c in result["checks"]} assert by_name["acceptedTotal"]["pass"] is True assert result["pass"] is True def test_fresh25_genre_2_of_5_rejected_even_with_20_total(): """总数 20/25 达标但某品类 2/5 → 品类闸拒(任一品类不低于 3/5)。""" result = evaluate_fresh25_gate(_fresh_rows(accepted_per_genre=(5, 5, 5, 3, 2))) assert result["pass"] is False by_name = {c["name"]: c for c in result["checks"]} assert by_name["acceptedTotal"]["pass"] is True # 总数 20 达标 assert by_name["perGenreMin"]["pass"] is False assert "sim-business" in by_name["perGenreMin"]["detail"] def test_fresh25_first_pass_boundary(): """firstPassAccepted 18 过 / 17 拒。""" assert evaluate_fresh25_gate(_fresh_rows(first_pass=18))["pass"] is True result = evaluate_fresh25_gate(_fresh_rows(first_pass=17)) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["firstPassAccepted"]["actual"] == "17/25" def test_fresh25_repair_rate_boundary(): """writer repair 启动 5 过 / 6 拒。""" assert evaluate_fresh25_gate(_fresh_rows(repair=5))["pass"] is True assert evaluate_fresh25_gate(_fresh_rows(repair=6))["pass"] is False def test_fresh25_rescued_boundary(): """rescuedByRoll 5 过 / 6 拒。""" assert evaluate_fresh25_gate(_fresh_rows(rescued=5))["pass"] is True assert evaluate_fresh25_gate(_fresh_rows(rescued=6))["pass"] is False def test_fresh25_run_cost_boundary(): """单局验收成本 ¥1.5 过 / ¥1.5001 拒。""" assert evaluate_fresh25_gate(_fresh_rows(run_cost=1.5))["pass"] is True result = evaluate_fresh25_gate(_fresh_rows(run_cost=1.5001)) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["perRunCost"]["pass"] is False def test_fresh25_chain_cost_boundary(): """parentRun 全链 ¥15 过 / ¥15.0001 拒。""" assert evaluate_fresh25_gate(_fresh_rows(chain_cost=15.0))["pass"] is True assert evaluate_fresh25_gate(_fresh_rows(chain_cost=15.0001))["pass"] is False def test_fresh25_missing_cost_fails_closed(): """缺验收成本 = 无法证明达标 → fail-closed 拒。""" rows = _fresh_rows() rows[3]["acceptanceCostRmb"] = None result = evaluate_fresh25_gate(rows) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["perRunCost"]["pass"] is False def test_fresh25_nan_cost_treated_as_missing(): rows = _fresh_rows() rows[0]["parentChainCostRmb"] = float("nan") assert evaluate_fresh25_gate(rows)["pass"] is False def test_fresh25_incomplete_sample_rejected(): """24 局(分母不完整)→ 样本闸拒,不能拿残缺基线冒充达标。""" result = evaluate_fresh25_gate(_fresh_rows(drop=1)) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["sampleSize"]["pass"] is False def test_fresh25_duplicate_gid_rejected(): """同 gid 重跑洗数字 → 样本闸拒(distinct gid 纪律)。""" result = evaluate_fresh25_gate(_fresh_rows(dup_gid=True)) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["sampleSize"]["pass"] is False def test_fresh25_accepted_after_repair_is_observation_not_numerator(): """acceptedAfterRepair 只作修复救回率观测,绝不当成功率分子。""" result = evaluate_fresh25_gate(_fresh_rows()) assert result["metrics"]["acceptedAfterRepair_observation"] == 0 assert "acceptedAfterRepair" not in {c["name"] for c in result["checks"]} # 不作闸门分子 # ────────────────────────── historical11 ────────────────────────── def _expected_matched_mapping(): """与固定预期表逐局相符的重放结果(needs_human 局给 inconclusive,仍挂起待定标)。""" return { "narrative-r1": "accept", "narrative-r2": "accept", "narrative-r3": "accept", "trpg-r1": "reject", "puzzle-r1": "inconclusive", "trpg-r2": "reject", "heritage-r2": "inconclusive", "sim-business-r2": "reject", "puzzle-r2": "reject", "heritage-r1": "inconclusive", "sim-business-r1": "inconclusive", } def _rows_from(mapping): return [{"gid": gid, "outcome": outcome} for gid, outcome in mapping.items()] def test_expectations_fixture_is_eleven_well_formed_games(): data = load_historical_expectations() rows = data["expectations"] assert len(rows) == 11 assert {r["expected"] for r in rows} <= {"accept", "reject", "not_accept", "needs_human"} categories = {r["category"]: sum(1 for x in rows if x["category"] == r["category"]) for r in rows} assert categories["positive_control"] == 3 # 3 narrative 正例 assert categories["known_false_positive"] == 5 # 5 旧假阳 assert categories["true_bug"] == 1 # puzzle-r2 assert categories["suspected_false_negative"] == 2 # 2 疑似假阴 def test_historical_matched_but_blocked_on_human_calibration(): """逐局相符 + needs_human 未定标 → 闸门不自动 PASS,挂起 2 局交创始人(红线)。""" result = evaluate_historical_gate(_rows_from(_expected_matched_mapping())) assert result["pass"] is False assert result["falsePositiveReleased"] == [] assert sorted(result["blockedOnHumanCalibration"]) == ["heritage-r1", "sim-business-r1"] by_name = {c["name"]: c for c in result["checks"]} assert by_name["falsePositiveRelease"]["pass"] is True assert by_name["perGameExpectation"]["pass"] is True assert by_name["humanCalibrationSettled"]["pass"] is False def test_historical_known_false_positive_accept_is_release_fail(): """旧假阳无新硬证被 accept = 确认假阳放行,闸门红线。""" mapping = _expected_matched_mapping() mapping["sim-business-r2"] = "accept" result = evaluate_historical_gate(_rows_from(mapping)) assert result["pass"] is False assert result["falsePositiveReleased"] == ["sim-business-r2"] assert {c["name"]: c for c in result["checks"]}["falsePositiveRelease"]["pass"] is False def test_historical_true_bug_accept_is_release_fail(): """puzzle-r2 真 bug 被 accept = 假阳放行。""" mapping = _expected_matched_mapping() mapping["puzzle-r2"] = "accept" result = evaluate_historical_gate(_rows_from(mapping)) assert "puzzle-r2" in result["falsePositiveReleased"] def test_historical_true_bug_inconclusive_is_mismatch(): """puzzle-r2 固定预期 reject,inconclusive 与预期不符(查验收器为何拿不到硬证)。""" mapping = _expected_matched_mapping() mapping["puzzle-r2"] = "inconclusive" result = evaluate_historical_gate(_rows_from(mapping)) assert result["pass"] is False by_name = {c["name"]: c for c in result["checks"]} assert by_name["perGameExpectation"]["pass"] is False assert "puzzle-r2" in by_name["perGameExpectation"]["detail"] def test_historical_positive_control_must_still_accept(): """narrative 正例应保留:v3 判 inconclusive = 验收器回退,逐局预期不符。""" mapping = _expected_matched_mapping() mapping["narrative-r1"] = "inconclusive" result = evaluate_historical_gate(_rows_from(mapping)) assert result["pass"] is False assert "narrative-r1" in {c["name"]: c for c in result["checks"]}["perGameExpectation"]["detail"] def test_historical_needs_human_accept_is_pre_calibration_fail(): """疑似假阴未定标即 accept → 定标前不得自动接受(红线)。""" mapping = _expected_matched_mapping() mapping["heritage-r1"] = "accept" result = evaluate_historical_gate(_rows_from(mapping)) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["preCalibrationAccept"]["pass"] is False def test_historical_tester_error_is_mismatch_not_evidence(): """tester_error 不构成证据 → 逐局不符,需重跑。""" mapping = _expected_matched_mapping() mapping["trpg-r1"] = "tester_error" result = evaluate_historical_gate(_rows_from(mapping)) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["perGameExpectation"]["pass"] is False def test_historical_missing_game_is_coverage_fail(): mapping = _expected_matched_mapping() del mapping["puzzle-r1"] result = evaluate_historical_gate(_rows_from(mapping)) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["coverage"]["pass"] is False def test_historical_fully_calibrated_can_auto_pass(): """创始人定标后(needs_human 改为固定预期)+ 逐局相符 → 闸门可自动 PASS。""" data = load_historical_expectations() calibrated = json.loads(json.dumps(data)) for row in calibrated["expectations"]: if row["expected"] == "needs_human": row["expected"] = "accept" # 真人定标结论:其实是好游戏 row["category"] = "calibrated" mapping = _expected_matched_mapping() mapping["heritage-r1"] = "accept" mapping["sim-business-r1"] = "accept" result = evaluate_historical_gate(_rows_from(mapping), expectations=calibrated, fresh25_gate={"pass": True}) assert result["pass"] is True assert result["blockedOnHumanCalibration"] == [] def test_historical_chain_blocked_by_failed_fresh25(): """链式校验:fresh25 未过 → warn + PASS 受阻(顺序建议,指标仍评估)。""" data = load_historical_expectations() calibrated = json.loads(json.dumps(data)) for row in calibrated["expectations"]: if row["expected"] == "needs_human": row["expected"] = "reject" mapping = _expected_matched_mapping() mapping["heritage-r1"] = "reject" mapping["sim-business-r1"] = "reject" result = evaluate_historical_gate(_rows_from(mapping), expectations=calibrated, fresh25_gate={"pass": False}) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["chainFresh25"]["pass"] is False assert any("fresh25" in w for w in result["warnings"]) # ────────────────────────── shadow20 ────────────────────────── _IDENTITY = {"commitHash": "c0ffee00", "chromeVersion": "126.0.6478.183", "actorModel": "MiniMax-M3", "judgeModel": "MiniMax-M3", "promptVersion": "3.0.14", "configSnapshotHash": "cfg-snap-01"} def _shadow_rows(n=20, **overrides_by_index): rows = [] for i in range(n): row = {"gid": f"shadow20-{i + 1:02d}", "outcome": "accept", "proofComplete": True, "confirmedFalsePositive": False, "problems": [], "contradictions": [], "proofMissing": False, "rescued": False, "discrepancy": False, "humanReviewed": True, "identity": dict(_IDENTITY)} row.update(overrides_by_index.get(i) or {}) rows.append(row) return rows _F25_PASS = {"pass": True} _F25_FAIL = {"pass": False} def test_shadow20_all_green_passes_with_fresh25_prereq(): result = evaluate_shadow20_gate(_shadow_rows(), fresh25_gate=_F25_PASS) assert result["pass"] is True assert all(c["pass"] for c in result["checks"]) def test_shadow20_fresh25_prereq_hard_required(): """fresh25 未提供 → 硬前置无法验证,PASS 阻断(§5.4 顺序硬要求)。""" result = evaluate_shadow20_gate(_shadow_rows(), fresh25_gate=None) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["prerequisiteFresh25"]["pass"] is False result2 = evaluate_shadow20_gate(_shadow_rows(), fresh25_gate=_F25_FAIL) assert result2["pass"] is False assert {c["name"]: c for c in result2["checks"]}["prerequisiteFresh25"]["actual"] == "fresh25 未达标" def test_shadow20_nineteen_samples_rejected(): result = evaluate_shadow20_gate(_shadow_rows(n=19), fresh25_gate=_F25_PASS) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["sampleSize"]["pass"] is False def test_shadow20_single_tester_error_rejected(): """20 局口径 <5% → tester_error 必须为 0:1 个即拒。""" rows = _shadow_rows() rows[7]["outcome"] = "tester_error" rows[7]["humanReviewed"] = True result = evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS) assert result["pass"] is False check = {c["name"]: c for c in result["checks"]}["testerError"] assert check["pass"] is False and check["actual"] == "1/20" def test_shadow20_inconclusive_boundary_one_ok_two_rejected(): """<10% → inconclusive 最多 1:1 个过、2 个拒。""" rows = _shadow_rows() rows[0]["outcome"] = "inconclusive" assert evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS)["pass"] is True rows[1]["outcome"] = "inconclusive" result = evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["inconclusive"]["actual"] == "2/20" def test_shadow20_confirmed_false_positive_rejected(): rows = _shadow_rows() rows[3]["confirmedFalsePositive"] = True result = evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["confirmedFalsePositives"]["detail"] == "shadow20-04" def test_shadow20_accepted_proof_incomplete_rejected(): """accepted proof 完整率必须 100%:一局不完整即拒。""" rows = _shadow_rows() rows[5]["proofComplete"] = False result = evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS) assert result["pass"] is False assert "shadow20-06" in {c["name"]: c for c in result["checks"]}["proofCompleteOnAccepted"]["detail"] def test_shadow20_accepted_with_conflict_rejected(): """accepted 与 problems/缺证/矛盾共存 0:三种脏状态各测一遍。""" for field, value in (("problems", ["结论与截图矛盾"]), ("contradictions", ["事件序倒序"]), ("proofMissing", True)): rows = _shadow_rows() rows[2][field] = value result = evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS) assert result["pass"] is False, f"{field} 共存未拦" assert {c["name"]: c for c in result["checks"]}["acceptedWithoutConflict"]["pass"] is False def test_shadow20_identity_drift_rejected(): """固定六项漂移即拒(shadow 可比性前提)。""" rows = _shadow_rows() rows[9]["identity"] = {**_IDENTITY, "commitHash": "drifted1"} result = evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["identityFixed"]["pass"] is False def test_shadow20_identity_drift_against_plan_rejected(): plan = build_shadow_plan([f"prompt-{i}" for i in range(20)], commit_hash=_IDENTITY["commitHash"], chrome_version=_IDENTITY["chromeVersion"], actor_model=_IDENTITY["actorModel"], judge_model=_IDENTITY["judgeModel"], prompt_version=_IDENTITY["promptVersion"], config_snapshot_hash=_IDENTITY["configSnapshotHash"]) rows = _shadow_rows() rows[0]["identity"] = {**_IDENTITY, "promptVersion": "3.0.13"} # 与 plan 不一致 result = evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS, plan=plan) assert result["pass"] is False def test_shadow20_human_review_coverage_enforced(): """分歧/reject/inconclusive/tester_error 必查 + 普通 accept 至少抽 5。""" rows = _shadow_rows() rows[4]["outcome"] = "reject" rows[4]["humanReviewed"] = False # 必查局未复核 result = evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS) assert result["pass"] is False assert {c["name"]: c for c in result["checks"]}["humanReviewCoverage"]["pass"] is False # accept 抽查不足 5:6 个 accept 只复核 1 个 rows2 = _shadow_rows() for i in range(14): rows2[i]["outcome"] = "reject" # 14 reject(全已复核)+ 6 accept for i in range(14, 19): rows2[i]["humanReviewed"] = False # 6 accept 中 5 个未复核 → 只 1 个 < 5 result2 = evaluate_shadow20_gate(rows2, fresh25_gate=_F25_PASS) assert result2["pass"] is False def test_shadow20_plan_builder_validates_fixed_identity(): prompts = [f"prompt-{i}" for i in range(20)] plan = build_shadow_plan(prompts, commit_hash="c1", chrome_version="126", actor_model="MiniMax-M3", judge_model="MiniMax-M3", prompt_version="3.0.14", config_snapshot_hash="cfg1") assert plan["acceptanceMode"] == "v3_shadow" assert len(plan["prompts"]) == 20 with pytest.raises(ValueError, match="≥20"): build_shadow_plan(prompts[:19], commit_hash="c1", chrome_version="126", actor_model="m", judge_model="m", prompt_version="p", config_snapshot_hash="c") with pytest.raises(ValueError, match="不得为空"): build_shadow_plan(prompts, commit_hash="", chrome_version="126", actor_model="m", judge_model="m", prompt_version="p", config_snapshot_hash="c") def test_shadow20_runner_framework_stamps_identity_serially(): """runner 框架:串行调用注入的真跑 callable,每局盖固定身份戳(罐头 run_one,零模型)。""" plan = build_shadow_plan([f"p{i}" for i in range(20)], commit_hash="c1", chrome_version="126", actor_model="MiniMax-M3", judge_model="MiniMax-M3", prompt_version="3.0.14", config_snapshot_hash="cfg1") calls = [] async def _fake_run_one(prompt, identity): calls.append(prompt) return {"outcome": "accept", "proofComplete": True, "humanReviewed": True} rows = asyncio.run(run_shadow_batch(plan, run_one=_fake_run_one)) assert len(rows) == 20 and len(calls) == 20 assert all(row["identity"]["commitHash"] == "c1" for row in rows) assert [row["gid"] for row in rows[:2]] == ["shadow20-01", "shadow20-02"] # 框架产出的行直接可进闸门(全绿 + fresh25 前置 → PASS) assert evaluate_shadow20_gate(rows, fresh25_gate=_F25_PASS, plan=plan)["pass"] is True def test_shadow20_runner_rejects_malformed_plan(): with pytest.raises(ValueError): asyncio.run(run_shadow_batch({"prompts": "not-a-list"}, run_one=lambda p, i: {}))