games-development-ai/cheap-worker/tests/test_cheap_failure_layer.py
lili dc89701fcc feat(cheap-gen): W-AXIS 波1 真相层——turns 全文落盘+per-run 归档+evidence 清单化+failureLayer
治「真相层缺失」病根(历史上模型文本/工具返回/门反馈零落盘、失败 run 被同 gid 重跑
覆盖,归因只能建在聚合数字上):

- cheap_turns_sink.py:逐 turn 全文落 amgen-<gid>/turns.jsonl(模型文本/工具全参/
  工具返回/门续修反馈);observe-only middleware、best-effort,接线失败绝不阻断生成;
  CLI 与 Service 双路接线(cheap_run/cheap_service_app)
- per-run 归档:同 gid 重跑前工程整体归档 _amgen-archive/<gid>-<ts>/,失败现场不再被覆盖
- evidence 清单化清理:按白名单保留,跨 run 残留(旧截图/旧 verdict)开跑即清,防证据串局
- failureLayer 三层归因:run-summary 落 generation/gate/gameplay 失败层字段,
  聚合报表可分「生成没跑完/机械门挂/玩法层拒」

主会话亲验:两局真跑归档 179KB、turns 含门反馈全文;pytest 新增三个测试档全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 04:27:17 -07:00

113 lines
4.9 KiB
Python

"""test_cheap_failure_layer.py — W-AXIS 波1 失败三层归因单测(driver_contract/mechanical/gameplay/none)。
覆盖 gate_judge 的两件归因(诊断档 §2.2 判卷合约反噬的对症):
1. classify_cheap_failure_layer:verdict → 三层归因(供 run-summary/批次账;成功/未跑出裁决=none);
2. _feedback_layer_lead + _render_cheap_feedback:续修回喂顶部按真实层次措辞(治「合约缺口误当玩法坏死」)。
跑法:cheap-worker/.venv/bin/python -m pytest cheap-worker/tests/test_cheap_failure_layer.py -q
"""
import os
import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_CW = os.path.dirname(_HERE)
_REPO_ROOT = os.path.dirname(_CW)
_GEN_WORKER = os.path.join(_REPO_ROOT, "tier2", "gen-worker")
for _p in (_CW, _GEN_WORKER):
if _p not in sys.path:
sys.path.insert(0, _p)
from worker.gate_judge import ( # noqa: E402
classify_cheap_failure_layer,
build_cheap_feedback_obj,
_feedback_layer_lead,
_render_cheap_feedback,
)
def _h(pass_=False, *, before=0, after=0, phase="menu"):
return {"pass": pass_,
"checks": [{"path": "score", "op": "increased", "before": before, "after": after, "pass": pass_}],
"latch": {"pass": False, "advisory": True, "phaseNow": phase}}
# ── classify:四态 ────────────────────────────────────────────────────────────
def test_classify_none_when_no_verdict():
assert classify_cheap_failure_layer(None)["layer"] == "none"
assert classify_cheap_failure_layer({})["layer"] == "none"
assert classify_cheap_failure_layer({"pass": False, "guards": {}})["layer"] == "none"
def test_classify_none_when_all_pass():
v = {"pass": True, "guards": {"A_boot": {"pass": True}, "H_progress": {"pass": True}}}
assert classify_cheap_failure_layer(v)["layer"] == "none"
def test_classify_mechanical_takes_priority():
# D_render(黑屏)失败即机械层,即便 H_progress 也挂(根因在机械)。
v = {"pass": False, "guards": {"D_render": {"pass": False, "bright": 2}, "H_progress": _h(phase="menu")}}
r = classify_cheap_failure_layer(v)
assert r["layer"] == "mechanical"
assert "D_render" in r["reason"]
def test_classify_driver_contract_menu_stuck():
# H_progress 挂 + latch phaseNow=菜单类 → 判卷驱动器进不去菜单,合约层(不是玩法坏)。
v = {"pass": False, "guards": {"H_progress": _h(phase="menu")}}
r = classify_cheap_failure_layer(v)
assert r["layer"] == "driver_contract"
assert "菜单" in r["reason"]
def test_classify_gameplay_score_increased_non_menu():
# score 真涨(after>before)、phase 已在 play、无回退考卷 → 玩法层(真玩驱动起来了但到不了终局)。
v = {"pass": False, "guards": {"H_progress": _h(before=0, after=216, phase="play")}}
assert classify_cheap_failure_layer(v)["layer"] == "gameplay"
def test_classify_driver_contract_fallback_zero_increment(tmp_path):
# selectionBasis=回退 + 零增量 → 合约层(考卷驱不动本游戏),需 staged play-spec 提供 selectionBasis。
import json
(tmp_path / "play-spec.json").write_text(json.dumps({
"driver": {"type": "key-cycle", "selectionBasis": "state 无 targets/target 键 → 按键类回退 → key-cycle 族"}
}), encoding="utf-8")
v = {"pass": False, "guards": {"H_progress": _h(before=5, after=5, phase="play")}}
r = classify_cheap_failure_layer(v, staged_dir=tmp_path)
assert r["layer"] == "driver_contract"
# ── 回喂反馈按真实层次措辞 ────────────────────────────────────────────────────
def test_feedback_lead_mechanical():
v = {"pass": False, "guards": {"D_render": {"pass": False, "bright": 2}}}
obj = build_cheap_feedback_obj(v, game_id="g")
lead = _feedback_layer_lead(obj)
assert lead and "机械门" in lead
assert _render_cheap_feedback(obj).startswith("【失败层次·机械门】")
def test_feedback_lead_driver_contract_menu():
v = {"pass": False, "guards": {"H_progress": _h(phase="menu")}}
obj = build_cheap_feedback_obj(v, game_id="g")
lead = _feedback_layer_lead(obj)
assert lead and "判卷驱动器合约" in lead
# 续修全文顶部横幅点明「不是你的玩法写坏」——治合约缺口被误当玩法坏死。
assert "不是你的玩法写坏" in _render_cheap_feedback(obj)
def test_feedback_lead_gameplay():
v = {"pass": False, "guards": {"H_progress": _h(before=0, after=216, phase="play")}}
obj = build_cheap_feedback_obj(v, game_id="g")
lead = _feedback_layer_lead(obj)
assert lead and "玩法层" in lead
def test_feedback_lead_none_when_no_items():
assert _feedback_layer_lead({}) is None
assert _feedback_layer_lead({"failedGates": []}) is None
if __name__ == "__main__":
import pytest
sys.exit(pytest.main([__file__, "-q"]))