opus 执行位交付、主会话四步验收过(三套测试亲跑/红线 diff 亲读/冒烟截图亲眼): - 验收消费:check 新增源码级插件调用存在断言(_pluginCallPresenceErrors 词法层, 接替 F_wiring 真接线护栏);_forensicsView「必须实现」拦截摘除,降级为可选调试 接口(handleTap 输入契约与 ctx.log 日志纪律保留);gate_judge 放行措辞对齐四门 - play-spec/驱动器退役(逐件裁定 10 件):gen.mjs done 门自动产 play-spec 摘除、 cheap_gates/cheap_studio/cheap_modify/service_driver 调用摘除、_read_driver_type 改读 evidence/playtest/playtest.json;golden-spec 三工具(auto_vs_golden/bake_off/ compare_node)显式封存;ensure_play_spec 两实现保留一个版本窗口(只摘调用不删码) - prompt 全加载链清洗:cheap-system.md v1.6.3→1.7.0(删 _forensicsView 自动验收 契约红线与全部 targets/occupied 语义,五法句/品类指路同步),cheap_roles 内置 回退逐字节同步(test_roles 12/12 证 parity),modify 切片经运行时切块自动同步; 5 品类 skills+littlejs 手册+recipes×2+模板 8 件+README 6 件教学清洗,真设计 价值(单击 casual/可解性质/双层反馈/latch)保留 - 批账:hard_genre_batch/xtheme 行加 playtest 段,_row_total_cost 修 shadow 下 测试员成本漏算(v2 取 max 防双计) 验收(主会话亲验):pytest cheap-worker 512 绿 + tier2 141 绿 + node 49/49; 25 局基线新旧口径重放对照——真坏 5 局全拦(floor 拦 2/测试员拦 3)、无一例旧拦 新放,翻案 2 局均为破案坐实的仪器误杀;n=3 冒烟(mode=v2)全链绿:w2v2-sim harness verdict.pass=True 而测试员 reject(机械放行测试员逮住,盘上双证)、 w2v2-sim-fix 现象反馈两轮回喂→fix2 收敛(配方显示修成,局内截图亲眼); rg 零残留(模板残留均为「验收不读它」的降级调试接口合规形态);真跑 ¥8.4。 已知余项:repairFeedback 有产地、Service 续修环消费者未接(测试员 reject 不入 RepairMiddleware,续修判据仍四门)——是否接入随波3 校准数据一并裁;v1 配置级 回退窗口关闭为创始人知悉项,genconfig 默认 mode 仍 shadow、切 v2 归创始人。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
202 lines
7.4 KiB
Python
202 lines
7.4 KiB
Python
"""
|
||
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 取证契约教学已清洗(W-AXIS-V2 波2:自动验收随驱动器退役,
|
||
验收 = 四门投影 ∧ 测试 agent 真玩,模型不再为死契约写代码)——正文必须零残留。"""
|
||
p = build_system_prompt("t1")
|
||
assert "createGame({ plugins, bundle, viewport })" in p
|
||
assert "_forensicsView" not in p
|
||
assert "occupied" not in p # targets/occupied 语义同批清洗
|
||
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)
|