lili cbfd4d871b
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(acceptance): 闭合 playtest v3 与 A+ 可信消费链
固化 Match-3 生产者、视觉、音频与双 Judge 证据闭包。

将《山海行纪》r1.1 绑定新的不可变 release,并以生产预检现场核验 bundle、Registry/2 和 25 项 Writer 快照。

同步地图1平衡锁值、跨游戏回归修复、验收契约与 SoT 证据。
2026-07-28 20:16:13 -07:00

147 lines
5.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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)