from __future__ import annotations import importlib.util import json import sys import tempfile import unittest from pathlib import Path ROUTE_PATH = Path(__file__).resolve().parents[4] / "skills" / "cad-router" / "scripts" / "route.py" SPEC = importlib.util.spec_from_file_location("cad_router_route", ROUTE_PATH) assert SPEC is not None and SPEC.loader is not None route_module = importlib.util.module_from_spec(SPEC) sys.modules[SPEC.name] = route_module SPEC.loader.exec_module(route_module) class CadRouterTests(unittest.TestCase): def parse(self, *args: str): return route_module.build_parser().parse_args(list(args)) def test_routes_gear_to_simplecadapi(self) -> None: result = route_module.route(self.parse("生成一个模数 2 的人字齿轮")) self.assertEqual("simplecadapi", result["selected_backend"]) def test_routes_new_standard_factories_to_simplecadapi(self) -> None: for request in ( "生成一个 M8 六角螺栓", "生成一个 08B 滚子链轮", "生成一个模数 2 的直齿锥齿轮", ): with self.subTest(request=request): result = route_module.route(self.parse(request)) self.assertEqual("simplecadapi", result["selected_backend"]) def test_routes_semantic_units_and_tolerances_to_simplecadapi(self) -> None: result = route_module.route( self.parse("生成参数化法兰,保留语义标签、毫米单位和公差链") ) self.assertEqual("simplecadapi", result["selected_backend"]) self.assertIn( "semantic_graph_and_replay", result["routing_policy"]["requirements"], ) def test_routes_custom_freeform_surface_to_build123d(self) -> None: result = route_module.route( self.parse("生成带 NURBS 自由曲面的定制外壳") ) self.assertEqual("build123d", result["selected_backend"]) def test_routes_connecting_rod_to_build123d(self) -> None: result = route_module.route(self.parse("生成中心距 120mm 的发动机连杆")) self.assertEqual("build123d", result["selected_backend"]) def test_existing_step_stays_step_first(self) -> None: result = route_module.route(self.parse("修改现有零件的孔径", "--existing-model")) self.assertEqual("build123d", result["selected_backend"]) def test_uploaded_step_uses_surfaceir_and_simplecad_brep_validation(self) -> None: result = route_module.route( self.parse( "重建上传的零件", "--edit-source", "/tmp/source.step", ) ) policy = result["routing_policy"] self.assertEqual("surfaceir", policy["effective_execution_engine"]) self.assertEqual( [ "surfaceir_geometry_acceptance", "simplecadapi_brep_compare", ], policy["validation_engines"], ) self.assertEqual( "SimpleCADAPI 2.0.2", result["execution_adapter"]["supplemental_brep_acceptance"], ) def test_existing_designir_preserves_simplecadapi_backend(self) -> None: with tempfile.TemporaryDirectory() as directory: source = Path(directory) / "part.designir.json" source.write_text( json.dumps( { "schema_version": "3.0", "backend_hint": "simplecadapi", } ), encoding="utf-8", ) result = route_module.route( self.parse("把孔径增加 1mm", "--edit-source", str(source)) ) self.assertEqual("simplecadapi", result["selected_backend"]) self.assertEqual( "simplecadapi", result["routing_policy"]["native_source_backend"], ) def test_every_route_has_fixed_step_artifact_contract(self) -> None: result = route_module.route(self.parse("做一个参数化旋钮")) self.assertEqual( { "primary_format": "step", "editable_contract": "DesignIR 3.0", }, result["artifact_contract"], ) def test_every_route_reports_only_runtime_project_roles(self) -> None: result = route_module.route(self.parse("生成一个 40mm 法兰")) self.assertEqual( {"text-to-cad", "SimpleCADAPI"}, set(result["project_contributions"]), ) def test_explicit_override_is_respected(self) -> None: result = route_module.route(self.parse("生成一个齿轮", "--backend", "build123d")) self.assertEqual("build123d", result["selected_backend"]) selected = next(item for item in result["backend_scores"] if item["backend"] == "build123d") self.assertIn("explicit backend override", selected["reasons"]) def test_probe_reports_simplecadapi_202_capability_contract(self) -> None: result = route_module.route(self.parse("生成一个齿轮", "--probe")) simplecad = result["availability"]["simplecadapi"] self.assertTrue(simplecad["available"]) self.assertTrue(simplecad["compatible"]) self.assertEqual("2.0.2", simplecad["minimum_version"]) def test_generalized_experience_is_loaded_without_case_geometry(self) -> None: payload = { "schema_version": "2.0", "library_kind": "generalized_cad_experience", "policy": { "instance_parameters_allowed": False, "absolute_coordinates_allowed": False, "single_case_promotion_allowed": False, "llm_semantic_review_required": True, "draft_evidence_verified": True, "router_consumable": True, }, "experiences": [ { "id": "constraint.coaxial_stack", "kind": "constraint", "scope": ["flanged_hub_adapter"], "when": {"relation": "coaxial_stack"}, "guidance": "Preserve the semantic relationship.", "support": 40, "confidence": 0.95, } ], } with tempfile.TemporaryDirectory() as directory: library = Path(directory) / "library.json" library.write_text(json.dumps(payload), encoding="utf-8") result = route_module.route( self.parse( "根据经验生成一个法兰轴套", "--experience-library", str(library), "--experience-family", "flanged_hub_adapter", ) ) context = result["experience_context"] self.assertEqual("flanged_hub_adapter", context["family"]) self.assertEqual(1, len(context["experiences"])) self.assertFalse(context["policy"]["contains_instance_parameters"]) def test_case_parameters_are_rejected_as_experience(self) -> None: payload = { "schema_version": "2.0", "library_kind": "generalized_cad_experience", "policy": { "instance_parameters_allowed": False, "absolute_coordinates_allowed": False, "single_case_promotion_allowed": False, "llm_semantic_review_required": True, "draft_evidence_verified": True, "router_consumable": True, }, "parameters": {"base_outer_diameter": 91.0}, "experiences": [], } with tempfile.TemporaryDirectory() as directory: library = Path(directory) / "library.json" library.write_text(json.dumps(payload), encoding="utf-8") with self.assertRaisesRegex(ValueError, "forbidden instance key"): route_module.load_experience_library(library) def test_candidate_experience_is_visible_but_not_consumed(self) -> None: payload = { "schema_version": "2.0", "library_kind": "generalized_cad_experience", "policy": { "instance_parameters_allowed": False, "absolute_coordinates_allowed": False, "single_case_promotion_allowed": False, "llm_semantic_review_required": True, "draft_evidence_verified": True, "router_consumable": True, }, "experiences": [], "candidate_experiences": [ { "id": "motif.flange.base_flange+hollow_sleeve", "kind": "feature_motif", "scope": ["flange"], "when": {"features": ["base_flange", "hollow_sleeve"]}, "guidance": "Candidate only.", "support": 1, "confidence": 1.0, "promotion_state": "candidate", } ], } with tempfile.TemporaryDirectory() as directory: library = Path(directory) / "library.json" library.write_text(json.dumps(payload), encoding="utf-8") context = route_module.load_experience_library( library, "flange", ["base_flange", "hollow_sleeve"] ) self.assertEqual([], context["experiences"]) def test_unreviewed_statistical_library_is_rejected(self) -> None: payload = { "schema_version": "2.0", "library_kind": "generalized_cad_experience", "policy": { "instance_parameters_allowed": False, "absolute_coordinates_allowed": False, "single_case_promotion_allowed": False, }, "experiences": [], } with tempfile.TemporaryDirectory() as directory: library = Path(directory) / "library.json" library.write_text(json.dumps(payload), encoding="utf-8") with self.assertRaisesRegex(ValueError, "LLM semantic review"): route_module.load_experience_library(library) def test_request_automatically_builds_experience_query_and_design_plan(self) -> None: payload = { "schema_version": "2.0", "library_kind": "generalized_cad_experience", "policy": { "instance_parameters_allowed": False, "absolute_coordinates_allowed": False, "single_case_promotion_allowed": False, "llm_semantic_review_required": True, "draft_evidence_verified": True, "router_consumable": True, }, "experiences": [ { "id": "motif.base_flange+hollow_sleeve", "kind": "feature_motif", "scope": ["flanged_hub_adapter"], "when": {"features": ["base_flange", "hollow_sleeve"]}, "guidance": "Treat these feature roles as a reusable composition.", "support": 24, "confidence": 0.9, } ], } with tempfile.TemporaryDirectory() as directory: library = Path(directory) / "library.json" library.write_text(json.dumps(payload), encoding="utf-8") result = route_module.route( self.parse( "生成一个带法兰轴套和中心通孔", "--experience-library", str(library), ) ) context = result["experience_context"] plan = result["design_plan"] self.assertEqual("flanged_hub_adapter", context["family"]) self.assertIn("base_flange", context["requested_features"]) self.assertIn("hollow_sleeve", context["requested_features"]) self.assertEqual(1, len(context["experiences"])) self.assertEqual("matched", plan["experience_status"]) self.assertEqual( "motif.base_flange+hollow_sleeve", plan["generalized_methods"][0]["id"], ) def test_design_plan_exposes_promoted_reconstruction_grammar(self) -> None: payload = { "schema_version": "2.0", "library_kind": "generalized_cad_experience", "policy": { "instance_parameters_allowed": False, "absolute_coordinates_allowed": False, "single_case_promotion_allowed": False, "llm_semantic_review_required": True, "draft_evidence_verified": True, "router_consumable": True, }, "experiences": [ { "id": "reconstruction.flanged_hub_adapter.primary", "kind": "reconstruction_grammar", "scope": ["flanged_hub_adapter"], "when": {"features": ["base_flange", "hollow_sleeve"]}, "guidance": "Establish shared datums before composing the envelope.", "reconstruction_grammar": { "parameter_roles": ["overall_long_span"], "datum_roles": ["part_center", "primary_axis"], "feature_roles": ["base_flange", "hollow_sleeve"], "relation_roles": [], "canonical_stages": [ { "id": "establish_reference_frame", "operation": "define_datums", "feature_roles": [], "reference_roles": ["part_center", "primary_axis"], } ], "cardinality_classes": ["cylinder_dense"], "validation_roles": ["closed_solid"], }, "support": 30, "confidence": 0.9, } ], } with tempfile.TemporaryDirectory() as directory: library = Path(directory) / "library.json" library.write_text(json.dumps(payload), encoding="utf-8") result = route_module.route( self.parse( "生成一个带法兰轴套", "--experience-library", str(library), ) ) grammars = result["design_plan"]["reconstruction_grammars"] self.assertEqual(1, len(grammars)) self.assertEqual(["overall_long_span"], grammars[0]["parameter_roles"]) def test_request_family_matches_geometry_facing_distilled_scope(self) -> None: payload = { "schema_version": "2.0", "library_kind": "generalized_cad_experience", "policy": { "instance_parameters_allowed": False, "absolute_coordinates_allowed": False, "single_case_promotion_allowed": False, "llm_semantic_review_required": True, "draft_evidence_verified": True, "router_consumable": True, }, "experiences": [ { "id": "motif.patterned_plate.holes", "kind": "feature_motif", "scope": ["patterned_plate"], "when": { "features": [ "planar_dominant_body", "repeated_axial_hole_pattern", ] }, "guidance": "Build the plate first, then place holes from symmetry.", "support": 40, "confidence": 0.94, } ], } with tempfile.TemporaryDirectory() as directory: library = Path(directory) / "library.json" library.write_text(json.dumps(payload), encoding="utf-8") result = route_module.route( self.parse( "Generate a mounting plate with four corner holes", "--experience-library", str(library), ) ) context = result["experience_context"] self.assertEqual("plate", context["family"]) self.assertIn("patterned_plate", context["compatible_scopes"]) self.assertIn("planar_dominant_body", context["requested_features"]) self.assertIn( "repeated_axial_hole_pattern", context["requested_features"] ) self.assertEqual( ["motif.patterned_plate.holes"], [item["id"] for item in context["experiences"]], ) def test_step_edit_uses_private_evidence_and_isolated_designir(self) -> None: result = route_module.route( self.parse( "把中心孔径调整大一些", "--edit-source", "/tmp/source.step", ) ) plan = result["design_plan"] self.assertEqual("modify", plan["operation"]) self.assertEqual( "private_evidence_to_isolated_designir_reconstruction", plan["edit_context"]["modification_mode"], ) forbidden = plan["edit_context"]["forbidden_knowledge_sources"] self.assertIn("teacher STEP during reconstruction", forbidden) self.assertIn( "deterministically extracted private teacher evidence", plan["edit_context"]["allowed_knowledge_sources"], ) self.assertEqual( "pending_source_classification", plan["experience_status"] ) self.assertTrue( plan["experience_query_plan"]["requires_source_inspection"] ) self.assertEqual( "DesignIR 3.0", result["artifact_contract"]["editable_contract"] ) self.assertEqual( "DesignIR 3.0 / SurfaceIR", result["execution_adapter"]["runtime"], ) self.assertEqual( "surfaceir_runtime_conditioned", result["experience_context"]["selection_policy"], ) self.assertGreater( len(result["experience_context"]["experiences"]), 0 ) def test_execute_requires_step_source_and_task_directory(self) -> None: args = self.parse("生成一个法兰", "--execute") result = route_module.route(args) with self.assertRaisesRegex(ValueError, "requires --edit-source"): route_module.execute_reconstruction(args, result) def test_native_generator_is_preferred_for_modification(self) -> None: result = route_module.route( self.parse( "把孔径改为 12mm", "--edit-source", "/tmp/model.py", ) ) self.assertEqual( "native_parameter_edit", result["design_plan"]["edit_context"]["modification_mode"], ) def test_teacher_vault_is_rejected_as_reconstruction_source(self) -> None: teacher_source = ( Path(__file__).resolve().parents[5] / "designir-pipeline" / "input" / "part.step" ) with self.assertRaisesRegex(ValueError, "cannot read distillation input"): route_module.route( self.parse( "修改这个零件", "--edit-source", str(teacher_source), ) ) def test_private_run_artifact_is_rejected_as_edit_source(self) -> None: run_source = ( Path(__file__).resolve().parents[5] / "designir-pipeline" / "runs" / "designir" / "part.designir.json" ) with self.assertRaisesRegex(ValueError, "cannot read distillation input"): route_module.route( self.parse( "修改这个零件", "--edit-source", str(run_source), ) ) if __name__ == "__main__": unittest.main()