lili 8d851ea716
Some checks failed
contract-gates / contract-gates (push) Has been cancelled
docs-gate / docs-gate (push) Has been cancelled
feat(cheap-gen): W-AXIS-V2 波2——取证契约退役+提示全加载链清洗,四门∧测试员为唯一验收链
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>
2026-07-10 09:21:04 -07:00

202 lines
7.4 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 取证契约教学已清洗(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():
"""完成工具名已统一为 finishKTD5不残留 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)