games-development-ai/cheap-worker/tests/test_golden_specs.py
lili db4aadd785
Some checks failed
docs-gate / docs-gate (push) Has been cancelled
merge: W-GENRE 解谜品类四件套(skill puzzle-game-design + _template-puzzle 9/9 + _fewshot-puzzle 正例 + rubric fixture/金标 spec + 范例签名机器门)——品类路由并集 v1.4.0(剧情6/TRPG7/非遗8/解谜9)
合并裁定(rubric 挂点两套实现收敛,fable 亲审):评分尺主干 = TRPG 机制(单 checks 数组契约+嵌套
richness.genre,19 单测钉住);从分支嫁接 fixtures/genre-rubrics/<genre>.json 加载器为第二来源
(内联 GENRE_CHECKLISTS 优先、fixture 兜底,puzzle.json 即刻生效);分支平铺 genreHits/genreChecks
双数组契约弃用,其接线测试适配主干形状。run_studio 增显式 genre= 参数(None→按模板推断,原行为不变)。
GENRE_BY_TEMPLATE += _template-puzzle。顺手补账:.agents/README 索引四个品类 skill(前三次 merge 欠账)。

验收(本机):金标 7 品类 6/6 + cheap_verify 19/19 + 解谜资产门 6/6(含 node scaffold/check 真 shell-out)
+ 模板 node 13/13 + check_registry 17 条 + node --check/py_compile 全过;test_roles 2 红=主干既有基线
(回落分叉,归一单已排)。

诚实记录:分支验收唯「n=3-5 小批全过门」BLOCKED(4/7;三失败=AI 大面重写引入运行时 bug,九门正确
拦截,根因已回写 skill §9 品类红线;spike 路无续修环是结构缺口,生产路 RepairMiddleware 覆盖)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:48:41 -07:00

102 lines
5.3 KiB
Python

"""
test_golden_specs.py — U1 三品类金标 play-spec fixtures 校验(静态结构,不依赖真跑)。
只验金标 spec 自身正确:schema 合法 / 品类对位(三类同属 occupied 反应族)/ 契约依赖(只引 forensics
契约字段、不绑定具体游戏坐标)。**不验"现有样本能被驱动"**——那在 U3 实跑、按 R3 判(plan U1 Test expectation)。
跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_golden_specs.py
"""
import json
import sys
from pathlib import Path
_FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "golden-specs"
# tap-targets 代表品类:三个基线品类(plan KTD2,各 formalize base1/base2/base4 的 occupied spec)
# + W-GENRE 品类件④ 追加(story-branch=_template-story / trpg=_template-trpg / heritage-craft=_template-feiyi /
# puzzle=_template-puzzle;新品类进金标须同步本期望集)。
_EXPECTED_GENRES = {"click-score", "whack-mole", "shop-serve", "story-branch", "trpg", "heritage-craft", "puzzle"}
# driver 只许这些键 —— 全是与具体游戏无关的契约级参数;出现坐标类键(x/y/coords/points)即"绑定具体游戏"违规。
_ALLOWED_DRIVER_KEYS = {"type", "targetMode", "targetsPath", "steps", "stepMs"}
_FORBIDDEN_COORD_KEYS = {"x", "y", "coords", "points", "positions", "targets"}
def _load_specs() -> dict:
"""加载 fixtures/golden-specs/*.play-spec.json → {genre: spec}。"""
out = {}
for p in sorted(_FIXTURES.glob("*.play-spec.json")):
spec = json.loads(p.read_text(encoding="utf-8"))
out[spec.get("_genre") or p.stem] = spec
return out
def test_three_genres_present():
"""tap-targets 代表品类集合恰好对位(无遗漏、无多余;新品类进金标须同步 _EXPECTED_GENRES)。"""
specs = _load_specs()
assert set(specs) == _EXPECTED_GENRES, f"品类集不符:{set(specs)} != {_EXPECTED_GENRES}"
def test_schema_tap_targets_occupied():
"""schema 合法:每份都是 tap-targets + targetMode=occupied + 必填字段在。"""
for genre, spec in _load_specs().items():
d = spec.get("driver") or {}
assert d.get("type") == "tap-targets", f"{genre}: driver.type 应为 tap-targets,得 {d.get('type')}"
assert d.get("targetMode") == "occupied", f"{genre}: targetMode 应为 occupied,得 {d.get('targetMode')}"
assert d.get("targetsPath") == "targets", f"{genre}: targetsPath 应为 targets,得 {d.get('targetsPath')}"
assert isinstance(d.get("steps"), int) and d["steps"] > 0, f"{genre}: steps 缺/非正"
assert isinstance(d.get("stepMs"), int) and d["stepMs"] > 0, f"{genre}: stepMs 缺/非正"
def test_score_increased_assertion():
"""品类对位:每份均带 score 递增进度断言(occupied 反应族的可见成长口径)。"""
for genre, spec in _load_specs().items():
asserts = spec.get("assertAfterPlay") or []
score_inc = [a for a in asserts if a.get("path") == "score" and a.get("op") == "increased"]
assert score_inc, f"{genre}: 缺 score increased 进度断言"
assert spec.get("expectLatch") is True, f"{genre}: expectLatch 应为 true(限时类有进展降 advisory)"
def test_no_other_driver_families():
"""有意不产放置族 / key-cycle / safeOnly —— 三类同族,别的家族在五品类无消费者(F5/C2/O2)。"""
for genre, spec in _load_specs().items():
d = spec.get("driver") or {}
assert d.get("type") != "key-cycle", f"{genre}: 不应是 key-cycle(延到输入键契约)"
assert d.get("targetMode") != "safe" and not d.get("safeOnly"), f"{genre}: 不应是规避 safeOnly 族"
# 放置族 = occupied 缺省/点空位;本计划三类全 occupied,不允许其它 targetMode。
assert d.get("targetMode") == "occupied", f"{genre}: 不应是放置族,应 occupied 反应族"
def test_contract_only_no_hardcoded_coords():
"""契约依赖:driver 只含契约级参数,不含坐标类键(不绑定具体游戏);断言路径只引 forensics 字段。"""
for genre, spec in _load_specs().items():
d = spec.get("driver") or {}
extra = set(d) - _ALLOWED_DRIVER_KEYS
assert not extra, f"{genre}: driver 含非契约键 {extra}(疑似绑定具体游戏)"
coord = set(d) & _FORBIDDEN_COORD_KEYS
assert not coord, f"{genre}: driver 含坐标类键 {coord}(违反'不绑定具体游戏坐标')"
for a in spec.get("assertAfterPlay") or []:
assert a.get("path") in ("score", "phase"), f"{genre}: 断言路径 {a.get('path')} 非 forensics 契约字段"
def test_click_score_is_occupied_not_placement():
"""base1 同族:点击得分按 occupied 反应族写(非放置族)——C2/O2 修正。"""
specs = _load_specs()
cs = specs.get("click-score")
assert cs is not None, "缺 click-score 金标 spec"
assert cs["driver"]["targetMode"] == "occupied", "点击得分应 occupied(base1 targets 即 occupied:true),非放置族"
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)