from __future__ import annotations import asyncio from dataclasses import replace import json import sys import tempfile import unittest from pathlib import Path from unittest.mock import patch ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "backend")) from fastapi import HTTPException # noqa: E402 from app import main as api # noqa: E402 from app.services.cdsl_fragment import CdslFragmentError, cdsl_sha256, validate_fragment # noqa: E402 from app.services.generation_plan import GenerationPlanError, descendant_closure, mark_nodes_stale, validate_generation_plan # noqa: E402 from app.services.incremental_generation import IncrementalGenerationRunner # noqa: E402 from app.services.review_renderer import CANONICAL_VIEWS, REVIEW_SIZE, RENDER_SIZE, render_checkpoint # noqa: E402 from app.services.storage import WorkspaceStore # noqa: E402 from app.services.visual_review import _selected_review_views # noqa: E402 from app.settings import ProviderConfig, ProviderModel, Settings # noqa: E402 def plan() -> dict: return { "schema_version": "cad.generation-plan.v2", "plan_id": "incremental_test", "requirements": [ {"id": "req_base", "source": "explicit", "priority": "hard", "description": "base solid"}, {"id": "req_round", "source": "explicit", "priority": "hard", "description": "edge round"}, ], "assumptions": [], "nodes": [ { "id": "base", "intent": "base", "atomic_id": "extrude_add_blind", "depends_on": [], "requirement_ids": ["req_base"], "verification_rules": [], "review_targets": [], }, { "id": "round", "intent": "round", "atomic_id": "fillet", "depends_on": ["base"], "requires_topology": True, "requirement_ids": ["req_round"], "verification_rules": [], "review_targets": [], }, ], } class GenerationPlanTests(unittest.TestCase): def test_hard_requirements_and_backend_outputs_are_owned(self) -> None: invalid = plan() invalid["nodes"][1]["requirement_ids"] = [] with self.assertRaisesRegex(GenerationPlanError, "Hard requirements"): validate_generation_plan(invalid, supported_atomic_ids={"extrude_add_blind", "fillet"}, task_id="cad_abcdef123456") generated = validate_generation_plan(plan(), supported_atomic_ids={"extrude_add_blind", "fillet"}, task_id="cad_abcdef123456") self.assertEqual(generated["id_strategy"], "backend-derived-v1") self.assertEqual(len(generated["nodes"][0]["cdsl_feature_ids"]), 1) self.assertEqual(len(generated["nodes"][0]["cdsl_sketch_ids"]), 1) self.assertEqual(generated["nodes"][1]["cdsl_sketch_ids"], []) # Legacy fields from an untrusted model output cannot choose CDSL IDs. supplied = plan() supplied["nodes"][0]["expected_feature_ids"] = ["model_chosen_id"] self.assertEqual( validate_generation_plan(supplied, supported_atomic_ids={"extrude_add_blind", "fillet"}, task_id="cad_abcdef123456")["nodes"][0]["cdsl_feature_ids"], generated["nodes"][0]["cdsl_feature_ids"], ) def test_upstream_invalidation_marks_all_descendants_stale(self) -> None: spec = validate_generation_plan(plan(), supported_atomic_ids={"extrude_add_blind", "fillet"}, task_id="cad_abcdef123456") spec["nodes"][0]["status"] = "completed" spec["nodes"][1]["status"] = "completed" self.assertEqual(descendant_closure(spec, "base"), {"base", "round"}) stale = mark_nodes_stale(spec, "base", reason="topology_changed") self.assertTrue(all(node["stale"] for node in stale["nodes"])) self.assertTrue(all(node["status"] == "planned" for node in stale["nodes"])) class FragmentTests(unittest.TestCase): def setUp(self) -> None: self.spec = validate_generation_plan(plan(), supported_atomic_ids={"extrude_add_blind", "fillet"}, task_id="cad_abcdef123456") def base_fragment(self) -> dict: return { "schema_version": "cad.cdsl-fragment.v1", "node_id": "base", "base_revision_id": "", "base_cdsl_sha256": cdsl_sha256(None), "add_sketches": [{}], "add_features": [{}], "verification_rules": [], "assumptions": [], } def test_fragment_assigns_ids_dependencies_and_atomic_from_plan(self) -> None: accepted = validate_fragment(self.base_fragment(), plan=self.spec, node_id="base", base_revision_id="", base_cdsl=None) base = self.spec["nodes"][0] self.assertEqual(accepted["add_sketches"][0]["id"], base["cdsl_sketch_ids"][0]) self.assertEqual(accepted["add_features"][0]["id"], base["cdsl_feature_ids"][0]) self.assertEqual(accepted["add_features"][0]["sketch_id"], base["cdsl_sketch_ids"][0]) self.assertEqual(accepted["add_features"][0]["atomic_id"], "extrude_add_blind") self.assertEqual(accepted["add_features"][0]["depends_on"], []) model_ids = self.base_fragment() model_ids["add_sketches"][0]["id"] = "model_sketch" model_ids["add_features"][0].update({"id": "model_feature", "atomic_id": "fillet", "sketch_id": "model_sketch"}) overwritten = validate_fragment(model_ids, plan=self.spec, node_id="base", base_revision_id="", base_cdsl=None) self.assertEqual(overwritten["add_features"][0]["id"], base["cdsl_feature_ids"][0]) self.assertEqual(overwritten["add_features"][0]["atomic_id"], "extrude_add_blind") def test_fragment_rejects_wrong_hash_and_output_shape(self) -> None: wrong_hash = self.base_fragment() wrong_hash["base_cdsl_sha256"] = "0" * 64 with self.assertRaisesRegex(CdslFragmentError, "sha256"): validate_fragment(wrong_hash, plan=self.spec, node_id="base", base_revision_id="", base_cdsl=None) unexpected_sketch = self.base_fragment() unexpected_sketch["add_sketches"].append({"id": "extra"}) with self.assertRaisesRegex(CdslFragmentError, "one new sketch"): validate_fragment(unexpected_sketch, plan=self.spec, node_id="base", base_revision_id="", base_cdsl=None) def test_topology_fragment_requires_active_snapshot_owner_and_geometry(self) -> None: base = self.spec["nodes"][0] round_node = self.spec["nodes"][1] base_cdsl = { "geometry": {"sketches": [{"id": base["cdsl_sketch_ids"][0]}]}, "features": [{"id": base["cdsl_feature_ids"][0], "atomic_id": "extrude_add_blind"}], } fragment = { "schema_version": "cad.cdsl-fragment.v1", "node_id": "round", "base_revision_id": "rev_001", "base_cdsl_sha256": cdsl_sha256(base_cdsl), "required_snapshot_id": "cad_test/rev_001", "add_sketches": [], "add_features": [{ "selectors": [{ "kind": "edge", "stable_id": "body:feature_base:edge:0", "owner_node_id": "base", "snapshot_id": "cad_test/rev_001", "geometry": {"curve_type": "line", "length_mm": 10}, }], }], "verification_rules": [], "assumptions": [], } accepted = validate_fragment( fragment, plan=self.spec, node_id="round", base_revision_id="rev_001", base_cdsl=base_cdsl, required_snapshot_id="cad_test/rev_001", ) self.assertEqual(accepted["node_id"], "round") self.assertEqual(accepted["add_features"][0]["id"], round_node["cdsl_feature_ids"][0]) self.assertEqual(accepted["add_features"][0]["depends_on"], [base["cdsl_feature_ids"][0]]) self.assertEqual(accepted["add_features"][0]["selectors"][0]["owner_feature_id"], base["cdsl_feature_ids"][0]) fragment["add_features"][0]["selectors"][0].pop("geometry") with self.assertRaisesRegex(CdslFragmentError, "geometry signature"): validate_fragment( fragment, plan=self.spec, node_id="round", base_revision_id="rev_001", base_cdsl=base_cdsl, required_snapshot_id="cad_test/rev_001", ) class StorageAndRunnerTests(unittest.TestCase): def settings(self, root: Path) -> Settings: provider = ProviderConfig("author", "Author", "https://example.invalid/v1", "secret", (ProviderModel("author-model"),)) return Settings( task_root=root / "tasks", conversation_root=root / "conversations", library_root=root / "library", engine_root=ROOT / "backend" / "engine" / "cdsl_engine", llm_base_url="", llm_api_key="", llm_model="author-model", llm_timeout_s=1, default_provider_id="author", providers=(provider,), incremental_generation=True, ) def test_rollback_anchor_rewinds_before_affected_nodes(self) -> None: with tempfile.TemporaryDirectory() as directory: store = WorkspaceStore(self.settings(Path(directory))) task = store.ensure_task(None, "test") task_id = task["task_id"] for revision_id, parent, node_id in (("rev_001", "", "base"), ("rev_002", "rev_001", "middle"), ("rev_003", "rev_002", "tip")): store.update_task(task_id, {"revision_id": revision_id, "status": "success", "parent_revision_id": parent, "node_id": node_id, "visibility": "checkpoint"}) self.assertEqual(store.rollback_anchor_for_nodes(task_id, ["middle", "tip"], fallback_revision_id="rev_002"), "rev_001") self.assertEqual(store.rollback_anchor_for_nodes(task_id, ["base"], fallback_revision_id="rev_001"), "") store.rollback_to_revision(task_id, "rev_002", branch_id="branch_repair") revisions = {item["revision_id"]: item for item in (store.read_task(task_id) or {})["revisions"]} self.assertEqual(revisions["rev_001"]["visibility"], "checkpoint") self.assertEqual(revisions["rev_002"]["visibility"], "checkpoint") self.assertEqual(revisions["rev_003"]["visibility"], "superseded") def test_run_context_is_persisted_for_worker_recovery(self) -> None: with tempfile.TemporaryDirectory() as directory: store = WorkspaceStore(self.settings(Path(directory))) task = store.ensure_task(None, "test") store.write_generation_run_context(task["task_id"], { "schema_version": "cad.generation-run-context.v1", "request": "test", "conversation_id": "conv_abcdef123456", "provider_id": "author", "model_id": "author-model", "author_messages": [], "part_skills": {}, }) recovered = store.read_generation_run_context(task["task_id"]) self.assertEqual(recovered and recovered["request"], "test") task = store.start_generation(task["task_id"], request="test") self.assertEqual([item["task_id"] for item in store.running_tasks()], [task["task_id"]]) def test_missing_visual_review_configuration_fails_the_run(self) -> None: with tempfile.TemporaryDirectory() as directory: settings = self.settings(Path(directory)) store = WorkspaceStore(settings) task = store.ensure_task(None, "test") async def complete(*_args: object) -> dict: raise AssertionError("author must not be called before visual configuration validation") runner = IncrementalGenerationRunner(settings, store, complete) async def collect() -> list[tuple[str, dict]]: return [item async for item in runner.run( task_id=task["task_id"], request="test", conversation={"conversation_id": "", "attachments": []}, provider=settings.providers[0], model=settings.providers[0].models[0], author_messages=[], )] events = asyncio.run(collect()) self.assertEqual(events[-1][0], "task_terminal") self.assertEqual(events[-1][1]["lifecycle"], "failed") self.assertEqual((store.read_task(task["task_id"]) or {})["lifecycle"], "failed") def test_invalid_plan_is_preserved_in_failure_diagnostics(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) provider = ProviderConfig( "author", "Author", "https://example.invalid/v1", "secret", (ProviderModel("author-model", vision=True),), ) settings = replace( self.settings(root), providers=(provider,), review_provider_id="author", review_model_id="author-model", ) store = WorkspaceStore(settings) task = store.ensure_task(None, "test") raw_plan = {"schema_version": "cad.generation-plan.v2", "plan_id": "broken", "requirements": []} async def complete(*_args: object) -> dict: return { "choices": [{"message": {"tool_calls": [{"function": { "name": "plan_generation_task", "arguments": json.dumps(raw_plan), }}]}}], } runner = IncrementalGenerationRunner(settings, store, complete) engine = type("Engine", (), {"SUPPORTED_ATOMIC_IDS": ("extrude_add_blind",)})() async def collect() -> list[tuple[str, dict]]: return [item async for item in runner.run( task_id=task["task_id"], request="test", conversation={"conversation_id": "", "attachments": []}, provider=provider, model=provider.models[0], author_messages=[], )] with patch("app.services.incremental_generation.load_engine", return_value=engine), patch( "app.services.incremental_generation.renderer_status", return_value=(True, "") ): events = asyncio.run(collect()) self.assertEqual(events[-1], ("task_terminal", { "taskId": task["task_id"], "lifecycle": "failed", "message": "Generation plan requires a non-empty requirements array", })) failed_task = store.read_task(task["task_id"]) or {} failure_path = root / "tasks" / task["task_id"] / str(failed_task["run_failure_path"]) with failure_path.open(encoding="utf-8") as handle: failure = json.load(handle) diagnostic_path = root / "tasks" / task["task_id"] / str(failure["plan_diagnostic_path"]) with diagnostic_path.open(encoding="utf-8") as handle: diagnostic = json.load(handle) self.assertEqual(diagnostic["stage"], "generation_plan_validation") self.assertEqual(diagnostic["raw_plan"], raw_plan) class ArtifactAccessTests(unittest.TestCase): def test_checkpoint_allows_only_the_active_glb_until_publication(self) -> None: with tempfile.TemporaryDirectory() as directory: store = WorkspaceStore(StorageAndRunnerTests().settings(Path(directory))) task = store.ensure_task(None, "test") task_id = task["task_id"] revision_id, revision_dir = store.next_revision(task_id) glb = revision_dir / "model.glb" step = revision_dir / "model.step" glb.write_bytes(b"glb") step.write_bytes(b"step") store.update_task(task_id, { "revision_id": revision_id, "status": "success", "cdsl_path": f"revisions/{revision_id}/model.cdsl.json", "glb_path": f"revisions/{revision_id}/model.glb", "step_path": f"revisions/{revision_id}/model.step", "report_path": f"revisions/{revision_id}/rebuild-report.json", "visibility": "checkpoint", }) previous_store = api.store api.store = store try: response = asyncio.run(api.read_artifact(task_id, f"revisions/{revision_id}/model.glb")) self.assertEqual(response.media_type, "model/gltf-binary") with self.assertRaises(HTTPException) as rejected: asyncio.run(api.read_artifact(task_id, f"revisions/{revision_id}/model.step")) self.assertEqual(rejected.exception.status_code, 403) store.finish_generation(task_id, lifecycle="completed") response = asyncio.run(api.read_artifact(task_id, f"revisions/{revision_id}/model.step")) self.assertEqual(response.status_code, 200) finally: api.store = previous_store class TechnicalRenderTests(unittest.TestCase): def test_cpu_renderer_emits_fixed_views_and_keeps_detail_separate(self) -> None: from build123d import Box, export_step with tempfile.TemporaryDirectory() as directory: root = Path(directory) step_path = root / "box.step" export_step(Box(30, 20, 10), step_path) manifest = render_checkpoint( StorageAndRunnerTests().settings(root), step_path=step_path, output_dir=root / "review", review_targets=[{"bbox_mm": [-5, -5, -5, 5, 5, 5]}], ) self.assertEqual(manifest["renderer"], "python-occ-hlr-pillow") views = {item["id"]: item for item in manifest["views"]} self.assertEqual(set(CANONICAL_VIEWS), set(views) & set(CANONICAL_VIEWS)) self.assertIn("detail-1", views) self.assertNotEqual(views["isometric"]["path"], views["detail-1"]["path"]) self.assertTrue(all(Path(views[view_id]["path"]).is_file() for view_id in CANONICAL_VIEWS)) self.assertTrue(all(views[view_id]["diagnostics"]["valid"] for view_id in CANONICAL_VIEWS)) self.assertTrue(views["detail-1"]["diagnostics"]["intentional_crop"]) from PIL import Image with Image.open(views["isometric"]["path"]) as image: self.assertEqual(image.size, (REVIEW_SIZE, REVIEW_SIZE)) with Image.open(views["isometric"]["high_resolution_path"]) as image: self.assertEqual(image.size, (RENDER_SIZE, RENDER_SIZE)) def test_routine_visual_review_uses_compact_evidence(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) contact = root / "contact-sheet.jpg" contact.write_bytes(b"jpg") manifest = { "contact_sheet_path": str(contact), "views": [{"id": view_id, "path": str(root / f"{view_id}.png")} for view_id in (*CANONICAL_VIEWS, "detail-1", "detail-2")], } routine = _selected_review_views(manifest, final_checkpoint=False) final = _selected_review_views(manifest, final_checkpoint=True) self.assertEqual([item["id"] for item in routine], ["contact-sheet", "detail-1", "detail-2"]) self.assertEqual({item["id"] for item in final}, {"contact-sheet", "detail-1", "detail-2", *CANONICAL_VIEWS}) if __name__ == "__main__": unittest.main()