固化 Match-3 生产者、视觉、音频与双 Judge 证据闭包。 将《山海行纪》r1.1 绑定新的不可变 release,并以生产预检现场核验 bundle、Registry/2 和 25 项 Writer 快照。 同步地图1平衡锁值、跨游戏回归修复、验收契约与 SoT 证据。
147 lines
5.6 KiB
Python
147 lines
5.6 KiB
Python
"""
|
||
test_run.py — U4 scaffold / stage / ensure_play_spec / play 薄壳。
|
||
|
||
scaffold/stage/ensure_play_spec 快(确定性);test_play_real 真起 Chrome 跑九门(~1min,验 play 薄壳跑通、
|
||
verdict 由 serve-and-play.sh 产出且结构正确)。前台串行(非 LLM gen 循环)。
|
||
|
||
跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_run.py
|
||
"""
|
||
|
||
import json
|
||
import sys
|
||
from pathlib import Path
|
||
from types import SimpleNamespace
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/
|
||
import cheap_run
|
||
|
||
|
||
def _clean_logic_from_template() -> str:
|
||
"""_template 合法游戏改一行(≠模板,过 check)。"""
|
||
tmpl = (cheap_run._TEMPLATE_DIR / "src" / "game-logic.js").read_text(encoding="utf-8")
|
||
return "// cheap-worker U4 测试:在 _template 合法游戏基础上改一行使 ≠模板\n" + tmpl
|
||
|
||
|
||
def test_scaffold():
|
||
gid = "cheap-run-sc"
|
||
r = cheap_run.scaffold(gid)
|
||
assert r["ok"], r["output"]
|
||
gd = cheap_run.game_dir(gid)
|
||
assert (gd / "src" / "game-logic.js").exists()
|
||
assert (gd / "src" / "host-config.js").exists()
|
||
|
||
|
||
def test_scaffold_interaction_profile_cli_passthrough(monkeypatch):
|
||
"""可信 profile 必须进入 scaffold 固定入口,不能只私下注入 smoke 浏览器。"""
|
||
calls = []
|
||
|
||
def fake_run(argv, **_kwargs):
|
||
calls.append(argv)
|
||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||
|
||
monkeypatch.setattr(cheap_run.subprocess, "run", fake_run)
|
||
assert cheap_run.scaffold(
|
||
"profile-match3", "_template-puzzle",
|
||
interaction_profile_id="match3.orthogonal-swap-v1",
|
||
)["ok"]
|
||
assert calls[-1][-4:] == [
|
||
"scaffold-saa", "profile-match3", "_template-puzzle", "match3.orthogonal-swap-v1",
|
||
]
|
||
|
||
|
||
def test_stage():
|
||
gid = "cheap-run-st"
|
||
assert cheap_run.scaffold(gid)["ok"]
|
||
cheap_run.write_file(gid, f"game-runtime/games/amgen-{gid}/src/game-logic.js", _clean_logic_from_template())
|
||
assert cheap_run.check(gid)["ok"]
|
||
assert cheap_run.build(gid)["ok"]
|
||
rs = cheap_run.stage(gid)
|
||
assert rs["ok"], rs["output"]
|
||
dst = cheap_run.wg1_game_dir(gid)
|
||
assert (dst / "bundle.iife.js").exists() and (dst / "index.html").exists()
|
||
|
||
|
||
def test_smoke_interaction_profile_cli_passthrough(monkeypatch):
|
||
"""显式 profile 追加 CLI 参数;默认 None 保持历史参数数量与顺序。"""
|
||
calls = []
|
||
|
||
def fake_run(argv, **_kwargs):
|
||
calls.append(argv)
|
||
return SimpleNamespace(
|
||
returncode=0,
|
||
stdout='[smoke] PASS {"ok":true,"state":null}\n',
|
||
stderr='',
|
||
)
|
||
|
||
monkeypatch.setattr(cheap_run.subprocess, "run", fake_run)
|
||
assert cheap_run.smoke("profile-default")["ok"] is True
|
||
assert calls[-1][-4:] == ["smoke", "profile-default", "4320", "9222"]
|
||
|
||
assert cheap_run.smoke(
|
||
"profile-match3", interaction_profile_id="match3.orthogonal-swap-v1",
|
||
)["ok"] is True
|
||
assert calls[-1][-5:] == [
|
||
"smoke", "profile-match3", "4320", "9222", "match3.orthogonal-swap-v1",
|
||
]
|
||
|
||
|
||
def test_ensure_play_spec_keycycle():
|
||
"""state 无 targets → key-cycle driver。"""
|
||
gid = "cheap-run-spec-kc"
|
||
sp = cheap_run.wg1_game_dir(gid) / "play-spec.json"
|
||
sp.unlink(missing_ok=True)
|
||
r = cheap_run.ensure_play_spec(gid, {"phase": "menu", "score": 0})
|
||
assert r["wrote"] and r["driverType"] == "key-cycle"
|
||
assert json.loads(sp.read_text(encoding="utf-8"))["driver"]["type"] == "key-cycle"
|
||
|
||
|
||
def test_ensure_play_spec_taptargets():
|
||
"""state 有 targets 数组 → tap-targets occupied driver。"""
|
||
gid = "cheap-run-spec-tt"
|
||
sp = cheap_run.wg1_game_dir(gid) / "play-spec.json"
|
||
sp.unlink(missing_ok=True)
|
||
r = cheap_run.ensure_play_spec(gid, {"phase": "play", "score": 5, "targets": [{"x": 1, "y": 2, "occupied": True}]})
|
||
assert r["wrote"] and r["driverType"] == "tap-targets"
|
||
spec = json.loads(sp.read_text(encoding="utf-8"))
|
||
assert spec["driver"]["type"] == "tap-targets" and "targets" in spec["exportState"]
|
||
|
||
|
||
def test_ensure_play_spec_no_overwrite():
|
||
"""已存在 play-spec(如别处已产)不覆盖。"""
|
||
gid = "cheap-run-spec-no"
|
||
sp = cheap_run.wg1_game_dir(gid) / "play-spec.json"
|
||
sp.parent.mkdir(parents=True, exist_ok=True)
|
||
sp.write_text('{"existing": true}', encoding="utf-8")
|
||
r = cheap_run.ensure_play_spec(gid, {"targets": []})
|
||
assert not r["wrote"] and "不覆盖" in r["reason"]
|
||
assert json.loads(sp.read_text(encoding="utf-8"))["existing"] is True
|
||
|
||
|
||
def test_play_real():
|
||
"""真起 Chrome 跑九门:验 play 薄壳跑通、verdict 由 serve-and-play.sh 产出且结构正确(不纠结 pass/fail)。"""
|
||
gid = "cheap-run-play"
|
||
assert cheap_run.scaffold(gid)["ok"]
|
||
cheap_run.write_file(gid, f"game-runtime/games/amgen-{gid}/src/game-logic.js", _clean_logic_from_template())
|
||
assert cheap_run.check(gid)["ok"]
|
||
assert cheap_run.build(gid)["ok"]
|
||
assert cheap_run.stage(gid)["ok"]
|
||
sm = cheap_run.smoke(gid)
|
||
cheap_run.ensure_play_spec(gid, sm.get("state"))
|
||
pr = cheap_run.play(gid)
|
||
assert pr["verdict"] is not None, "play 应产 verdict.json,实际无:\n" + pr["raw"]
|
||
assert isinstance(pr["verdict"].get("guards"), dict), "verdict 应含九门 guards:" + str(pr["verdict"])[:300]
|
||
|
||
|
||
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)
|