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

104 lines
4.2 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_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
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_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_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)