"""Intent-driven editing contract tests: intent -> plan -> approval-gated execution.""" from __future__ import annotations import json import wave import pytest from m3d.modules.gltf_mcp import build_module as build_gltf from m3d.modules.intent_planner import build_module as build_planner from m3d.modules.mock_edit import build_sfx_module, build_texture_module from m3d.runner import Runner from m3d.store import Store from tests.fixtures import make_character_glb, make_sample_image def _env(tmp_path): store = Store(root=tmp_path / "cas") modules = { "std/gltf-mcp": build_gltf(store), "std/mock-texture": build_texture_module(store), "std/mock-sfx": build_sfx_module(store), "std/intent-planner": build_planner(store), } runner = Runner(store, modules, gates_dir=tmp_path / "gates") return store, modules, runner def _hero_asset(tmp_path, store): """A hero-knight asset: mesh GLB + texture PNG + footsteps WAV + .asset.json.""" mesh = tmp_path / "hero.glb" make_character_glb(mesh) tex = tmp_path / "cloak.png" make_sample_image(tex) wav_path = tmp_path / "footsteps.wav" with wave.open(str(wav_path), "w") as w: w.setnchannels(1) w.setsampwidth(2) w.setframerate(8000) w.writeframes(b"\x00\x00" * 8000) manifest = { "asset": store.put_file(mesh), "canonical": "hero.glb", "parts": [ {"role": "mesh", "uri": str(mesh)}, {"role": "texture/albedo", "uri": str(tex)}, {"role": "audio", "uri": str(wav_path)}, ], "provenance": [], } manifest_uri = store.put_bytes(json.dumps(manifest).encode(), "hero.asset.json") return manifest_uri def test_intent_plan_and_execute(tmp_path): store, modules, runner = _env(tmp_path) manifest_uri = _hero_asset(tmp_path, store) # 1) intent -> plan planner = modules["std/intent-planner"] res = planner.run_tool("plan", { "intent": "make the cloak tattered and the footsteps heavier", "manifest_uri": manifest_uri, }) assert res["ok"] is True plan_wf = res["plan"] node_ids = {n["id"] for n in plan_wf["nodes"]} # texture + audio modules AND the Blender human-gate, but rig untouched assert "tex" in node_ids and "sfx" in node_ids and "mesh-edit" in node_ids assert "rig" not in node_ids # the planner must respect the manifest: no matching part -> no invented nodes with pytest.raises(ValueError): planner.run_tool("plan", {"intent": "re-rig the character", "manifest_uri": manifest_uri}) # 2) execute the emitted plan through the same runner, approvals pre-seeded for nid in ("mesh-edit", "review"): stem = Runner._gate_stem(plan_wf["id"], nid) (tmp_path / "gates" / f"{stem}.approved.json").write_text(json.dumps({"decision": "approve"})) result = runner.run(plan_wf) out = result["outputs"] assert out["tex"]["output_kind"] == "uri:image" assert out["sfx"]["output_kind"] == "uri:audio" assert out["review"]["decision"] == "approve" assert out["post"]["counts"]["meshes"] == 1 # conditioned edge taken