第 1 卷(人物主线):第 1–3 集 + 人物表
主角线:Guspu Frillyknots / Ral Fastenhatchets / Alath Blottedmine 由线索挖掘器自动选定,按因果链切集,经 5 维度自评闸门(≥12 分)后入库。
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# 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())
|
||||
Reference in New Issue
Block a user