m3d-core / tests /test_intent.py
jkorstad's picture
intent planning tests
bf9488e verified
Raw History Blame Contribute Delete
3.19 kB
"""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