""" test_toolkit.py — U2 工具底座(read/list/write)+ 形状门三态。 形状门:define 单对象 FAIL / 干净源 PASS / drawButton 赋值改签名后 PASS(#5 回吐命中矩形)经 shell-out 现有 node tools.mjs check — 便宜档与 Node 侧跑同一份门代码,口径必然一致。真 shell-out node(确定性命令,非 LLM gen 循环,前台串行)。 跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_toolkit.py """ import subprocess import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/ import cheap_run _GAME_RUNTIME = cheap_run._GAME_RUNTIME _AMODEL_GEN = cheap_run._AMODEL_GEN GID = "cheap-tk-test" # gitignore: amgen-cheap-* / _wg1-gen/cheap-* def _scaffold(gid: str = GID) -> None: """shell-out node tools.mjs scaffold-saa 起一个干净工程(SAA 信封)。""" subprocess.run( ["node", str(_AMODEL_GEN / "tools.mjs"), "scaffold-saa", gid], cwd=str(_GAME_RUNTIME), check=True, capture_output=True, text=True, ) def _write_logic(src: str, gid: str = GID) -> None: r = cheap_run.write_file(gid, f"game-runtime/games/amgen-{gid}/src/game-logic.js", src) assert r["ok"], r # ── 形状门合成输入:define 触发目标 footgun;drawButton 改签名后赋值消费已合法(#5 回吐命中矩形),其余 check 门尽量满足以免掩盖 ── _DRAWBUTTON_RETURN_OK = """export function createGame({ plugins, bundle, viewport }) { const { hudUi } = plugins; return { init(boot) { this.ctx = boot.ctx; }, update(dt) { bundle.tick(dt); }, render(g) { const btn = hudUi.drawButton(g, { x: 0, y: 0, w: 10, h: 10 }, { label: 'x' }); if (btn) { this.x = 1; } }, destroy() {}, handleTap(x, y) { this.tapped = true; }, _forensicsView() { return { state() { return { phase: 'menu', score: 0 }; }, measures() { return {}; } }; }, }; } """ _DEFINE_BAD = """export function createGame({ plugins, bundle, viewport }) { const { sceneFsm } = plugins; return { init(boot) { this.ctx = boot.ctx; sceneFsm.define({ menu: {}, play: {} }); }, update(dt) { bundle.tick(dt); }, render(g) {}, destroy() {}, handleTap(x, y) { this.tapped = true; }, _forensicsView() { return { state() { return { phase: 'menu', score: 0 }; }, measures() { return {}; } }; }, }; } """ def test_read_file_ok(): r = cheap_run.read_file("game-runtime/games/_template/src/game-logic.js") assert r["ok"] and "createGame" in r["content"] def test_read_out_of_repo(): r = cheap_run.read_file("../../../etc/hosts") assert not r["ok"] and "越界" in r["error"] def test_list_dir(): r = cheap_run.list_dir("game-runtime/games/_template/src") assert r["ok"] and "game-logic.js" in r["entries"] def test_write_l1_rejected(): _scaffold() r = cheap_run.write_file(GID, f"game-runtime/games/amgen-{GID}/src/host-config.js", "x") assert not r["ok"] and "L1" in r["error"] def test_write_l3_ok(): _scaffold() r = cheap_run.write_file(GID, f"game-runtime/games/amgen-{GID}/src/game-logic.js", "// hi\n") assert r["ok"] and r["bytes"] > 0 def test_check_shape_drawbutton_ok(): # 阶段一B #5:drawButton 改返命中矩形后,const btn = drawButton(...); if (btn) 是合法直觉写法、形状门不再拦。 _scaffold() _write_logic(_DRAWBUTTON_RETURN_OK) r = cheap_run.check(GID) assert r["ok"], f"drawButton 赋值消费应通过(改签名后返命中矩形):{r.get('output', '')}" def test_check_shape_define_fail(): _scaffold() _write_logic(_DEFINE_BAD) r = cheap_run.check(GID) assert not r["ok"], "define 单对象批量应触发形状门②" assert "define" in r["output"] def test_check_and_build_clean_pass(): """干净源(_template 合法游戏改一行使 ≠模板)应 check+build 全 PASS——证形状门/各门对合法游戏不误伤。""" _scaffold() tmpl = (cheap_run._TEMPLATE_DIR / "src" / "game-logic.js").read_text(encoding="utf-8") _write_logic("// cheap-worker U2 clean-pass 测试:在 _template 合法游戏基础上改一行使 ≠模板\n" + tmpl) rc = cheap_run.check(GID) assert rc["ok"], "干净源应 check PASS,实际 FAIL:\n" + rc["output"] rb = cheap_run.build(GID) assert rb["ok"], "干净源应 build PASS,实际 FAIL:\n" + rb["output"] 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)