降AI参与·阶段一B 改签名第一处(附A #5)。逐插件改逐插件回归的黄金样板。 drawButton 由返 void 改为回吐它绘制的归一化命中矩形 {x,y,w,h}——AI 的直觉写法 const btn = hudUi.drawButton(g,{x,y,w,h},{label}); if (hudUi.pointInRect(px,py,btn)) 现在就对, 治了 #1 翻车点(drawButton 返 void 误用→变量恒 undefined→命中短路→卡菜单)的根因,而非教绕路。 全链路同改(漏一个造新脏数据): - hud-ui/impl.js:drawButton 模块函数 + 实例方法回吐归一化矩形 r(g=null 也回吐、绝不 undefined) - hud-ui/api.d.ts:模块函数 + 实例方法 两处 :void → :Rect + JSDoc - tools.mjs 形状门:删 drawButton 特例分支(api.d.ts 改后自然掉出 void 名集), 泛化判据留给仍真返 void 的方法(drawText/drawPanel/drawBar/play/flashScreen) - prompt.mjs/cheap_roles.py(双源)/skill:指引由"返void绝不接"翻为"回吐命中矩形可直接 pointInRect" - check.test.mjs:drawButton 测试反转(赋值不再拦)+ 链式测改 drawText - test_roles.py/test_toolkit.py:footgun 断言 + 形状门 drawButton 测试随签名更新 验证(逐插件回归矩阵): - 形状门单测 11/11(amodel-gen)+ cheap-worker test_roles/test_toolkit 16/16 + hud-ui 19/19 - 确定性 impl 铁证:drawButton 回吐 {x,y,w,h}、g=null 不 undefined、回吐矩形直接 pointInRect 闭环 - 真生成代码(菜单+按钮游戏)用了 drawButton、check 未拦 drawButton,证明 #5 不破坏生成 - 双源 drawButton 文案一致;prompt.mjs node --check + cheap_roles.py py_compile 绿 正交发现(非 #5):该局九门 gen 栽在 save.get() 方法名幻觉(save 插件无 get)—— 是另一处方法名摩擦,记为阶段一B 后续/补密度候选,不在 #5 范围。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
129 lines
4.8 KiB
Python
129 lines
4.8 KiB
Python
"""
|
||
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)
|