from __future__ import annotations import json from pathlib import Path import pytest from agent.compiler import compile_urdf_report, persist_compile_success_artifacts, update_manifest from agent.runner import compile_urdf _REMOVED_PACKAGE = "_".join(("sdk", "hybrid")) def _write_isolated_part_model_script( script_path: Path, *, allowed_part: str | None = None, disconnected_base: bool = False, ) -> None: base_visuals = [ "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", ] if disconnected_base: base_visuals.append( "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.4, 0.0, 0.05)))" ) lines = [ "from __future__ import annotations", "", "from pathlib import Path", "", "from sdk import ArticulatedObject, ArticulationType, Box, Origin, TestContext", "", "HERE = Path(__file__).resolve().parent", "object_model = ArticulatedObject(name='isolated_allowance')", "base = object_model.part('base')", *base_visuals, "support = object_model.part('support')", "support.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.15)))", "antenna = object_model.part('antenna')", "antenna.visual(Box((0.04, 0.04, 0.2)), origin=Origin(xyz=(0.0, 0.0, 0.1)))", "object_model.articulation(", " 'base_to_support',", " ArticulationType.FIXED,", " parent=base,", " child=support,", " origin=Origin(xyz=(0.0, 0.0, 0.0)),", ")", "object_model.articulation(", " 'base_to_antenna',", " ArticulationType.FIXED,", " parent=base,", " child=antenna,", " origin=Origin(xyz=(0.6, 0.0, 0.0)),", ")", "", "def run_tests():", " ctx = TestContext(object_model, asset_root=HERE)", ] if allowed_part is not None: lines.append( f" ctx.allow_isolated_part({allowed_part!r}, reason='intentionally freestanding decorative part')" ) lines.append(" ctx.fail_if_isolated_parts()") if disconnected_base: lines.append(" ctx.warn_if_part_contains_disconnected_geometry_islands()") lines.extend( [ " return ctx.report()", ] ) script_path.write_text("\n".join(lines), encoding="utf-8") def _write_overlap_allowance_model_script(script_path: Path) -> None: script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, ArticulationType, Box, Origin, TestContext", "", "object_model = ArticulatedObject(name='overlap_allowance')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", "child = object_model.part('child')", "child.visual(Box((0.08, 0.08, 0.08)), origin=Origin(xyz=(0.0, 0.0, 0.04)))", "object_model.articulation(", " 'base_to_child',", " ArticulationType.FIXED,", " parent=base,", " child=child,", " origin=Origin(xyz=(0.0, 0.0, 0.02)),", ")", "", "def run_tests():", " ctx = TestContext(object_model)", " ctx.allow_overlap('base', 'child', reason='bearing sleeve nests into the mount')", " return ctx.report()", ] ), encoding="utf-8", ) def _write_multiple_root_model_script(script_path: Path) -> None: script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, ArticulationType, Box, Origin, TestContext", "", "object_model = ArticulatedObject(name='multiple_roots')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", "child = object_model.part('child')", "child.visual(Box((0.05, 0.05, 0.05)), origin=Origin(xyz=(0.0, 0.0, 0.125)))", "object_model.articulation(", " 'base_to_child',", " ArticulationType.FIXED,", " parent=base,", " child=child,", " origin=Origin(xyz=(0.0, 0.0, 0.1)),", ")", "extra = object_model.part('extra')", "extra.visual(Box((0.05, 0.05, 0.05)), origin=Origin(xyz=(0.4, 0.0, 0.025)))", "", "def run_tests():", " ctx = TestContext(object_model)", " return ctx.report()", ] ), encoding="utf-8", ) def test_compile_artifacts_update_manifest(tmp_path: Path) -> None: outputs_root = tmp_path / "outputs" run_dir = outputs_root / "sample_run" viewer_dir = outputs_root / "viewer" run_dir.mkdir(parents=True) viewer_dir.mkdir(parents=True) urdf_path = run_dir / "sample_run.urdf" sig = persist_compile_success_artifacts( urdf_xml="", urdf_out=urdf_path, outputs_root=outputs_root, ) assert sig is not None assert urdf_path.read_text(encoding="utf-8") == "" manifest = json.loads((outputs_root / "manifest.json").read_text(encoding="utf-8")) assert manifest == { "generated": [ { "name": "sample_run", "path": "sample_run/sample_run.urdf", } ] } duplicate_sig = persist_compile_success_artifacts( urdf_xml="", urdf_out=urdf_path, outputs_root=outputs_root, previous_sig=sig, ) assert duplicate_sig == sig extra_urdf = outputs_root / "second" / "second.urdf" extra_urdf.parent.mkdir(parents=True) extra_urdf.write_text("", encoding="utf-8") (viewer_dir / "ignored.urdf").write_text("", encoding="utf-8") update_manifest(outputs_root) manifest = json.loads((outputs_root / "manifest.json").read_text(encoding="utf-8")) assert manifest == { "generated": [ { "name": "sample_run", "path": "sample_run/sample_run.urdf", }, { "name": "second", "path": "second/second.urdf", }, ] } assert callable(compile_urdf) def test_compile_urdf_report_can_skip_required_checks(tmp_path: Path) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin", "", "object_model = ArticulatedObject(name='unsafe')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", ] ), encoding="utf-8", ) with pytest.raises(RuntimeError, match="Missing required `run_tests\\(\\)`"): compile_urdf_report(script_path) report = compile_urdf_report(script_path, run_checks=False) assert " None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin, TestContext", "", "object_model = ArticulatedObject(name='validate_once')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", "", "def run_tests():", " ctx = TestContext(object_model)", " return ctx.report()", ] ), encoding="utf-8", ) from sdk._core.v0.articulated_object import ArticulatedObject validate_calls = {"count": 0} original_validate = ArticulatedObject.validate def wrapped_validate(self, strict: bool = True): validate_calls["count"] += 1 return original_validate(self, strict=strict) monkeypatch.setattr(ArticulatedObject, "validate", wrapped_validate) compile_urdf_report(script_path, run_checks=True, target="full") assert validate_calls["count"] == 1 def test_compile_urdf_report_can_ignore_geometry_qc_after_materialization( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin", "", "object_model = ArticulatedObject(name='qc_ok')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", ] ), encoding="utf-8", ) def fake_run_required_tests(*_args, **_kwargs): raise RuntimeError( "URDF compile failure (physical, blocking): isolated parts detected " "(not contacting any other part in the checked pose)." ) monkeypatch.setattr("agent.compiler._run_required_tests", fake_run_required_tests) with pytest.raises(RuntimeError, match="URDF compile failure \\(physical, blocking\\)"): compile_urdf_report(script_path) report = compile_urdf_report(script_path, ignore_geom_qc=True) assert " None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin, TestContext", "from pathlib import Path", "", "HERE = Path(__file__).resolve().parent", "object_model = ArticulatedObject(name='tests_only')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", "", "def run_tests():", " ctx = TestContext(object_model, asset_root=HERE)", " return ctx.report()", ] ), encoding="utf-8", ) def fail_if_called(*_args, **_kwargs): raise AssertionError("compiler-owned QC should not run during full validation") monkeypatch.setattr("agent.compiler._warn_cwd_relative_asset_paths", fail_if_called) monkeypatch.setattr("agent.compiler._warn_geometry_scale_anomalies", fail_if_called) report = compile_urdf_report(script_path, run_checks=True, target="full") assert " None: script_path = tmp_path / "model.py" _write_isolated_part_model_script(script_path) with pytest.raises(RuntimeError, match="Isolated parts detected"): compile_urdf_report(script_path, run_checks=True, target="full") def test_compile_urdf_report_allows_explicitly_allowed_isolated_part(tmp_path: Path) -> None: script_path = tmp_path / "model.py" _write_isolated_part_model_script(script_path, allowed_part="antenna") report = compile_urdf_report(script_path, run_checks=True, target="full") assert " None: script_path = tmp_path / "model.py" _write_overlap_allowance_model_script(script_path) report = compile_urdf_report(script_path, run_checks=True, target="full") assert " None: script_path = tmp_path / "model.py" _write_isolated_part_model_script(script_path, allowed_part="support") with pytest.raises(RuntimeError, match="Isolated parts detected"): compile_urdf_report(script_path, run_checks=True, target="full") def test_compile_urdf_report_fails_when_model_has_multiple_root_parts(tmp_path: Path) -> None: script_path = tmp_path / "model.py" _write_multiple_root_model_script(script_path) with pytest.raises( RuntimeError, match="check_single_root_part|exactly one root part", ): compile_urdf_report(script_path, run_checks=True, target="full") def test_compile_urdf_report_preserves_run_test_warnings_on_success(tmp_path: Path) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin, TestReport", "", "object_model = ArticulatedObject(name='warns')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", "", "def run_tests() -> TestReport:", " return TestReport(", " passed=True,", " checks_run=1,", " checks=('warn_if_part_contains_disconnected_geometry_islands',),", " failures=(),", " warnings=('custom non-blocking warning',),", " )", ] ), encoding="utf-8", ) report = compile_urdf_report(script_path, run_checks=True, target="full") assert " None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin, TestReport", "", "object_model = ArticulatedObject(name='floating_warning')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", "", "def run_tests() -> TestReport:", " return TestReport(", " passed=True,", " checks_run=1,", " checks=('warn_if_part_contains_disconnected_geometry_islands',),", " failures=(),", " warnings=(", ' "warn_if_part_contains_disconnected_geometry_islands(tol=1e-06): "', " \"Disconnected geometry islands detected:\\npart='controls' connected=1/19\",", " ),", " )", ] ), encoding="utf-8", ) report = compile_urdf_report(script_path, run_checks=True, target="full") assert " None: script_path = tmp_path / "model.py" _write_isolated_part_model_script( script_path, allowed_part="antenna", disconnected_base=True, ) report = compile_urdf_report(script_path, run_checks=True, target="full") assert " None: script_path = tmp_path / "model.py" _write_isolated_part_model_script(script_path) with pytest.raises(RuntimeError, match="Isolated parts detected") as excinfo: compile_urdf_report(script_path, run_checks=True, target="full") assert str(excinfo.value).count("fail_if_isolated_parts()") == 1 def test_compile_urdf_report_does_not_ignore_non_geometry_failures(tmp_path: Path) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin", "", "object_model = ArticulatedObject(name='unsafe')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", ] ), encoding="utf-8", ) with pytest.raises(RuntimeError, match="Missing required `run_tests\\(\\)`"): compile_urdf_report(script_path, ignore_geom_qc=True) def test_compile_urdf_report_rejects_removed_legacy_sdk_package(tmp_path: Path) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin", "", "object_model = ArticulatedObject(name='wrong_sdk')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", ] ), encoding="utf-8", ) with pytest.raises(ValueError, match="Unsupported SDK package"): compile_urdf_report(script_path, sdk_package=_REMOVED_PACKAGE, run_checks=False) def test_compile_urdf_report_visual_target_omits_collision_entries(tmp_path: Path) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin", "", "object_model = ArticulatedObject(name='visual_only')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", ] ), encoding="utf-8", ) report = compile_urdf_report(script_path, target="visual") assert " None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from pathlib import Path", "", "from sdk import ArticulatedObject, BoxGeometry, Mesh, Origin, mesh_from_geometry", "", "HERE = Path(__file__).resolve().parent", "mesh_from_geometry(", " BoxGeometry((0.1, 0.1, 0.1)),", " HERE / 'assets' / 'meshes' / 'part.obj',", ")", "object_model = ArticulatedObject(name='mesh_visual')", "base = object_model.part('base')", "base.visual(", " Mesh(filename='assets/meshes/part.obj'),", " origin=Origin(xyz=(0.0, 0.0, 0.0)),", ")", ] ), encoding="utf-8", ) report = compile_urdf_report(script_path, run_checks=False, target="visual") assert "assets/meshes/part.obj" in report.urdf_xml assert "assets/meshes/part.glb" not in report.urdf_xml assert not (tmp_path / "assets" / "meshes" / "part.glb").exists() assert report.signal_bundle.status == "success" def test_compile_urdf_report_auto_suffixes_managed_mesh_name_conflicts(tmp_path: Path) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, BoxGeometry, Origin, mesh_from_geometry", "", "mesh_a = mesh_from_geometry(BoxGeometry((0.1, 0.1, 0.1)), 'shared_name')", "mesh_b = mesh_from_geometry(BoxGeometry((0.2, 0.1, 0.1)), 'shared_name')", "object_model = ArticulatedObject(name='managed_mesh_conflict')", "base = object_model.part('base')", "base.visual(mesh_a, origin=Origin())", "base.visual(mesh_b, origin=Origin(xyz=(0.2, 0.0, 0.0)))", ] ), encoding="utf-8", ) report = compile_urdf_report( script_path, run_checks=False, target="visual", rewrite_visual_glb=False, ) assert "assets/meshes/shared_name.obj" in report.urdf_xml assert "assets/meshes/shared_name--" in report.urdf_xml assert len(list((tmp_path / "assets" / "meshes").glob("shared_name*.obj"))) == 2 assert report.signal_bundle.status == "success" def test_compile_urdf_report_can_opt_in_to_visual_glb_rewrite(tmp_path: Path) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from pathlib import Path", "", "from sdk import ArticulatedObject, BoxGeometry, Mesh, Origin, mesh_from_geometry", "", "HERE = Path(__file__).resolve().parent", "mesh_from_geometry(", " BoxGeometry((0.1, 0.1, 0.1)),", " HERE / 'assets' / 'meshes' / 'part.obj',", ")", "object_model = ArticulatedObject(name='mesh_visual')", "base = object_model.part('base')", "base.visual(", " Mesh(filename='assets/meshes/part.obj'),", " origin=Origin(xyz=(0.0, 0.0, 0.0)),", ")", ] ), encoding="utf-8", ) report = compile_urdf_report( script_path, run_checks=False, target="visual", rewrite_visual_glb=True, ) assert "assets/meshes/part.glb" in report.urdf_xml assert (tmp_path / "assets" / "meshes" / "part.glb").exists() assert report.signal_bundle.status == "success" def test_compile_urdf_report_preserves_export_exception_details( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin", "", "object_model = ArticulatedObject(name='export_failure')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", ] ), encoding="utf-8", ) import agent.compiler as compiler original_import_sdk_module = compiler._import_sdk_module class _FakeExportModule: @staticmethod def compile_object_to_urdf_xml(*_args, **_kwargs) -> str: raise RuntimeError("Standard_Failure: BRep_API: command not done") def fake_import_sdk_module(sdk_package: str, module_suffix: str = ""): if module_suffix == ".v0._urdf_export": return _FakeExportModule() return original_import_sdk_module(sdk_package, module_suffix) monkeypatch.setattr(compiler, "_import_sdk_module", fake_import_sdk_module) with pytest.raises(RuntimeError, match="Standard_Failure: BRep_API: command not done"): compile_urdf_report(script_path, run_checks=False) def test_compile_urdf_report_rejects_removed_collision_target(tmp_path: Path) -> None: script_path = tmp_path / "model.py" script_path.write_text( "\n".join( [ "from __future__ import annotations", "", "from sdk import ArticulatedObject, Box, Origin", "", "object_model = ArticulatedObject(name='full_only')", "base = object_model.part('base')", "base.visual(Box((0.1, 0.1, 0.1)), origin=Origin(xyz=(0.0, 0.0, 0.05)))", ] ), encoding="utf-8", ) with pytest.raises(ValueError, match="Unsupported compile target 'collision'"): compile_urdf_report(script_path, target="collision", run_checks=False)