第 1 卷(人物主线):第 1–3 集 + 人物表

主角线:Guspu Frillyknots / Ral Fastenhatchets / Alath Blottedmine
由线索挖掘器自动选定,按因果链切集,经 5 维度自评闸门(≥12 分)后入库。
This commit is contained in:
Chen Yi
2026-10-05 19:28:49 +08:00
parent 594cf48a1a
commit e0073e90bd
24 changed files with 2070 additions and 79 deletions
+125
View File
@@ -0,0 +1,125 @@
# 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())