主角线:Guspu Frillyknots / Ral Fastenhatchets / Alath Blottedmine 由线索挖掘器自动选定,按因果链切集,经 5 维度自评闸门(≥12 分)后入库。
126 lines
4.6 KiB
Python
126 lines
4.6 KiB
Python
# 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())
|