lili e6525aaa3d feat(cheap-worker): 便宜档生成丰富度 + M1 达标门富化重验(切片一 A/B)
接 sim-business 设计指导到生成 agent(cheap_roles+prompt.mjs 双源,先设计后写码+MVP-first);新建纯 LLM 丰富度验证 agent cheap_verify(非阻塞·不进 verdict·零 code-presence 断言,红线落地);bake_off 加 richness 列;cheap_roles 三级回落配置热取(C2a 加载器);cheap_studio trace 接线(C1b)。富游戏 M1 三品类各 5/5=100% 过九门达标。测试 test_roles 12 / test_cheap_verify 11 / test_bake_off 17 / test_cheap_trace 4 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 01:39:06 -07:00

200 lines
7.1 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_roles.py — U3 cheap_roles 静态断言(关键约束串在场;prompt 是静态内容,非行为门)。
不 import agentscope,系统/venv python 均可跑:
cheap-worker/.venv/bin/python cheap-worker/tests/test_roles.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/
from cheap_roles import build_system_prompt, SKILL_PATH
def test_entry_contract_present():
"""入口契约五法 + _forensicsView 在场。"""
p = build_system_prompt("t1")
assert "createGame({ plugins, bundle, viewport })" in p
assert "_forensicsView" in p
assert "init(boot)/update(dt)/render(g)/destroy()" in p
def test_two_footguns_present():
"""收口的关键 footgun 正反例串在场(阶段一B #5 后 drawButton 改为回吐命中矩形、sceneFsm.define 逐场景调)。"""
p = build_system_prompt("t1")
# ①:drawButton 回吐命中矩形,命中用 pointInRect(改签名后 const btn = drawButton(...) 直接判命中是直觉写法)
assert "drawButton" in p and "pointInRect" in p and "命中矩形" in p
# ②:sceneFsm.define 逐场景调,不要单对象批量
assert "sceneFsm.define('menu'" in p and "单对象批量" in p
def test_input_contract_present():
"""输入契约 handleTap/handleKey + 不调 getInput。"""
p = build_system_prompt("t1")
assert "handleTap(x, y)" in p
assert "handleKey(key)" in p
assert "不写 ctx.getInput" in p
def test_redline_present():
"""核心红线串:bundle.tick、ctx.time.nowMs()、ctx.random.next()。"""
p = build_system_prompt("t1")
assert "bundle.tick(dt)" in p
assert "ctx.time.nowMs()" in p
assert "ctx.random.next()" in p
def test_skill_pointer():
"""prompt 明确要求先 read 真 skill(不手抄)。"""
p = build_system_prompt("t1")
assert SKILL_PATH in p
assert "read_file('.agents/skills/littlejs-game-dev.md')" in p
def test_finish_not_done():
"""完成工具名已统一为 finish(KTD5),不残留 Node 的 done。"""
p = build_system_prompt("t1")
assert "调 finish" in p
assert "调 done" not in p
def test_no_tool_json_schema():
"""Python 侧工具 schema 由 FunctionTool 自动抽,prompt 不手写工具 JSON。"""
p = build_system_prompt("t1")
assert '"parameters"' not in p
assert "type: 'function'" not in p
def test_game_id_injected():
"""占位符 ⟦G⟧ 已据 game_id 全部替换,无残留。"""
p = build_system_prompt("cheap-abc")
assert "game-runtime/games/amgen-cheap-abc" in p
assert "⟦G⟧" not in p
import os
import tempfile
# ── C2a:热取加载器不变量 + 回落测试 ──────────────────────────────────────────────
def test_hotload_invariant_equals_inline():
"""不变量:热取路输出 == 内置回落路输出(同 game_id/scaffold_desc),逐字节一致。
没人改 .md、没设 CHEAP_PROMPTS_DIR 时,build_system_prompt 输出必须逐字节等于
改造前(直接用 _SYSTEM_PROMPT.replace)的结果——本相红线。
"""
import cheap_roles as _r
game_id = "inv-test-abc"
scaffold_desc = "测试脚手架描述"
# 热取路(默认:读 contracts/prompts/04-config/cheap-system.md)
_r._cr_reload_cache()
hot = build_system_prompt(game_id, scaffold_desc)
# 内置回落路:直接用 _SYSTEM_PROMPT
g = f"game-runtime/games/amgen-{game_id}"
inline = (
_r._SYSTEM_PROMPT
.replace("⟦SCAFFOLD_DESC⟧", scaffold_desc)
.replace("⟦G⟧", g)
)
assert hot == inline, (
f"热取路与内置路输出不一致(前 100 字符 diff):\n"
f" hot : {hot[:100]!r}\n"
f" inline: {inline[:100]!r}"
)
def test_hotload_default_scaffold_desc():
"""scaffold_desc=None 时,热取路与内置路同样使用 _DEFAULT_SCAFFOLD_DESC。"""
import cheap_roles as _r
_r._cr_reload_cache()
hot = build_system_prompt("inv-default")
g = "game-runtime/games/amgen-inv-default"
inline = (
_r._SYSTEM_PROMPT
.replace("⟦SCAFFOLD_DESC⟧", _r._DEFAULT_SCAFFOLD_DESC)
.replace("⟦G⟧", g)
)
assert hot == inline, "scaffold_desc=None 时热取路与内置路输出不一致"
def test_fallback_on_empty_prompts_dir():
"""CHEAP_PROMPTS_DIR 指向空目录(无 registry)→ 回落内置 _SYSTEM_PROMPT,不抛。"""
import cheap_roles as _r
old_env = os.environ.get("CHEAP_PROMPTS_DIR")
with tempfile.TemporaryDirectory() as d:
os.environ["CHEAP_PROMPTS_DIR"] = d
_r._cr_reload_cache()
try:
result = build_system_prompt("fallback-empty")
# 应与内置替换结果一致
g = "game-runtime/games/amgen-fallback-empty"
expected = (
_r._SYSTEM_PROMPT
.replace("⟦SCAFFOLD_DESC⟧", _r._DEFAULT_SCAFFOLD_DESC)
.replace("⟦G⟧", g)
)
assert result == expected, "空目录回落后输出与内置不一致"
finally:
if old_env is None:
os.environ.pop("CHEAP_PROMPTS_DIR", None)
else:
os.environ["CHEAP_PROMPTS_DIR"] = old_env
_r._cr_reload_cache() # 恢复默认路径缓存
def test_fallback_on_missing_md_file():
"""registry 有该 id 但 .md 文件不存在 → 回落内置 _SYSTEM_PROMPT,不抛。"""
import cheap_roles as _r
old_env = os.environ.get("CHEAP_PROMPTS_DIR")
with tempfile.TemporaryDirectory() as d:
# 在临时目录里建一个 registry.yaml,指向不存在的 .md
import pathlib
reg = pathlib.Path(d) / "registry.yaml"
reg.write_text(
"version: 1\nprompts:\n"
" - id: config.cheap-system\n"
" version: 1.0.0\n"
" file: 04-config/cheap-system.md\n",
encoding="utf-8",
)
# .md 文件不创建 → _cr_read_body 返回 None → 回落内置
os.environ["CHEAP_PROMPTS_DIR"] = d
_r._cr_reload_cache()
try:
result = build_system_prompt("fallback-no-md")
g = "game-runtime/games/amgen-fallback-no-md"
expected = (
_r._SYSTEM_PROMPT
.replace("⟦SCAFFOLD_DESC⟧", _r._DEFAULT_SCAFFOLD_DESC)
.replace("⟦G⟧", g)
)
assert result == expected, ".md 不存在时回落输出与内置不一致"
finally:
if old_env is None:
os.environ.pop("CHEAP_PROMPTS_DIR", None)
else:
os.environ["CHEAP_PROMPTS_DIR"] = old_env
_r._cr_reload_cache()
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 AssertionError as e:
_failed += 1
print(f" FAIL {_fn.__name__}: {e}")
print(f"\n{len(_fns) - _failed}/{len(_fns)} passed")
sys.exit(1 if _failed else 0)