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>
159 lines
9.1 KiB
Python
159 lines
9.1 KiB
Python
"""
|
|
test_genre_puzzle_assets.py — 解谜品类范例资产的「范例签名 == 现契约」机器校验门(W-GENRE 品类件③④配套)。
|
|
|
|
防什么(设计档 §3 决策⑥/§6「新漂移面」):per-genre 脚手架与 few-shot 正例是给生成 agent 学的范例,
|
|
插件 API 改签名后若范例没同步,它们就成了**下一代漂移源**(教 AI 写已不存在的调用)。本门把
|
|
「范例调的每个 API 在现契约(api.d.ts)里真实存在 + 结构契约/红线/形状全过」编译成机器检查:
|
|
scaffold 克隆范例 → 跑 tools.mjs check(语法/结构契约/红线/API-存在静态门/形状静态门)→ 必须 PASS。
|
|
|
|
纯机械确定(shell-out node + 断言退出码),零 LLM、零网络;改坏范例签名 → 本测试红 → CI 拦。
|
|
|
|
跑:cheap-worker/.venv/bin/python -m pytest cheap-worker/tests/test_genre_puzzle_assets.py
|
|
(需 node 在 PATH——与 cheap_run 的 shell-out 前提一致。)
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
_REPO = Path(__file__).resolve().parents[2]
|
|
_GAME_RUNTIME = _REPO / "game-runtime"
|
|
_TOOLS = _GAME_RUNTIME / "tools" / "amodel-gen" / "tools.mjs"
|
|
|
|
# 解谜品类的两件范例资产:轻脚手架(件②)+ few-shot 过门正例(件③)。
|
|
_PUZZLE_EXEMPLARS = ("_template-puzzle", "_fewshot-puzzle")
|
|
# 校验用临时 gameId 前缀(scaffold 落 games/amgen-<id>/,测后清理;与生成产物 p11b-*/cheap-* 不串)。
|
|
_CHECK_ID_PREFIX = "gate-puzzle-sig-"
|
|
|
|
|
|
def _scaffold_and_check(template_name: str) -> tuple:
|
|
"""scaffold 克隆范例 → tools.mjs check;返回 (scaffold_ok, check_ok, check_output)。测后清理产物目录。"""
|
|
gid = _CHECK_ID_PREFIX + template_name.lstrip("_")
|
|
game_dir = _GAME_RUNTIME / "games" / f"amgen-{gid}"
|
|
try:
|
|
r1 = subprocess.run(
|
|
["node", str(_TOOLS), "scaffold-saa", gid, template_name],
|
|
cwd=str(_GAME_RUNTIME), capture_output=True, text=True, timeout=60, check=False,
|
|
)
|
|
# 模板缺失时 scaffoldSaa 会回落 _template(带 warn)——那也是失败态:范例目录没了/改名了,必须红。
|
|
fell_back = "回落 _template" in (r1.stdout + r1.stderr)
|
|
scaffold_ok = r1.returncode == 0 and not fell_back
|
|
if not scaffold_ok:
|
|
return False, False, (r1.stdout + r1.stderr).strip()
|
|
r2 = subprocess.run(
|
|
["node", str(_TOOLS), "check", gid],
|
|
cwd=str(_GAME_RUNTIME), capture_output=True, text=True, timeout=60, check=False,
|
|
)
|
|
return True, r2.returncode == 0, (r2.stdout + r2.stderr).strip()
|
|
finally:
|
|
shutil.rmtree(game_dir, ignore_errors=True) # 清理临时产物,不留脏目录
|
|
|
|
|
|
def test_puzzle_exemplars_pass_contract_check():
|
|
"""两件解谜范例(轻脚手架 + few-shot 正例)scaffold 后都必须过 check(范例签名 == 现契约)。"""
|
|
for tmpl in _PUZZLE_EXEMPLARS:
|
|
scaffold_ok, check_ok, out = _scaffold_and_check(tmpl)
|
|
assert scaffold_ok, f"{tmpl}: scaffold 失败/回落(范例目录缺失或克隆坏):{out[:500]}"
|
|
assert check_ok, f"{tmpl}: tools.mjs check 未过(范例签名漂移出现契约):{out[:800]}"
|
|
|
|
|
|
def test_puzzle_exemplars_forensics_contract_present():
|
|
"""静态兜底:两件范例的 game-logic.js 必须带现行输入契约面(createGame/handleTap/bundle.tick)。
|
|
|
|
W-AXIS-V2 波2:取证契约(targets/occupied)随 tap-targets 驱动器退役,不再是范例必备面;
|
|
_forensicsView 降级为可选调试接口(范例保留实现但不作断言)。
|
|
"""
|
|
for tmpl in _PUZZLE_EXEMPLARS:
|
|
src = (_GAME_RUNTIME / "games" / tmpl / "src" / "game-logic.js").read_text(encoding="utf-8")
|
|
for needle in ("export function createGame({ plugins, bundle, viewport })",
|
|
"handleTap", "bundle.tick"):
|
|
assert needle in src, f"{tmpl}: game-logic.js 缺契约面「{needle}」"
|
|
|
|
|
|
def test_puzzle_fewshot_has_remix_meta():
|
|
"""few-shot 正例必须带同款钩子元数据 GENRE_META(品类原型 + 主题标签,L4 传播结构范例)。"""
|
|
core = (_GAME_RUNTIME / "games" / "_fewshot-puzzle" / "src" / "core.js").read_text(encoding="utf-8")
|
|
assert "GENRE_META" in core and "archetype" in core and "themeTags" in core, \
|
|
"few-shot 正例缺 GENRE_META(genre/archetype/themeTags)——同款钩子范例面"
|
|
|
|
|
|
def test_puzzle_genre_rubric_fixture_shape():
|
|
"""品类 rubric fixture(件④)形状校验:≥4 条、逐条 0/1 可判、标层 L2/L3/L4、带正反例(质量模型 §3.3 最小形状)。"""
|
|
p = Path(__file__).resolve().parents[1] / "fixtures" / "genre-rubrics" / "puzzle.json"
|
|
assert p.is_file(), "缺 fixtures/genre-rubrics/puzzle.json(品类 rubric 机器版)"
|
|
data = json.loads(p.read_text(encoding="utf-8"))
|
|
assert data.get("genre") == "puzzle"
|
|
items = data.get("items")
|
|
assert isinstance(items, list) and len(items) >= 4, "品类扩展须 ≥4 条(§3.3 规范二)"
|
|
for it in items:
|
|
assert isinstance(it.get("name"), str) and it["name"], "条目缺 name"
|
|
assert it.get("layer") in ("L2", "L3", "L4"), f"条目 {it.get('name')} 层标非法:{it.get('layer')}"
|
|
assert isinstance(it.get("meaning"), str) and it["meaning"], f"条目 {it.get('name')} 缺一句可判定判据"
|
|
assert isinstance(it.get("positive"), str) and it["positive"], f"条目 {it.get('name')} 缺正例"
|
|
assert isinstance(it.get("negative"), str) and it["negative"], f"条目 {it.get('name')} 缺反例"
|
|
|
|
|
|
def _import_cheap_verify():
|
|
"""import cheap_verify(tests/ 直跑时补 sys.path;纯函数路不依赖 agentscope)。"""
|
|
import sys
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
import cheap_verify
|
|
return cheap_verify
|
|
|
|
|
|
def test_genre_loader_and_none_path_identity():
|
|
"""④接线不变量:genre=None/缺 fixture → loader 返 [];judge prompt 与无品类版**逐字节一致**(评分尺零漂移)。"""
|
|
cv = _import_cheap_verify()
|
|
assert cv.load_genre_checklist(None) == []
|
|
assert cv.load_genre_checklist("") == []
|
|
assert cv.load_genre_checklist("no-such-genre-xyz") == [], "缺 fixture 必须退化为空扩展"
|
|
items = cv.load_genre_checklist("puzzle")
|
|
assert len(items) >= 4 and all(len(t) == 3 and t[1] in ("L2", "L3", "L4") for t in items)
|
|
src = "// demo src"
|
|
assert cv._build_judge_user(src, "b") == cv._build_judge_user(src, "b", genre_key="puzzle", genre_checklist=None), \
|
|
"无品类条目时 judge prompt 必须与既有版本逐字节一致(锚定纪律)"
|
|
# 品类段真的进 prompt(有品类时;主干契约=单 checks 数组、通用底座在前、品类条目紧随)。
|
|
p = cv._build_judge_user(src, "b", genre_key="puzzle", genre_checklist=tuple(items))
|
|
n_base = len(cv.RICHNESS_CHECKLIST) # 12(2026-07-03 ⑨升第 12 条)
|
|
assert "品类扩展清单" in p and items[0][0] in p and f"共 {n_base + len(items)} 条" in p
|
|
|
|
|
|
def test_genre_parse_separate_subtotals():
|
|
"""④解析:品类段独立解析出 result["genre"](分母独立小计);无 genre_checklist 时无新增键。
|
|
|
|
主干契约 = **单 checks 数组**(通用 11 条在前、品类条目紧随其后),输出嵌套 richness.genre
|
|
{key,hits,score,max,groups}——与 TRPG 品类件同一机制(merge 收敛:分支原 genreChecks 双数组契约弃用)。
|
|
"""
|
|
cv = _import_cheap_verify()
|
|
items = tuple(cv.load_genre_checklist("puzzle"))
|
|
base_checks = [{"name": n, "hit": True, "why": "ok"} for (n, _l, _m) in cv.RICHNESS_CHECKLIST]
|
|
genre_checks = [{"name": n, "hit": (i % 2 == 0), "why": "ok"} for i, (n, _l, _m) in enumerate(items)]
|
|
text = json.dumps({"checks": base_checks + genre_checks, "notes": "n"}, ensure_ascii=False)
|
|
r = cv.parse_judge_output(text, genre_checklist=items, genre_key="puzzle")
|
|
assert r["degraded"] is False and r["score"] == len(cv.RICHNESS_CHECKLIST)
|
|
g = r["genre"]
|
|
assert g["key"] == "puzzle" and g["max"] == len(items) and len(g["hits"]) == len(items)
|
|
gg = g["groups"]
|
|
layer_max = {ly: sum(1 for (_n, l, _m) in items if l == ly) for ly in ("L2", "L3", "L4")}
|
|
assert {ly: gg[ly]["max"] for ly in gg} == layer_max, "品类 groups 分母应来自品类清单(独立小计)"
|
|
assert g["score"] == sum(1 for i in range(len(items)) if i % 2 == 0)
|
|
# 模型漏答品类段(checks 只有通用底座条目)→ 扩展全 0、底座不受影响。
|
|
r2 = cv.parse_judge_output(json.dumps({"checks": base_checks, "notes": "n"}),
|
|
genre_checklist=items, genre_key="puzzle")
|
|
assert r2["score"] == len(cv.RICHNESS_CHECKLIST) and all(not h["hit"] for h in r2["genre"]["hits"])
|
|
# 无品类路:输出键集与既有版本一致(不带 genre 键)。
|
|
r3 = cv.parse_judge_output(json.dumps({"checks": base_checks, "notes": "n"}))
|
|
assert "genre" not in r3
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 裸跑模式(与本目录其它测试同款:无 pytest 也可直接 python 执行)。
|
|
test_puzzle_exemplars_pass_contract_check()
|
|
test_puzzle_exemplars_forensics_contract_present()
|
|
test_puzzle_fewshot_has_remix_meta()
|
|
test_puzzle_genre_rubric_fixture_shape()
|
|
test_genre_loader_and_none_path_identity()
|
|
test_genre_parse_separate_subtotals()
|
|
print("test_genre_puzzle_assets: 全部通过")
|