wip: preserve local engine and agent workflow changes before ganjihong refactor integration
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
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")
|
||||
Reference in New Issue
Block a user