games-development-ai/cheap-worker/tests/test_a11_m5_assertions.py
lili 9a6feddfee feat(studio): A11 切片三 M5 三结构断言机器门(改对了没/有没有误伤/血缘可查)
九门只验"仍能玩",验不出"改对了没、有没有误伤";三断言补这层(plan① 线 154)。

cheap_assert.py:
- ① change_applied:改动真生效非 no-op(deterministic=manifest found 且 old≠new / regenerate=changed)
- ② nontarget_stable:base→new 真比对算内容差,非目标文件变=误伤——不信执行器自报标记,
  防执行器 bug 自证(兑现 Opus 评审 P1-2)
- ③ lineage_queryable:baseVersionId 在(worker 侧验必要条件,DB 真可查由 M1 回填 + 后端 e2e)
- three_assertions 合并 verdict

集成进 execute_*:两档加 base_version_id 参 + 附 assertions verdict + 收紧 status
(succeeded ⟺ 九门 ∧ ① ∧ ②;regenerate 原漏判 ①changed 已补);worker 透传 modify.baseVersionId。

测试:test_a11_m5_assertions 12/12 + 全 85 测试独立复跑全绿(含 M3/M4 桩接 kwarg)。
集成端到端 smoke(真九门):ROUND_MS 30000→19000 + baseVersionId=4096
→ status=succeeded、a1/a2/a3 全 true、allPass=true、落盘 19000。

受计费真后端 e2e(经 studio→aigc→worker→回调→新版本+D12计费+血缘)待 mini-desktop 全栈。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 02:34:06 -07:00

118 lines
4.6 KiB
Python

"""test_a11_m5_assertions.py — A11 切片三 M5 三结构断言机器门单测。
plan① 线 154 的 A11 完成判据(九门之外补三条,验"改对了没、有没有误伤"):
① 改动真生效(非 no-op):请求的结构化改动可断言真实落地;
② 非目标稳定:除目标文件外,其余文件字节未变(没顺手改坏别的模块);
③ 血缘可查:新版本带 baseVersionId(可回溯改自哪一版)。
三断言跨两档统一(deterministic manifest=list / regenerate manifest=dict),由 base→new files 真比对得出,
不只信执行器自报的标记(防执行器 bug 自证)。
跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_a11_m5_assertions.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/
import cheap_assert as A # noqa: E402
_BASE = {
"src/core.js": "export const ROUND_MS = 30000;\n",
"src/game-logic.js": "// 玩法 v1\n",
"src/render.js": "// 渲染\n",
"src/assets.js": "export const IMAGE_FILES = {};\n",
}
# ── 断言①:改动真生效(非 no-op)──
def test_a1_deterministic_real_change():
manifest = [{"file": "src/core.js", "kind": "config", "key": "ROUND_MS", "old": "30000", "new": "20000", "found": True}]
assert A.assert_change_applied(manifest)["pass"] is True
def test_a1_deterministic_noop_found_false():
manifest = [{"file": "src/core.js", "kind": "config", "key": "NOPE", "old": None, "new": None, "found": False}]
assert A.assert_change_applied(manifest)["pass"] is False
def test_a1_deterministic_noop_same_value():
manifest = [{"file": "src/core.js", "kind": "config", "key": "ROUND_MS", "old": "30000", "new": "30000", "found": True}]
assert A.assert_change_applied(manifest)["pass"] is False, "found 但值没变=no-op"
def test_a1_regenerate_changed_true():
assert A.assert_change_applied({"file": "src/game-logic.js", "kind": "behavior", "changed": True})["pass"] is True
def test_a1_regenerate_changed_false():
assert A.assert_change_applied({"file": "src/game-logic.js", "kind": "behavior", "changed": False})["pass"] is False
# ── 断言②:非目标稳定(base→new 真比对,不信自报)──
def test_a2_only_target_changed():
new = dict(_BASE)
new["src/core.js"] = "export const ROUND_MS = 20000;\n" # 只改目标
out = A.assert_nontarget_stable(_BASE, new, target_files={"src/core.js"})
assert out["pass"] is True
assert out["nontargetChanged"] == []
def test_a2_collateral_damage_caught():
new = dict(_BASE)
new["src/core.js"] = "export const ROUND_MS = 20000;\n"
new["src/render.js"] = "// 渲染 被顺手改坏\n" # 误伤非目标
out = A.assert_nontarget_stable(_BASE, new, target_files={"src/core.js"})
assert out["pass"] is False, "误伤非目标必须被抓"
assert "src/render.js" in out["nontargetChanged"]
def test_a2_regenerate_target_gamelogic():
new = dict(_BASE)
new["src/game-logic.js"] = "// 玩法 v2 连击\n"
out = A.assert_nontarget_stable(_BASE, new, target_files={"src/game-logic.js"})
assert out["pass"] is True
# ── 断言③:血缘可查(baseVersionId 在)──
def test_a3_lineage_present():
assert A.assert_lineage(4096)["pass"] is True
def test_a3_lineage_missing():
assert A.assert_lineage(None)["pass"] is False
# ── 合并三断言 verdict ──
def test_three_assertions_all_pass():
new = dict(_BASE)
new["src/core.js"] = "export const ROUND_MS = 20000;\n"
manifest = [{"file": "src/core.js", "kind": "config", "key": "ROUND_MS", "old": "30000", "new": "20000", "found": True}]
v = A.three_assertions(manifest, _BASE, new, target_files={"src/core.js"}, base_version_id=4096)
assert v["allPass"] is True
assert v["a1_changeApplied"]["pass"] and v["a2_nontargetStable"]["pass"] and v["a3_lineageQueryable"]["pass"]
def test_three_assertions_fails_on_collateral():
new = dict(_BASE)
new["src/core.js"] = "export const ROUND_MS = 20000;\n"
new["src/render.js"] = "// 误伤\n"
manifest = [{"file": "src/core.js", "kind": "config", "key": "ROUND_MS", "old": "30000", "new": "20000", "found": True}]
v = A.three_assertions(manifest, _BASE, new, target_files={"src/core.js"}, base_version_id=4096)
assert v["allPass"] is False, "误伤非目标 → 整体不过"
def _run_all():
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
for fn in fns:
fn()
print(f" ✓ {fn.__name__}")
print(f"\n[test_a11_m5_assertions] {len(fns)}/{len(fns)} passed")
if __name__ == "__main__":
_run_all()