# pyright: reportMissingImports=false, reportAttributeAccessIssue=false # 说明:LSP 的 Python 环境看不到本项目包,会把包内 import 误报为缺失模块。 """闸门流程的回归测试(不调用真实模型,用假评分器)。 对应 t4 契约:<12 先重写一次、仍不合格才换线索、失败上限、换线原因写入 notes/。 真实模型能否真的判出平淡,由 scripts/gate-selfcheck.py 负责证明。 """ from __future__ import annotations import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from dfannals import gate # noqa: E402 def verdict(total: int, reason: str = "测试用") -> gate.Verdict: """按总分构造一个评分(维度均分,保持与真实结构一致)。""" keys = [k for k, _, _ in gate.DIMENSIONS] per, rest = divmod(total, len(keys)) scores = {k: min(gate.MAX_SCORE, per + (1 if i < rest else 0)) for i, k in enumerate(keys)} return gate.Verdict(total=sum(scores.values()), scores=scores, reason=reason) def test_passes_first_try() -> None: calls: list[int] = [] def write(attempt: int, _prev) -> str: calls.append(attempt) return "好稿" text, run = gate.gate_episode("001", write, evaluate_fn=lambda _t: verdict(20)) assert text == "好稿" assert run.accepted and run.rewrites == 0 assert calls == [0], "通过后不应重写" def test_rewrite_once_then_pass() -> None: """<12 分先重写一次,重写后合格就通过。""" seq = [verdict(5), verdict(18)] seen: list[int] = [] def write(attempt: int, prev) -> str: seen.append(attempt) if attempt == 1: assert prev is not None and prev.total == 5, "重写时要带上上一稿评分" return f"第{attempt}稿" text, run = gate.gate_episode("002", write, evaluate_fn=lambda _t: seq.pop(0)) assert text == "第1稿" and run.accepted assert run.rewrites == 1 and seen == [0, 1] assert [v.total for v in run.verdicts] == [5, 18] def test_not_accepted_after_max_rewrites() -> None: """一直不合格:重写次数不得越过上限,且标记为未通过。""" runs: list[int] = [] text, run = gate.gate_episode( "003", lambda attempt, _p: (runs.append(attempt), "稿")[1], evaluate_fn=lambda _t: verdict(3), ) assert not run.accepted assert run.rewrites == gate.MAX_REWRITES assert runs == [0, 1], f"实际尝试次数:{runs}" assert len(run.verdicts) == gate.MAX_REWRITES + 1 def test_render_shows_weak_dimensions() -> None: v = verdict(6, "全是重复对战") text = v.render() assert "判平淡" in text and "全是重复对战" in text and "6/" in text assert len(v.weak()) == 2 def test_switch_note_records_reason(tmp_path: Path) -> None: """放弃线索必须记下原因,且写入仓库 notes/ 附录。""" path = gate.append_switch_note( tmp_path, world_name="Mon Sagus", members=["Guspu Frillyknots", "Ral Fastenhatchets"], span="193–239(46 年)", verdict=verdict(5, "只有阴谋事件重复,没有立场变化"), reason="重写一次后仍判平淡", ) assert path.exists() and path.parent.name == "notes" body = path.read_text(encoding="utf-8") assert "Guspu Frillyknots" in body and "重写一次后仍判平淡" in body assert "只有阴谋事件重复" in body def test_append_switch_note_is_append_only(tmp_path: Path) -> None: gate.append_switch_note(tmp_path, world_name="W", members=["A"], span="1–2", verdict=verdict(4), reason="第一次放弃") gate.append_switch_note(tmp_path, world_name="W", members=["B"], span="3–4", verdict=verdict(4), reason="第二次放弃") body = (tmp_path / "notes" / gate.SWITCH_LOG).read_text(encoding="utf-8") assert "第一次放弃" in body and "第二次放弃" in body assert body.count("# 被放弃的线索") == 1, "表头只应写一次" def _main() -> int: import tempfile plain = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)] failed = 0 for fn in plain: try: if fn.__code__.co_argcount: with tempfile.TemporaryDirectory() as d: fn(Path(d)) else: fn() print(f" ✓ {fn.__name__}") except AssertionError as exc: failed += 1 print(f" ✗ {fn.__name__}: {exc}") print(f"\n{len(plain) - failed}/{len(plain)} 通过") return 1 if failed else 0 if __name__ == "__main__": sys.exit(_main())