lili dc49f5f866 feat(cheap-gen): 阶段一B #5 drawButton 改返命中矩形(黄金首件·治 #1 翻车点根因)
降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>
2026-06-30 01:39:05 -07:00

129 lines
4.8 KiB
Python
Raw 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_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 触发目标 footgundrawButton 改签名后赋值消费已合法(#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)