Files
dwarf-fortress-annals/tests/test_episodes.py
T
Chen Yi e0073e90bd 第 1 卷(人物主线):第 1–3 集 + 人物表
主角线:Guspu Frillyknots / Ral Fastenhatchets / Alath Blottedmine
由线索挖掘器自动选定,按因果链切集,经 5 维度自评闸门(≥12 分)后入库。
2026-10-05 19:28:49 +08:00

120 lines
4.4 KiB
Python

# pyright: reportMissingImports=false, reportAttributeAccessIssue=false
# 说明:LSP 的 Python 环境看不到本项目包,会把包内 import 误报为缺失模块。
"""因果链切集的回归测试。
对应 t3 契约:间隔 >2 年断开;短链只合并、不设总量下限;过长按预算拆上下集;
每集预算 800–1500 字;输出含集号/年份范围/事件条数/主角与对手。
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from collections import Counter
from dfannals import episodes
from dfannals.legends import Event, Figure, World
from dfannals.threads import Thread
def ev(eid: int, year: int, etype: str = "add hf hf link") -> Event:
return Event(id=eid, year=year, seconds72=0, type=etype,
fields={"hfid": ["1"], "hfid_target": ["2"]})
def fake_thread(events: list[Event]) -> Thread:
return Thread(
members=[1, 2], interactions=len(events), turns=0,
span=(events[-1].year - events[0].year) if events else 0,
first_year=events[0].year if events else 0,
last_year=events[-1].year if events else 0,
types=Counter(), races=Counter(), non_person_races=[], events=events,
)
def test_gap_breaks_chain() -> None:
"""间隔 >2 年必须断开成两条链。"""
events = [ev(i, y) for i, y in enumerate([10, 11, 12, 20, 21, 22])]
chains = episodes._split_chains(events, gap_years=2)
assert len(chains) == 2, [len(c) for c in chains]
assert [len(c) for c in chains] == [3, 3]
def test_small_gap_does_not_break() -> None:
"""间隔恰好 2 年不该断开。"""
chains = episodes._split_chains([ev(1, 10), ev(2, 12)], gap_years=2)
assert len(chains) == 1
def test_long_chain_splits_into_budgeted_parts() -> None:
"""过长链路必须拆成上/下集,且每段都不跌破下限。"""
events = [ev(i, 100 + i) for i in range(30)]
parts = episodes._split_long(events)
assert len(parts) >= 2, "30 条事件没有拆集"
for p in parts:
assert episodes._min_events() <= len(p) <= episodes._max_events(), [len(x) for x in parts]
assert sum(len(p) for p in parts) == 30
def test_split_never_creates_undersized_fragments() -> None:
"""回归:14 条事件曾被均分成 7+7,两段都跌破 800 字下限。"""
events = [ev(i, 200 + i) for i in range(14)]
eps = episodes.plan_episodes(fake_thread(events))
assert len(eps) == 1, f"14 条事件被拆成了 {len(eps)} 集"
assert episodes.MIN_CHARS <= eps[0].estimated_chars <= episodes.MAX_CHARS
def test_short_chain_is_merged_not_dropped() -> None:
"""短链只合并,不设线索总量下限:2 条事件也要能成一集。"""
eps = episodes.plan_episodes(fake_thread([ev(1, 50), ev(2, 51)]))
assert len(eps) == 1
assert len(eps[0].events) == 2
def test_episode_key_marks_parts() -> None:
events = [ev(i, 300 + i) for i in range(30)]
eps = episodes.plan_episodes(fake_thread(events))
assert len(eps) >= 2
assert all(e.parts == len(eps) for e in eps)
assert eps[0].key.endswith("-p1") and eps[1].key.endswith("-p2")
assert eps[0].key != eps[1].key
def test_plan_reports_heroes_and_others() -> None:
"""输出必须能区分主角与对手。"""
world = World(name="test")
for fid, name in ((1, "Hero"), (2, "Ally"), (9, "Rival")):
world.figures[fid] = Figure(id=fid, name=name)
events = [ev(i, 400 + i) for i in range(3)]
events.append(Event(id=99, year=403, seconds72=0, type="hf wounded",
fields={"woundee_hfid": ["1"], "wounder_hfid": ["9"]}))
thread = fake_thread(events)
planned = episodes.plan_episodes(thread)
heroes, others = episodes.cast_of(world, thread, planned[0])
assert 1 in heroes and 2 in heroes
assert 9 in others and 9 not in heroes
text = episodes.render_plan(world, thread, planned)
assert "主角" in text and "对手/外人" in text and "Rival" in text
def _main() -> int:
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
failed = 0
for fn in tests:
try:
fn()
print(f" ✓ {fn.__name__}")
except AssertionError as exc:
failed += 1
print(f" ✗ {fn.__name__}: {exc}")
print(f"\n{len(tests) - failed}/{len(tests)} 通过")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(_main())