lili 675a8e688b feat(cheap-worker): 便宜档 Node→Python 重写进 AgentScope spike — 范式可移植坐实
WU-A 迁移面 spike:便宜档生成核心(多轮 ReAct + 五工具沙箱 + done 门)用 Python 在新建 cheap-worker/
重写成 AgentScope agent,import 复用 tier2 框架层(model client/四道熔断/历史压缩/observability),
check/build/九门 shell-out 现有 node/cjs(不重写 esbuild/CDP/形状门)。

- U1 地基:跨包 sys.path import tier2 worker + 代理旁路 + key 从内网文档注入 env + M3 装配
- U2 五工具:read/list/write Python 实现 + check/build shell-out(形状门三态与 Node 一致)
- U3 LittleJS system prompt:prompt.mjs Python 化(两条 footgun 正反例 + 输入契约)
- U4 scaffold/stage/play 薄壳 + ensure_play_spec(据 state 形态产 play-spec 治链路假绿)
- U5 ReAct 主编排:内置 Agent+ReActConfig+外层有界 resume + done 门 + 收口 stage/smoke/九门
- U6 对照 Node:产物形态 + 九门逐门 + 过门率对比

验收:同 brief Python 新路 vs Node 旧路,产物都真 src/ 6 文件、九门都 9/9 全 PASS、过门率 1.0=1.0
→ spikePass=True。端到端单局 attempt 1 一次收敛、148s。落点 cheap-worker/ 对 tier2/amodel-gen
零改动(结构性零回归)。测试 38/38 全绿。

origin: docs/plans/2026-06-26-001-feat-cheap-worker-python-spike-plan.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 02:42:14 -07:00

129 lines
4.5 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)+ 形状门三态。
形状门三态(drawButton 赋值 FAIL / define 单对象 FAIL / 干净源 PASS)经 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
# ── 形状门合成输入:触发目标 footgun,其余 check 门尽量满足以免掩盖 ──
_DRAWBUTTON_BAD = """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_fail():
_scaffold()
_write_logic(_DRAWBUTTON_BAD)
r = cheap_run.check(GID)
assert not r["ok"], "drawButton 赋值应触发形状门①"
assert "drawButton" in r["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)