治「真相层缺失」病根(历史上模型文本/工具返回/门反馈零落盘、失败 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>
113 lines
4.9 KiB
Python
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"]))
|