from __future__ import annotations import uuid from contextlib import contextmanager from pathlib import Path from types import SimpleNamespace def _patch_render_thumbnail_dependencies(monkeypatch, tmp_path: Path): from app.domains.pipeline.tasks.render_thumbnail import render_graph_thumbnail, render_step_thumbnail step_path = tmp_path / "bearing.step" step_path.write_text("STEP", encoding="utf-8") usd_path = tmp_path / "bearing.usdc" usd_path.write_text("USD", encoding="utf-8") cad_file = SimpleNamespace( id=uuid.uuid4(), stored_path=str(step_path), step_file_hash=None, mesh_attributes={}, tenant_id=uuid.uuid4(), ) class _FakeSession: def get(self, _model, _object_id): return cad_file def commit(self): return None @contextmanager def _fake_pipeline_session(_tenant_id=None): yield _FakeSession() @contextmanager def _fake_sample_cap(): yield queued_glb_exports: list[str] = [] workflow_updates: list[tuple[str, str, str | None, str | None]] = [] postprocess_calls: list[str] = [] regenerate_calls: list[tuple[tuple, dict]] = [] monkeypatch.setattr( "app.domains.pipeline.tasks.render_thumbnail._pipeline_session", _fake_pipeline_session, ) monkeypatch.setattr( "app.domains.pipeline.tasks.render_thumbnail._capped_thumbnail_samples", _fake_sample_cap, ) monkeypatch.setattr( "app.core.tenant_context.resolve_tenant_id_for_cad", lambda cad_file_id: "tenant-1", ) monkeypatch.setattr( "app.domains.products.cache_service.compute_step_hash", lambda _path: "hash-123", ) monkeypatch.setattr( "app.services.step_processor.regenerate_cad_thumbnail", lambda *args, **kwargs: (regenerate_calls.append((args, kwargs)), True)[1], ) monkeypatch.setattr( "app.domains.pipeline.tasks.render_thumbnail._resolve_thumbnail_render_context", lambda _session, _cad: { "material_library_path": "/tmp/materials.blend", "material_map": {"part-a": "HARTOMAT_010101_Steel-Bare"}, "part_names_ordered": ["part-a", "part-b"], "usd_path": usd_path, }, ) monkeypatch.setattr( "app.services.step_processor.extract_mesh_edge_data", lambda _step_path: {"sharp_edge_pairs": []}, ) monkeypatch.setattr( "app.domains.rendering.workflow_runtime_services.resolve_cad_bbox", lambda step_path, glb_path=None: ( postprocess_calls.append("bbox"), SimpleNamespace( bbox_data={ "dimensions_mm": {"x": 1.0, "y": 2.0, "z": 3.0}, "bbox_center_mm": {"x": 0.5, "y": 1.0, "z": 1.5}, } ), )[1], ) monkeypatch.setattr( "app.domains.pipeline.tasks.extract_metadata._auto_populate_materials_for_cad", lambda cad_file_id, tenant_id=None: postprocess_calls.append("auto_populate"), ) monkeypatch.setattr( "app.core.websocket.publish_event_sync", lambda tenant_id, payload: postprocess_calls.append("websocket"), ) monkeypatch.setattr( "app.domains.pipeline.tasks.export_glb.generate_gltf_geometry_task.delay", lambda cad_file_id: queued_glb_exports.append(cad_file_id), ) monkeypatch.setattr( "app.domains.rendering.tasks._update_workflow_run_status", lambda order_line_id, status, error=None, *, workflow_run_id=None, workflow_node_id=None: workflow_updates.append( (order_line_id, status, workflow_run_id, workflow_node_id) ), ) return ( render_step_thumbnail, render_graph_thumbnail, queued_glb_exports, workflow_updates, postprocess_calls, regenerate_calls, ) def test_render_step_thumbnail_skips_legacy_glb_follow_up_for_graph_runs(monkeypatch, tmp_path): render_step_thumbnail, _render_graph_thumbnail, queued_glb_exports, workflow_updates, postprocess_calls, regenerate_calls = _patch_render_thumbnail_dependencies( monkeypatch, tmp_path, ) render_step_thumbnail.run( "cad-123", workflow_run_id="run-123", workflow_node_id="save-thumb", renderer="threejs", width=512, height=512, transparent_bg=True, ) assert queued_glb_exports == [] assert workflow_updates == [("cad-123", "completed", "run-123", "save-thumb")] assert postprocess_calls == ["bbox", "auto_populate", "websocket"] assert regenerate_calls == [( ("cad-123",), { "part_colors": {}, "renderer": "threejs", "render_engine": None, "samples": None, "width": 512, "height": 512, "transparent_bg": True, "material_library_path": "/tmp/materials.blend", "material_map": {"part-a": "HARTOMAT_010101_Steel-Bare"}, "part_names_ordered": ["part-a", "part-b"], "usd_path": tmp_path / "bearing.usdc", }, )] def test_render_step_thumbnail_keeps_legacy_glb_follow_up_without_workflow_run(monkeypatch, tmp_path): render_step_thumbnail, _render_graph_thumbnail, queued_glb_exports, workflow_updates, postprocess_calls, _regenerate_calls = _patch_render_thumbnail_dependencies( monkeypatch, tmp_path, ) render_step_thumbnail.run("cad-123") assert queued_glb_exports == ["cad-123"] assert workflow_updates == [("cad-123", "completed", None, None)] assert postprocess_calls == ["bbox", "auto_populate", "websocket"] def test_render_graph_thumbnail_skips_legacy_postprocess_and_glb_follow_up(monkeypatch, tmp_path): _render_step_thumbnail, render_graph_thumbnail, queued_glb_exports, workflow_updates, postprocess_calls, _regenerate_calls = _patch_render_thumbnail_dependencies( monkeypatch, tmp_path, ) render_graph_thumbnail.run( "cad-123", workflow_run_id="run-123", workflow_node_id="save-thumb", renderer="threejs", width=512, height=512, transparent_bg=True, ) assert queued_glb_exports == [] assert workflow_updates == [("cad-123", "completed", "run-123", "save-thumb")] assert postprocess_calls == [] def test_regenerate_thumbnail_skips_retry_for_missing_cad_resource(monkeypatch): from app.domains.pipeline.tasks.render_thumbnail import regenerate_thumbnail from app.services.step_processor import MissingCadResourceError retry_calls: list[tuple[tuple, dict]] = [] monkeypatch.setattr("app.core.task_logs.log_task_event", lambda *args, **kwargs: None) monkeypatch.setattr( "app.core.tenant_context.resolve_tenant_id_for_cad", lambda cad_file_id: "tenant-1", ) monkeypatch.setattr( "app.services.step_processor.regenerate_cad_thumbnail", lambda *args, **kwargs: (_ for _ in ()).throw(MissingCadResourceError("CAD file not found: cad-123")), ) monkeypatch.setattr( regenerate_thumbnail, "retry", lambda *args, **kwargs: retry_calls.append((args, kwargs)), ) regenerate_thumbnail.run( "cad-123", {}, renderer="blender", width=512, height=512, ) assert retry_calls == []