M2 auto-vs-golden 门抓到的生成契约漂移修复(Codex 实现 + 主代理诊断/受控验证)。 根因:~20% click-score 款 _forensicsView 暴露单数 target(对象、菜单态为 null) 而非复数 targets(数组),tap-targets harness 只认复数数组:① _build_play_spec 推断漏判→误推 key-cycle;② 驱动器 targetsPath 找不到单数 target。 两处修(认两种合法形态、既有数组行为零变): - cheap_run.py:_build_play_spec 以【键存在性】判 tap-targets(targets/target 键 在即可,菜单态 null/空数组仍归 tap-targets)——非值类型(空数组过/null 不过的 非对称是漏判根因)。 - play.cdp.cjs:runTapTargets 取值非数组时回退读单数 target、包成 1 元素数组。 验证:退化款 avg-click-score-14 auto 路 E_live/G_input/H_progress 0.667→1.0 (3/3 确定性复现);test_ensure_play_spec 8→13 绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
156 lines
7.7 KiB
Python
156 lines
7.7 KiB
Python
"""
|
|
test_ensure_play_spec.py — 便宜档自动 play-spec 加厚到金标同质单测(M1 U2)。
|
|
|
|
守的不变量:
|
|
· tap-targets occupied 族:自动 spec 补 expectedEngineCallPrefixes,且与三份金标 fixtures 逐字段一致(同质)。
|
|
· 防自证:expectedEngineCallPrefixes 是【外生预置常量】,不随该局 state 实际内容(targets/score)变——
|
|
绝不从实际调用反推,否则 F_wiring 退化自证、必过、假绿(gen-path-parity skill 铁律)。
|
|
· key-cycle 族:薄版,不产 expectedEngineCallPrefixes(待输入键契约,WU-C 5.4)。
|
|
· ensure_play_spec 已存在不覆盖(不踩既有金标 fixtures)。
|
|
|
|
跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_ensure_play_spec.py
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/
|
|
import cheap_run as R # noqa: E402
|
|
|
|
_GOLDEN_PREFIXES = ["sessionScore.", "audioMusic.", "juice."]
|
|
_GOLDEN_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "golden-specs"
|
|
|
|
|
|
# ───────────────────────── tap-targets 加厚到金标同质 ─────────────────────────
|
|
|
|
def test_tap_targets_adds_golden_prefixes():
|
|
spec = R._build_play_spec({"phase": "play", "score": 5, "targets": [{"x": 1, "y": 1, "occupied": True}]})
|
|
assert spec["driver"]["type"] == "tap-targets"
|
|
assert spec["expectedEngineCallPrefixes"] == _GOLDEN_PREFIXES
|
|
|
|
|
|
def test_singular_target_object_uses_tap_targets():
|
|
"""单数 target 对象是合法点击目标形态,应走 tap-targets 驱动。"""
|
|
spec = R._build_play_spec({"target": {"x": 1, "y": 2, "r": 3, "occupied": True}, "phase": "play"})
|
|
assert spec["driver"]["type"] == "tap-targets"
|
|
|
|
|
|
def test_singular_target_null_menu_state_uses_tap_targets():
|
|
"""菜单态 target 可为 null,但暴露 target 键即声明点击目标类。"""
|
|
spec = R._build_play_spec({"target": None, "phase": "menu", "score": 0})
|
|
assert spec["driver"]["type"] == "tap-targets"
|
|
|
|
|
|
def test_targets_array_uses_tap_targets():
|
|
"""复数 targets 数组保持既有 tap-targets 驱动行为。"""
|
|
spec = R._build_play_spec({"targets": [{"x": 1, "y": 2, "r": 3, "occupied": True}], "phase": "play"})
|
|
assert spec["driver"]["type"] == "tap-targets"
|
|
|
|
|
|
def test_empty_targets_array_still_uses_tap_targets():
|
|
"""空 targets 数组仍代表点击类导出形态,不可退回 key-cycle。"""
|
|
spec = R._build_play_spec({"targets": [], "phase": "play"})
|
|
assert spec["driver"]["type"] == "tap-targets"
|
|
|
|
|
|
def test_matches_golden_fixtures():
|
|
"""加厚后的 prefixes 与三份金标 fixtures 的 expectedEngineCallPrefixes 逐字段一致(同质)。"""
|
|
spec = R._build_play_spec({"targets": []}) # targets=[] 仍是 list → tap-targets
|
|
for fn in ("whack-mole", "click-score", "shop-serve"):
|
|
golden = json.loads((_GOLDEN_DIR / f"{fn}.play-spec.json").read_text(encoding="utf-8"))
|
|
assert spec["expectedEngineCallPrefixes"] == golden["expectedEngineCallPrefixes"], f"{fn} 前缀与金标不一致"
|
|
|
|
|
|
def test_assertions_match_golden():
|
|
"""driver 形态 + assertAfterPlay(op/path)+ expectLatch 与金标一致(tap-targets occupied 族同质核心)。"""
|
|
spec = R._build_play_spec({"targets": [{"occupied": True}]})
|
|
golden = json.loads((_GOLDEN_DIR / "whack-mole.play-spec.json").read_text(encoding="utf-8"))
|
|
assert spec["driver"]["type"] == golden["driver"]["type"] == "tap-targets"
|
|
assert spec["driver"]["targetMode"] == golden["driver"]["targetMode"] == "occupied"
|
|
assert spec["assertAfterPlay"][0]["path"] == golden["assertAfterPlay"][0]["path"] == "score"
|
|
assert spec["assertAfterPlay"][0]["op"] == golden["assertAfterPlay"][0]["op"] == "increased"
|
|
assert spec["expectLatch"] == golden["expectLatch"] is True
|
|
|
|
|
|
# ───────────────────────── 防自证:期望基准外生 ─────────────────────────
|
|
|
|
def test_prefixes_are_external_constant_not_derived():
|
|
"""防自证核心:两个 targets 内容/score 完全不同的局 → expectedEngineCallPrefixes 恒等(外生常量,不反推实际)。"""
|
|
s1 = {"phase": "play", "score": 1, "targets": [{"x": 1, "y": 1, "occupied": True}]}
|
|
s2 = {"phase": "play", "score": 888, "targets": [{"x": 9, "y": 9, "occupied": False},
|
|
{"x": 3, "y": 2, "occupied": True}]}
|
|
p1 = R._build_play_spec(s1)["expectedEngineCallPrefixes"]
|
|
p2 = R._build_play_spec(s2)["expectedEngineCallPrefixes"]
|
|
assert p1 == p2 == _GOLDEN_PREFIXES, "期望前缀必须外生恒定,不随该局 state 实际内容变(防 F_wiring 自证)"
|
|
|
|
|
|
# ───────────────────────── key-cycle 薄版 ─────────────────────────
|
|
|
|
def test_key_cycle_thin_no_prefixes():
|
|
spec = R._build_play_spec({"phase": "play", "score": 0}) # 无 targets → key-cycle
|
|
assert spec["driver"]["type"] == "key-cycle"
|
|
assert "expectedEngineCallPrefixes" not in spec, "key-cycle 薄版不应加厚 prefixes(待输入键契约)"
|
|
assert "WU-C 5.4" in spec.get("_note", ""), "key-cycle 薄版应标注延后原因"
|
|
|
|
|
|
def test_state_without_target_keys_is_key_cycle_thin():
|
|
"""没有 targets/target 键时保持按键类薄版回退。"""
|
|
spec = R._build_play_spec({"phase": "play", "score": 0})
|
|
assert spec["driver"]["type"] == "key-cycle"
|
|
|
|
|
|
def test_none_state_is_key_cycle_thin():
|
|
spec = R._build_play_spec(None) # None → 保守按键类
|
|
assert spec["driver"]["type"] == "key-cycle"
|
|
assert "expectedEngineCallPrefixes" not in spec
|
|
|
|
|
|
# ───────────────────────── ensure_play_spec 已存在不覆盖 ─────────────────────────
|
|
|
|
def test_ensure_play_spec_no_overwrite():
|
|
"""已存在不覆盖:保护既有(金标)fixtures 不被自动 spec 踩掉。"""
|
|
with tempfile.TemporaryDirectory() as d:
|
|
orig = R._WG1_DIR
|
|
R._WG1_DIR = Path(d)
|
|
try:
|
|
gid = "test-overwrite"
|
|
gdir = R.wg1_game_dir(gid)
|
|
gdir.mkdir(parents=True, exist_ok=True)
|
|
(gdir / "play-spec.json").write_text('{"existing": true}', encoding="utf-8")
|
|
r = R.ensure_play_spec(gid, {"targets": [{"occupied": True}]})
|
|
assert r["wrote"] is False and "不覆盖" in r["reason"]
|
|
assert json.loads((gdir / "play-spec.json").read_text(encoding="utf-8"))["existing"] is True
|
|
finally:
|
|
R._WG1_DIR = orig
|
|
|
|
|
|
def test_ensure_play_spec_writes_tap_targets_with_prefixes():
|
|
"""正常写:tap-targets 落盘的 spec 含金标同质 prefixes。"""
|
|
with tempfile.TemporaryDirectory() as d:
|
|
orig = R._WG1_DIR
|
|
R._WG1_DIR = Path(d)
|
|
try:
|
|
gid = "test-write"
|
|
r = R.ensure_play_spec(gid, {"phase": "play", "score": 1, "targets": [{"occupied": True}]})
|
|
assert r["wrote"] is True and r["driverType"] == "tap-targets"
|
|
spec = json.loads((R.wg1_game_dir(gid) / "play-spec.json").read_text(encoding="utf-8"))
|
|
assert spec["expectedEngineCallPrefixes"] == _GOLDEN_PREFIXES
|
|
finally:
|
|
R._WG1_DIR = orig
|
|
|
|
|
|
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)
|