beac59fc8b
Phase 4 of the decoupling refactor (behavior-preserving):
- profile_schema.json: every operation contract now carries
runtime_capability {body_mutating, requires_active_body,
replayable, requires_selector, open_profile_ok} (single source of truth)
- operation_contracts.py: validates and forwards the flags
- capabilities.py: the five data-classification frozensets are now
derived from the schema at import time; dispatch-logic sets
(_HOLE_ATOMICS, _PATTERN_ATOMICS, extent constants) stay in code
- test_profile_schema.py: completeness + structural invariants test
Equivalence proven by flag counts (27/13/25/6/2) matching the previous
hand-written sets and by the unchanged test-baseline failure set.
204 lines
10 KiB
Python
204 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from app.services.engine_service import load_engine, validate_cdsl
|
|
from app.settings import get_settings
|
|
|
|
|
|
class ProfileSchemaTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.settings = get_settings()
|
|
cls.engine = load_engine(cls.settings)
|
|
cls.schema = json.loads((cls.settings.engine_root / "profile_schema.json").read_text(encoding="utf-8"))
|
|
cls.cdsl_schema = json.loads(
|
|
(cls.settings.engine_root / cls.schema["cdsl_json_schema_file"]).read_text(encoding="utf-8")
|
|
)
|
|
|
|
def test_schema_and_registered_profiles_stay_in_sync(self) -> None:
|
|
self.assertEqual(set(self.schema["runtime_supported_profiles"]), set(self.engine.SHAPE_GENERATORS))
|
|
|
|
def test_legacy_profile_macros_require_explicit_adapter_lowering(self) -> None:
|
|
legacy = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "legacy-rectangle",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [{
|
|
"id": "base", "workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "rectangle", "center": [0, 0], "width_mm": 10, "height_mm": 10},
|
|
}]},
|
|
"features": [{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "sketch_id": "base", "params": {"distance_mm": 2}}],
|
|
}
|
|
with self.assertRaisesRegex(ValueError, "CDSL schema violation"):
|
|
validate_cdsl(legacy, self.engine)
|
|
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
|
|
|
lowered = lower_legacy_profiles(legacy)
|
|
self.assertEqual(lowered["geometry"]["sketches"][0]["profile"]["type"], "analytic_contours")
|
|
validate_cdsl(lowered, self.engine)
|
|
|
|
def test_schema_and_executable_atomic_operations_stay_in_sync(self) -> None:
|
|
self.assertNotIn("runtime_supported_atomic_ids", self.schema)
|
|
self.assertNotIn("feature_atomic_ids", self.schema)
|
|
self.assertEqual(set(self.engine.SUPPORTED_ATOMIC_IDS), set(self.schema["operation_contracts"]))
|
|
self.assertEqual(
|
|
set(self.engine.SUPPORTED_ATOMIC_IDS),
|
|
set(self.engine.materialized_feature_contracts(self.schema)),
|
|
)
|
|
self.assertEqual(
|
|
set(self.cdsl_schema["$defs"]["feature_atomic_ids"]["enum"]),
|
|
set(self.engine.SUPPORTED_ATOMIC_IDS),
|
|
)
|
|
|
|
def test_machine_schema_and_human_contract_stay_in_sync(self) -> None:
|
|
self.assertEqual(
|
|
set(self.cdsl_schema["$defs"]["profile_type"]["enum"]),
|
|
set(self.schema["runtime_supported_profiles"]),
|
|
)
|
|
self.assertEqual(set(self.schema["profiles"]), set(self.schema["runtime_supported_profiles"]))
|
|
|
|
def test_runtime_capability_flags_are_complete_and_structurally_sound(self) -> None:
|
|
contracts = self.schema["operation_contracts"]
|
|
flags = ("body_mutating", "requires_active_body", "replayable", "requires_selector", "open_profile_ok")
|
|
for atomic_id, contract in contracts.items():
|
|
capability = contract.get("runtime_capability")
|
|
self.assertIsInstance(capability, dict, atomic_id)
|
|
for flag in flags:
|
|
self.assertIsInstance(capability.get(flag), bool, f"{atomic_id}.{flag}")
|
|
for atomic_id, contract in contracts.items():
|
|
capability = contract["runtime_capability"]
|
|
if capability["replayable"]:
|
|
# Only body-mutating features (or pattern replay itself) can be
|
|
# the source of another pattern replay.
|
|
self.assertTrue(
|
|
capability["body_mutating"] or atomic_id.startswith("pattern_"),
|
|
atomic_id,
|
|
)
|
|
if capability["requires_active_body"]:
|
|
self.assertTrue(capability["body_mutating"], atomic_id)
|
|
if capability["open_profile_ok"]:
|
|
self.assertTrue(atomic_id.startswith("extrude_cut"), atomic_id)
|
|
|
|
def test_rejects_unsupported_atomic_operation_before_rebuild(self) -> None:
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"part_id": "invalid-extrude",
|
|
"features": [{
|
|
"id": "f01", "atomic_id": "extrude", "depends_on": [],
|
|
"params": {"depth_mm": 10}, "sketch_id": "s01",
|
|
}],
|
|
"geometry": {"sketches": [{
|
|
"id": "s01", "workplane": {}, "profile": {"type": "circle", "radius_mm": 5},
|
|
}]},
|
|
}
|
|
with self.assertRaisesRegex(ValueError, "features\\[0\\]\\.atomic_id"):
|
|
validate_cdsl(cdsl, self.engine)
|
|
|
|
def test_semantic_validator_preserves_deferred_features_but_runtime_checks_current_capabilities(self) -> None:
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "part_id": "deferred-fillet",
|
|
"meta": {"unit": "mm"},
|
|
"features": [{
|
|
"id": "f01", "atomic_id": "fillet", "depends_on": [], "params": {"radius_mm": 1},
|
|
"execution_status": "deferred",
|
|
}],
|
|
"geometry": {"sketches": [{
|
|
"id": "s01", "workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 5},
|
|
}]},
|
|
}
|
|
result = self.engine.validate_semantic_cdsl(cdsl)
|
|
self.assertEqual(result["deferred_feature_ids"], ["f01"])
|
|
with self.assertRaisesRegex(ValueError, "missing_selector"):
|
|
validate_cdsl(cdsl, self.engine)
|
|
|
|
def test_rejects_bare_hole_coordinate_arrays_before_rebuild(self) -> None:
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"part_id": "invalid-hole-position",
|
|
"features": [{
|
|
"id": "f01", "atomic_id": "hole_blind", "depends_on": [], "sketch_id": "s01",
|
|
"params": {"diameter_mm": 6, "depth_mm": 10, "positions": [[0, 0]]},
|
|
}],
|
|
"geometry": {"sketches": [{
|
|
"id": "s01",
|
|
"workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "circle", "radius_mm": 20},
|
|
}]},
|
|
}
|
|
with self.assertRaisesRegex(ValueError, "positions\\[0\\].*not of type 'object'"):
|
|
validate_cdsl(cdsl, self.engine)
|
|
|
|
def test_cdsl_only_rebuild_preserves_its_actual_failure(self) -> None:
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1",
|
|
"part_id": "invalid-extrude",
|
|
"features": [{
|
|
"id": "f01", "atomic_id": "extrude", "depends_on": [],
|
|
"params": {"depth_mm": 10}, "sketch_id": "s01",
|
|
}],
|
|
"geometry": {"sketches": [{
|
|
"id": "s01", "workplane": {}, "profile": {"type": "circle", "radius_mm": 5},
|
|
}]},
|
|
}
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
out_step = Path(temporary_directory) / "model.step"
|
|
with self.assertRaisesRegex(RuntimeError, "CDSL-only rebuild failed: unsupported atomic_id: extrude"):
|
|
self.engine.run_rebuild(cdsl, out_step)
|
|
|
|
def test_hole_uses_inverse_host_face_normal_for_a_concave_l_bracket(self) -> None:
|
|
"""A local top face can be below the global body centre on an L part.
|
|
|
|
The drill must still enter its host face rather than point out into
|
|
empty space, otherwise the runtime reports a successful no-op hole.
|
|
"""
|
|
cdsl = {
|
|
"schema": "cad.cdsl.llm.v1", "schema_version": "1.1.0", "kind": "part", "part_id": "l-bracket-hole",
|
|
"meta": {"unit": "mm"},
|
|
"geometry": {"sketches": [
|
|
{
|
|
"id": "base", "workplane": {"origin_mm": [0, 0, 0], "x_dir": [1, 0, 0], "normal": [0, 0, 1]},
|
|
"profile": {"type": "polygon", "vertices": [[-25, 0], [25, 0], [25, 80], [-25, 80]]},
|
|
},
|
|
{
|
|
"id": "upright", "workplane": {"origin_mm": [0, 8, 0], "x_dir": [1, 0, 0], "normal": [0, -1, 0]},
|
|
"profile": {"type": "polygon", "vertices": [[-25, 0], [25, 0], [25, 60], [-25, 60]]},
|
|
},
|
|
]},
|
|
"features": [
|
|
{"id": "base_add", "atomic_id": "extrude_add_blind", "depends_on": [], "params": {"distance_mm": 8}, "sketch_id": "base"},
|
|
{"id": "upright_add", "atomic_id": "extrude_add_blind", "depends_on": ["base_add"], "params": {"distance_mm": 8}, "sketch_id": "upright"},
|
|
{
|
|
"id": "top_holes", "atomic_id": "hole_blind", "depends_on": ["upright_add"],
|
|
"params": {
|
|
"diameter_mm": 8, "depth_mm": 10,
|
|
"positions": [{"mm": [-15, 50, 0]}, {"mm": [15, 50, 0]}],
|
|
"host_face": {"frame": {"origin_mm": [0, 0, 8], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0], "normal": [0, 0, 1]}},
|
|
},
|
|
},
|
|
],
|
|
}
|
|
validate_cdsl(cdsl, self.engine)
|
|
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
result = self.engine.run_cdsl_only(cdsl, Path(temporary_directory) / "l-bracket.step")
|
|
self.assertAlmostEqual(float(result["volume_mm3"]), 52800 - 2 * 3.141592653589793 * 4 * 4 * 8, places=4)
|
|
self.assertEqual(int(result["solid_count"]), 1)
|
|
|
|
def test_all_official_samples_match_the_engine_schema(self) -> None:
|
|
from cdsl_importer.legacy_profile_adapter import lower_legacy_profiles
|
|
|
|
samples = sorted(self.settings.library_root.glob("samples/**/model.cdsl.json"))
|
|
self.assertGreater(len(samples), 0)
|
|
for sample_path in samples:
|
|
with self.subTest(sample=sample_path.parent.name):
|
|
legacy_sample = json.loads(sample_path.read_text(encoding="utf-8"))
|
|
lowered = lower_legacy_profiles(legacy_sample)
|
|
validate_cdsl(lowered, self.engine)
|
|
self.assertTrue(self.engine.analyze_cdsl(lowered).runtime_eligible)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|