- cheap_genre_route:纯词表路由(零 LLM/零 IO/确定性),规则表数据化单源(_GENRE_RULES 顺序即先验序: 非遗>TRPG>经营>解谜>剧情,特异词前泛词后);映射对齐 GENRE_BY_TEMPLATE 五类模板 - cheap_service_driver:取代硬编码 scaffold_template=None——命中选 per-genre 黄金骨架+genre 透传丰富度评分 (生产 create 路与 S5 bake_off 复验同轨);无命中 (None,None) 走通用 _template(路由只加不减) - tests/test_genre_route:五类命中/无命中回退/多类共现先验序/GENRE_BY_TEMPLATE 一致性/确定性/脏输入 14 用例 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
114 lines
4.8 KiB
Python
114 lines
4.8 KiB
Python
"""test_genre_route.py — brief→genre 确定性关键词路由单测(T4 生产 create 路品类接线)。
|
|
|
|
守的不变量:五类各自命中且映射到 GENRE_BY_TEMPLATE 登记的模板;无命中回退 (None, None)(=旧通用行为);
|
|
多类词共现取先验序(特异性高者胜);空/脏输入不炸;确定性(同 brief 恒同结果)。
|
|
|
|
跑:cheap-worker/.venv/bin/python -m pytest cheap-worker/tests/test_genre_route.py -v
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/
|
|
from cheap_genre_route import route_genre # noqa: E402
|
|
from cheap_verify import GENRE_BY_TEMPLATE # noqa: E402
|
|
|
|
|
|
# ───────────────────────── 五类命中 ─────────────────────────
|
|
|
|
def test_sim_business_hits():
|
|
for brief in ("经营一家奶茶店", "开店卖包子招待顾客", "餐厅上菜小游戏", "收银台接客人"):
|
|
t, g = route_genre(brief)
|
|
assert (t, g) == ("_template-shop", "sim-business"), brief
|
|
|
|
|
|
def test_narrative_hits():
|
|
for brief in ("一个多结局的文字冒险", "互动小说,对话选择推动剧情", "分支叙事小游戏"):
|
|
t, g = route_genre(brief)
|
|
assert (t, g) == ("_template-story", "narrative"), brief
|
|
|
|
|
|
def test_trpg_hits():
|
|
for brief in ("跑团风格的冒险", "掷骰过检定的 TRPG", "地下城探险,骰子决定命运"):
|
|
t, g = route_genre(brief)
|
|
assert (t, g) == ("_template-trpg", "trpg"), brief
|
|
|
|
|
|
def test_heritage_hits():
|
|
for brief in ("剪纸主题小游戏", "体验非遗皮影戏", "做一款刺绣传统工艺游戏"):
|
|
t, g = route_genre(brief)
|
|
assert (t, g) == ("_template-feiyi", "heritage"), brief
|
|
|
|
|
|
def test_puzzle_hits():
|
|
for brief in ("华容道解谜", "一个密室逃脱谜题", "数独小游戏"):
|
|
t, g = route_genre(brief)
|
|
assert (t, g) == ("_template-puzzle", "puzzle"), brief
|
|
|
|
|
|
# ───────────────────────── 无命中回退(=旧通用行为)─────────────────────────
|
|
|
|
def test_no_hit_falls_back_to_none():
|
|
for brief in ("一个简单的点击得分小游戏", "接住掉落的水果", "打砖块"):
|
|
assert route_genre(brief) == (None, None), brief
|
|
|
|
|
|
def test_empty_and_dirty_input():
|
|
for brief in (None, "", 123, [], {}):
|
|
assert route_genre(brief) == (None, None)
|
|
|
|
|
|
# ───────────────────────── 多类共现取先验序 ─────────────────────────
|
|
|
|
def test_cooccurrence_specific_beats_generic():
|
|
"""经营词 + 剧情泛词共现 → 判经营(特异性高者定品类,泛词只是修饰;先验序=规则表序)。"""
|
|
t, g = route_genre("经营一家小店,每位顾客都有自己的故事")
|
|
assert (t, g) == ("_template-shop", "sim-business")
|
|
|
|
|
|
def test_cooccurrence_heritage_beats_business():
|
|
"""非遗词 + 经营词共现 → 判非遗(非遗特异性序最前:『经营剪纸铺』的灵魂是剪纸)。"""
|
|
t, g = route_genre("经营一家剪纸铺,传承传统手艺")
|
|
assert (t, g) == ("_template-feiyi", "heritage")
|
|
|
|
|
|
def test_cooccurrence_trpg_beats_narrative():
|
|
t, g = route_genre("跑团故事:骰子决定每章剧情走向")
|
|
assert (t, g) == ("_template-trpg", "trpg")
|
|
|
|
|
|
# ───────────────────────── 一致性 ─────────────────────────
|
|
|
|
def test_templates_registered_in_genre_by_template():
|
|
"""路由产出的每个模板都必须在 GENRE_BY_TEMPLATE 登记,且 genre 键一致(不造孤儿模板/键漂移)。"""
|
|
from cheap_genre_route import _GENRE_RULES
|
|
|
|
for genre, template, _ in _GENRE_RULES:
|
|
assert GENRE_BY_TEMPLATE.get(template) == genre, \
|
|
f"规则表 {template}→{genre} 与 GENRE_BY_TEMPLATE({GENRE_BY_TEMPLATE.get(template)})漂移"
|
|
|
|
|
|
def test_deterministic():
|
|
"""确定性:同 brief 重复路由恒同结果(生产路由必须可复验)。"""
|
|
brief = "经营奶茶店的故事"
|
|
assert all(route_genre(brief) == route_genre(brief) for _ in range(5))
|
|
|
|
|
|
def test_case_insensitive():
|
|
assert route_genre("一场 DND 冒险")[1] == "trpg"
|
|
assert route_genre("一场 dnd 冒险")[1] == "trpg"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
|
_failed = 0
|
|
for _fn in _fns:
|
|
try:
|
|
_fn()
|
|
print(f" PASS {_fn.__name__}")
|
|
except Exception as e: # noqa: BLE001
|
|
_failed += 1
|
|
print(f" FAIL {_fn.__name__}: {type(e).__name__}: {e}")
|
|
print(f"\n{len(_fns) - _failed}/{len(_fns)} passed")
|
|
sys.exit(1 if _failed else 0)
|