from __future__ import annotations import asyncio import json import sys import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "backend")) from app.services.agent_service import AgentService # noqa: E402 from app.services.engine_service import load_engine # noqa: E402 from app.services.library import CdslLibrary # noqa: E402 from app.services.part_skills import PartSkillLibrary # noqa: E402 from app.services.storage import WorkspaceStore # noqa: E402 from app.settings import ProviderConfig, ProviderModel, Settings # noqa: E402 from tests.test_design_intent import mounting_plate_cdsl, mounting_plate_intent # noqa: E402 BACKEND = ROOT / "backend" PART_SKILL_ROOT = BACKEND / "agent" / "skills" / "cad-engine" / "references" / "part-skills" class DesignIntentFlowTests(unittest.TestCase): def settings(self, root: Path) -> Settings: provider = ProviderConfig("test", "Test", "https://example.invalid/v1", "test-key", (ProviderModel("test-model"),)) return Settings( task_root=root / "tasks", conversation_root=root / "conversations", library_root=BACKEND / "cdsl_library", engine_root=BACKEND / "engine" / "cdsl_engine", llm_base_url=provider.base_url, llm_api_key=provider.api_key, llm_model="test-model", llm_timeout_s=1, default_provider_id="test", providers=(provider,), ) def test_design_intent_is_required_before_library_or_cdsl(self) -> None: with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) agent = AgentService(settings, store, CdslLibrary(settings), PartSkillLibrary(PART_SKILL_ROOT)) state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""} searched, _ = asyncio.run(agent._run_tool("search_cdsl_library", {"query": "mounting plate"}, "", "mounting plate", [], intent_state=state)) generated, _ = asyncio.run(agent._run_tool("generate_cdsl_model", {"design_intent_id": "intent_aaaaaaaaaaaa", "cdsl": {}, "summary": "x", "assumptions": []}, "", "mounting plate", [], intent_state=state)) self.assertEqual(searched["code"], "DESIGN_INTENT_REQUIRED") self.assertEqual(generated["code"], "DESIGN_INTENT_REQUIRED") self.assertEqual(list(settings.task_root.glob("cad_*")), []) def test_ready_plan_persists_before_build_and_links_success_revision(self) -> None: with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) skills = PartSkillLibrary(PART_SKILL_ROOT) agent = AgentService(settings, store, CdslLibrary(settings), skills) request = "Create a mounting plate with four holes and a center slot" state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""} planned, _ = asyncio.run(agent._run_tool( "propose_design_intent", {"intent": mounting_plate_intent(), "summary": "plate plan", "assumptions": []}, "", request, [], part_skill_selection=skills.select(request), intent_state=state, )) self.assertTrue(planned["ok"]) task = store.read_task(planned["task_id"]) self.assertEqual(task["current_revision"], "") planning_path = store.artifact_path(planned["task_id"], planned["design_intent_path"]) self.assertTrue(planning_path.is_file()) saved = json.loads(planning_path.read_text(encoding="utf-8")) self.assertEqual(saved["part_skill_ids"], skills.select(request)["skill_ids"]) searched, _ = asyncio.run(agent._run_tool("search_cdsl_library", {"query": "plate"}, planned["task_id"], request, [], intent_state=state)) self.assertTrue(searched["ok"]) result, built = asyncio.run(agent._run_tool( "generate_cdsl_model", {"design_intent_id": planned["design_intent_id"], "cdsl": mounting_plate_cdsl(), "summary": "plate", "assumptions": []}, planned["task_id"], request, [], part_skill_selection=skills.select(request), intent_state=state, )) self.assertTrue(result["ok"]) self.assertIsNotNone(built) revision = store.read_task(planned["task_id"])["revisions"][-1] self.assertEqual(revision["design_intent_id"], planned["design_intent_id"]) self.assertEqual(revision["design_intent_path"], planned["design_intent_path"]) audit = json.loads(store.artifact_path(planned["task_id"], revision["part_skills_path"]).read_text(encoding="utf-8")) self.assertEqual(audit["design_intent_id"], planned["design_intent_id"]) self.assertEqual(audit["design_intent_assumptions"], []) def test_blocked_plan_is_persisted_without_a_revision_and_new_plan_supersedes_it(self) -> None: with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) skills = PartSkillLibrary(PART_SKILL_ROOT) agent = AgentService(settings, store, CdslLibrary(settings), skills) request = "Create a mounting plate" state = {"phase": "WAITING_FOR_INTENT", "design_intent_id": ""} blocked_intent = mounting_plate_intent() blocked_intent["open_questions"] = [{"id": "thickness", "question": "Thickness?", "blocking": True}] blocked_intent["status"] = "needs_clarification" blocked, _ = asyncio.run(agent._run_tool( "propose_design_intent", {"intent": blocked_intent, "summary": "blocked", "assumptions": []}, "", request, [], part_skill_selection=skills.select(request), intent_state=state, )) self.assertEqual(blocked["code"], "DESIGN_INTENT_BLOCKED") task = store.read_task(blocked["task_id"]) self.assertEqual(task["revisions"], []) self.assertEqual(task["design_intents"][0]["status"], "pending") ready, _ = asyncio.run(agent._run_tool( "propose_design_intent", {"intent": mounting_plate_intent(), "summary": "ready", "assumptions": []}, blocked["task_id"], request, [], part_skill_selection=skills.select(request), intent_state=state, )) self.assertTrue(ready["ok"]) task = store.read_task(blocked["task_id"]) self.assertEqual(task["design_intents"][0]["status"], "superseded") self.assertEqual(task["design_intents"][-1]["status"], "accepted")