Files
cdsl-cad/backend/tests/test_authoring_runtime.py

220 lines
9.0 KiB
Python

import math
from pathlib import Path
from tempfile import TemporaryDirectory
from app.cad_agent.adapters.artifact_store import FileArtifactStore
from app.cad_agent.adapters.runtime import ProfileCadRuntime
from app.cad_agent.ports import AdapterUnavailable
from app.cad_agent.application.single_stage import SingleStageExecutor
from app.services.engine_service import validate_cdsl, validate_cdsl_shape
from app.settings import get_settings
def _base_feature() -> dict:
return {
"name": "base",
"operation": "extrude_add_blind",
"params": {"distance_mm": 8, "result_mode": "new_body"},
"sketch": {
"workplane": {
"origin_mm": [0, 0, 0],
"x_dir": [1, 0, 0],
"normal": [0, 0, 1],
},
"profile": {
"type": "polygon",
"vertices": [[-40, -25], [40, -25], [40, 25], [-40, 25]],
},
},
}
def _authoring(*features: dict) -> dict:
return {
"schema_version": "cad.author.v1",
"units": "mm",
"bodies": [{"name": "main", "features": list(features)}],
}
def test_authoring_compiles_to_runtime_cdsl_and_publishes_one_revision() -> None:
runtime = ProfileCadRuntime(get_settings())
authoring = _authoring(_base_feature())
runtime_cdsl, audit = runtime.compile_authoring(authoring)
assert runtime_cdsl["schema"] == "cad.runtime.v1"
assert runtime_cdsl["bodies"] == [{"id": "body_001", "name": "main"}]
assert runtime_cdsl["features"][0]["id"] == "feature_001"
assert audit["feature_ids"] == {"base": "feature_001"}
validate_cdsl_shape(runtime_cdsl, runtime.engine)
validate_cdsl(runtime_cdsl, runtime.engine)
with TemporaryDirectory() as temporary:
artifacts = FileArtifactStore(Path(temporary) / "artifacts")
task_id = "cad_abcdefghijkl"
artifacts.initialize_task(task_id, "make a rectangular plate")
result = SingleStageExecutor(None, artifacts, runtime).execute(task_id, authoring)
assert result["status"] == "completed"
revision = str(result["revision_id"])
assert result["executed_feature_ids"] == ["feature_001"]
for relative in ("model.step", "model.glb", "model.cdsl.json", "rebuild-report.json", "renders/render-manifest.json"):
assert artifacts.artifact_path(task_id, f"revisions/{revision}/{relative}").is_file()
def test_selector_failure_keeps_the_successful_prefix() -> None:
runtime = ProfileCadRuntime(get_settings())
invalid_fillet = {
"name": "bad_fillet",
"operation": "fillet",
"depends_on": ["base"],
"params": {"radius_mm": 1},
# extrude.end is a face output. Using it for an edge-only operation
# is a deliberate selector-kind failure, not a geometry fallback.
"selectors": [{"kind": "edge", "source": "base.top_planar_face"}],
}
runtime_cdsl, _audit = runtime.compile_authoring(_authoring(_base_feature(), invalid_fillet))
with TemporaryDirectory() as temporary:
built, diagnostics = runtime.rebuild_best_effort(
runtime_cdsl, temporary, "cad_abcdefghijkl", "stage_selector_failure",
)
assert built["executed_feature_ids"] == ["feature_001"]
assert diagnostics[0]["feature_id"] == "feature_002"
assert diagnostics[0]["code"] == "SELECTOR_KIND_MISMATCH"
def test_cylinder_cap_selector_is_compiled_into_hole_host_face_and_executes() -> None:
runtime = ProfileCadRuntime(get_settings())
base = {
"name": "base_flange",
"operation": "cylinder_add",
"params": {
"radius_mm": 30,
"height_mm": 10,
"axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]},
},
}
bore = {
"name": "center_bore",
"operation": "hole_wizard",
"params": {
"hole_type": "simple",
"diameter_mm": 12,
"depth_mm": 10,
"end_condition": {"type": "through_all", "solidworks_code": 1},
"positions": [{"mm": [0, 0, 10]}],
},
"selectors": [{"kind": "face", "source": "base_flange.top_planar_face", "match": "unique"}],
}
runtime_cdsl, _audit = runtime.compile_authoring(_authoring(base, bore))
compiled_bore = runtime_cdsl["features"][1]
assert compiled_bore["depends_on"] == ["feature_001"]
assert "selectors" not in compiled_bore
assert compiled_bore["params"]["host_face"] == {
"kind": "face",
"output_role": "cylinder.end",
"owner_feature_id": "feature_001",
"source": "runtime_snapshot",
"confidence": 1.0,
"match_mode": "unique",
}
with TemporaryDirectory() as temporary:
built, diagnostics = runtime.rebuild_best_effort(
runtime_cdsl, temporary, "cad_abcdefghijkl", "stage_cylinder_host",
)
assert diagnostics == []
assert built["executed_feature_ids"] == ["feature_001", "feature_002"]
def test_authoring_circle_diameter_is_lowered_to_runtime_radius() -> None:
runtime = ProfileCadRuntime(get_settings())
authoring = _authoring({
"name": "round_base",
"operation": "extrude_add_blind",
"params": {"distance_mm": 8, "result_mode": "new_body"},
"sketch": {
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
"profile": {"type": "circle", "diameter_mm": 20, "center_mm": [0, 0]},
},
})
compiled, _audit = runtime.compile_authoring(authoring)
assert compiled["geometry"]["sketches"][0]["profile"] == {
"type": "circle", "center": [0.0, 0.0], "radius_mm": 10.0,
}
def test_flange_bolt_host_is_built_before_source_topology_is_replaced() -> None:
"""An exposed base cap remains a valid bolt host before later fusions/cuts."""
runtime = ProfileCadRuntime(get_settings())
circle_sketch = lambda z, normal, diameter: {
"workplane": {"origin_mm": [0, 0, z], "x_dir": [1, 0, 0], "normal": normal},
"profile": {"type": "circle", "diameter_mm": diameter, "center_mm": [0, 0]},
}
bolt_positions = [
{"mm": [43 * math.cos(math.radians(angle)), 43 * math.sin(math.radians(angle)), 12]}
for angle in range(0, 360, 45)
]
authoring = _authoring(
{
"name": "base_flange", "operation": "cylinder_add",
"params": {"radius_mm": 60, "height_mm": 12, "axis": {"origin_mm": [0, 0, 0], "direction": [0, 0, 1]}},
},
{
"name": "rear_shallow_pad", "operation": "extrude_add_blind", "depends_on": ["bolt_holes"],
"params": {"distance_mm": 4, "result_mode": "fuse", "reverse": False},
"sketch": circle_sketch(0, [0, 0, -1], 105),
},
{
"name": "rear_guide_boss", "operation": "extrude_add_blind", "depends_on": ["rear_shallow_pad"],
"params": {"distance_mm": 12, "result_mode": "fuse", "reverse": False},
"sketch": circle_sketch(0, [0, 0, -1], 38),
},
{
"name": "bolt_holes", "operation": "hole_wizard",
"params": {
"hole_type": "counterbore", "diameter_mm": 8, "depth_mm": 12,
"end_condition": {"type": "through_all_both", "solidworks_code": 7},
"counterbore": {"diameter_mm": 10, "depth_mm": 4}, "positions": bolt_positions,
},
"selectors": [{"kind": "face", "source": "base_flange.top_planar_face", "match": "unique"}],
},
{
"name": "front_hub_boss", "operation": "extrude_add_blind", "depends_on": ["bolt_holes"],
"params": {"distance_mm": 14, "result_mode": "fuse", "reverse": False},
"sketch": circle_sketch(12, [0, 0, 1], 56),
},
{
"name": "center_through_cut", "operation": "extrude_cut_through", "depends_on": ["front_hub_boss", "rear_guide_boss"],
"params": {"end_condition": {"type": "through_all_both", "solidworks_code": 7}},
"sketch": circle_sketch(0, [0, 0, 1], 32),
},
)
compiled, audit = runtime.compile_authoring(authoring)
assert audit["implicit_selector_dependencies"] == {"bolt_holes": ["base_flange"]}
with TemporaryDirectory() as temporary:
rebuilt = runtime.rebuild(compiled, temporary, "cad_abcdefghijkl", "flange_source_order")
assert rebuilt["health"]["feature_count"] == 6
def test_service_failure_during_prefix_export_is_not_reclassified_as_a_model_error(monkeypatch) -> None:
runtime = ProfileCadRuntime(get_settings())
runtime_cdsl, _audit = runtime.compile_authoring(_authoring(_base_feature()))
def unavailable(*_args, **_kwargs):
raise AdapterUnavailable("renderer unavailable")
monkeypatch.setattr(runtime, "rebuild", unavailable)
with TemporaryDirectory() as temporary:
try:
runtime.rebuild_best_effort(runtime_cdsl, temporary, "cad_abcdefghijkl", "stage_service_failure")
except AdapterUnavailable as error:
assert "renderer unavailable" in str(error)
else:
raise AssertionError("service failure was incorrectly converted into a model repair diagnostic")