M1 前置基座(便宜档源回传落库链 + base 源注入 + 血缘):
- derive_profile 兑现(便宜档基线 profile{realtime,discrete-choice,metric})解锁 sourceProject 回传
- worker parse_modify 提取 modify 区(mode/baseVersionId/modifyPatch/sourceProject)
- executor 据 baseVersionId 反查注入 base 源到 HTTP job(镜像 SaaGraphDispatcher.resolveBaseSourceProject)
- 回调落源回填 base_version_id 血缘(landSourceQuietly)
M2 worker 判意图(两段式第一段,NL→建议改动+风险):
- cheap_classify 把用户原话 + base 源可改面(assets.js 资产清单/core.js 集中数值/game-logic 玩法)
判成 {category,mode,target,payload,riskLevel,needsConfirm,clarify}
- mode×category 锁定(LLM 给的 mode 不采信)+ 危险回问硬编进 needsConfirm(只低风险确定性改免确认)
- worker /classify 同步端点(不入生成队列、不起 Chrome)
测试:Python test_a11_m1 5/5 + test_a11_m2_classify 9/9 + 回归全绿;
Java 执行器/回调/源服务 + SAA 回归(本地 maven 独立复跑 46 + 子代理 98)全绿。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
4.7 KiB
Python
115 lines
4.7 KiB
Python
"""test_a11_m1.py — A11 切片三 M1 前置基座(Python 侧)单测。
|
|
|
|
守两个不变量:
|
|
· derive_profile 兑现:便宜档 tap-targets 返基线 profile{realtime,discrete-choice,metric}(三枚举齐),
|
|
解锁 build_source_project → sourceProject 随 result-out 回传 → game_source_project 落库 → base 版本可反查。
|
|
· parse_modify 提取 §6.1 job 的 modify 区(modifyMode/baseVersionId/modifyPatch/sourceProject);
|
|
无 modifyMode = create 路、返 None(现行 create 路零副作用)。
|
|
|
|
跑:cheap-worker/.venv/bin/python cheap-worker/tests/test_a11_m1.py
|
|
"""
|
|
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # → cheap-worker/
|
|
import result_out as R # noqa: E402
|
|
import worker_service as W # noqa: E402
|
|
|
|
|
|
def _tmp() -> Path:
|
|
return Path(tempfile.mkdtemp(prefix="a11-m1-"))
|
|
|
|
|
|
def _make_game_dir(tmp: Path) -> Path:
|
|
"""造 amgen-<id>/ 形态产物目录(src/ 多文件 + bundle)。"""
|
|
gd = tmp / "amgen-m1"
|
|
(gd / "src").mkdir(parents=True)
|
|
(gd / "bundle.iife.js").write_text("var __GameBundle=(function(){return{}})();", encoding="utf-8")
|
|
(gd / "src" / "game-logic.js").write_text("// logic\nconst ROUND_MS=30000;\n", encoding="utf-8")
|
|
(gd / "src" / "core.js").write_text("// core\nexport const COLOR_PALETTE=['red'];\n", encoding="utf-8")
|
|
return gd
|
|
|
|
|
|
# ── M1① derive_profile 兑现 ──
|
|
|
|
def test_derive_profile_returns_valid_baseline():
|
|
"""derive_profile 返三枚举齐的合法 profile(非 None),且过 result_out._valid_profile。"""
|
|
job = {"gameId": "amgen-m1", "templateId": "generic", "brief": "点点乐"}
|
|
profile = W.derive_profile(job, _make_game_dir(_tmp()))
|
|
assert profile is not None, "derive_profile 不应再恒返 None"
|
|
assert R._valid_profile(profile), f"profile 三枚举不全/非法: {profile}"
|
|
assert profile["tickModel"] in ("realtime", "turn-based", "event")
|
|
assert profile["inputModel"] in ("continuous", "discrete-choice", "text-command")
|
|
assert profile["progressModel"] in ("metric", "narrative")
|
|
|
|
|
|
def test_derive_profile_unblocks_source_project():
|
|
"""有了合法 profile,build_source_project 能组出带 files 的 2.0 源工程(非 None)。"""
|
|
import json
|
|
gd = _make_game_dir(_tmp())
|
|
job = {"gameId": "amgen-m1", "brief": "点点乐"}
|
|
sp = R.build_source_project(gd, W.derive_profile(job, gd))
|
|
assert sp is not None, "profile 兑现后 sourceProject 不应再被省略"
|
|
obj = json.loads(sp)
|
|
assert obj["schemaVersion"] == "2.0"
|
|
assert "src/game-logic.js" in obj["files"] and "src/core.js" in obj["files"]
|
|
assert R._valid_profile(obj["profile"])
|
|
|
|
|
|
def test_process_job_carries_source_project():
|
|
"""端到端接线:derive_profile 兑现后,process_job 的回调 payload 带 sourceProject(源回传通)。"""
|
|
gd = _make_game_dir(_tmp())
|
|
summary = {"verdict": {"pass": True}, "wallSec": 1.0, "attempts": 1, "gameId": "amgen-m1", "stage": "done"}
|
|
captured = {}
|
|
|
|
def fake_send(url, payload, secret, **kw):
|
|
captured["payload"] = payload
|
|
return 200, "ok"
|
|
|
|
state = W.WorkerState(run_fn=lambda job: (summary, gd), send_fn=fake_send)
|
|
job = {"job_id": "t-m1", "traceId": "t-m1", "templateId": "generic", "brief": "点点乐",
|
|
"callback": {"target": "http://localhost:48080/cb"}}
|
|
W.process_job(state, job)
|
|
assert "sourceProject" in captured["payload"], "源回传应把 sourceProject 带进 result-out"
|
|
|
|
|
|
# ── M1④ parse_modify 提取 modify 区 ──
|
|
|
|
def test_parse_modify_extracts_fields():
|
|
"""有 modifyMode 的 job → 提取 {mode,baseVersionId,modifyPatch,sourceProject} 四件。"""
|
|
job = {
|
|
"gameId": "amgen-m1",
|
|
"modifyMode": "deterministic",
|
|
"baseVersionId": 4096,
|
|
"modifyPatch": '{"target":{"kind":"config","path":"/config/speed"},"payload":{"value":2}}',
|
|
"sourceProject": '{"schemaVersion":"2.0","files":{}}',
|
|
}
|
|
m = W.parse_modify(job)
|
|
assert m is not None
|
|
assert m["mode"] == "deterministic"
|
|
assert m["baseVersionId"] == 4096
|
|
assert m["modifyPatch"] and "config" in m["modifyPatch"]
|
|
assert m["sourceProject"] and "2.0" in m["sourceProject"]
|
|
|
|
|
|
def test_parse_modify_create_path_none():
|
|
"""无 modifyMode = create 路 → 返 None(现行 create 路零副作用)。"""
|
|
assert W.parse_modify({"gameId": "amgen-m1", "brief": "点点乐"}) is None
|
|
assert W.parse_modify({"gameId": "amgen-m1", "modifyMode": ""}) is None
|
|
|
|
|
|
def _run_all():
|
|
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
|
|
passed = 0
|
|
for fn in fns:
|
|
fn()
|
|
print(f" ✓ {fn.__name__}")
|
|
passed += 1
|
|
print(f"\n[test_a11_m1] {passed}/{len(fns)} passed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
_run_all()
|