from __future__ import annotations import sys import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "backend")) from app.services.feature_plan import FeaturePlanError, compute_node_statuses, validate_feature_plan # noqa: E402 class FeaturePlanTests(unittest.TestCase): def plan(self) -> dict: return { "schema_version": "cad.feature-plan.v1", "plan_id": "plan_test", "task_id": "cad_test", "nodes": [ {"id": "base", "atomic_id": "extrude_add_blind", "depends_on": []}, {"id": "round", "atomic_id": "fillet", "depends_on": ["base"], "requires_topology": True}, ], } def test_rejects_cycles_and_missing_dependencies(self) -> None: cyclic = self.plan() cyclic["nodes"][0]["depends_on"] = ["round"] with self.assertRaises(FeaturePlanError): validate_feature_plan(cyclic, supported_atomic_ids={"extrude_add_blind", "fillet"}) missing = self.plan() missing["nodes"][1]["depends_on"] = ["missing"] with self.assertRaises(FeaturePlanError): validate_feature_plan(missing, supported_atomic_ids={"extrude_add_blind", "fillet"}) def test_ready_prefix_waits_for_topology(self) -> None: statuses = compute_node_statuses(self.plan(), cdsl={"features": []}) self.assertEqual(statuses["ready_nodes"], ["base"]) self.assertEqual(statuses["waiting_nodes"], []) def test_built_prefix_unlocks_topology_dependent_node(self) -> None: statuses = compute_node_statuses( self.plan(), cdsl={"features": [{"id": "base"}]}, topology={"records": [{"record_id": "body:base", "kind": "body", "executable": True}]}, ) self.assertEqual(statuses["completed_nodes"], ["base"]) self.assertEqual(statuses["waiting_nodes"], ["round"]) def test_inspected_snapshot_unlocks_topology_dependent_node(self) -> None: plan = self.plan() plan["topology_snapshot_id"] = "cad_test/rev_001" statuses = compute_node_statuses( plan, cdsl={"features": [{"id": "base"}]}, topology={ "snapshot_id": "cad_test/rev_001", "records": [{"record_id": "body:base", "kind": "body", "executable": True}], }, ) self.assertEqual(statuses["ready_nodes"], ["round"]) self.assertEqual(statuses["waiting_nodes"], []) def test_dependency_must_appear_before_dependent_node(self) -> None: plan = self.plan() plan["nodes"] = [plan["nodes"][1], plan["nodes"][0]] with self.assertRaisesRegex(FeaturePlanError, "appear after dependency"): validate_feature_plan(plan, supported_atomic_ids={"extrude_add_blind", "fillet"})