from __future__ import annotations import os import uuid from pathlib import Path import pytest from sqlalchemy import create_engine, text from sqlalchemy.orm import Session from app.database import Base from app.domains.auth.models import User, UserRole from app.domains.materials.models import AssetLibrary from app.domains.media.models import MediaAsset, MediaAssetType from app.domains.orders.models import Order, OrderLine, OrderStatus from app.domains.products.models import CadFile, Product from app.domains.rendering.models import OutputType, RenderTemplate from app.domains.rendering.workflow_runtime_services import ( prepare_order_line_render_context, resolve_order_line_template_context, ) import app.models # noqa: F401 TEST_DB_URL = os.environ.get( "TEST_DATABASE_URL", "postgresql+asyncpg://hartomat:hartomat@localhost:5432/hartomat_test", ).replace("+asyncpg", "") @pytest.fixture def sync_session(): engine = create_engine(TEST_DB_URL) with engine.begin() as conn: Base.metadata.create_all(conn) session = Session(engine) try: yield session finally: session.close() with engine.begin() as conn: conn.execute(text("DROP SCHEMA public CASCADE")) conn.execute(text("CREATE SCHEMA public")) engine.dispose() def _seed_order_line_graph(session: Session, tmp_path: Path) -> OrderLine: step_path = tmp_path / "parts" / "bearing.step" step_path.parent.mkdir(parents=True, exist_ok=True) step_path.write_text("STEP", encoding="utf-8") user = User( id=uuid.uuid4(), email=f"workflow-{uuid.uuid4().hex[:8]}@test.local", password_hash="hash", full_name="Workflow Tester", role=UserRole.admin, is_active=True, ) cad_file = CadFile( id=uuid.uuid4(), original_name="bearing.step", stored_path=str(step_path), file_hash=f"hash-{uuid.uuid4().hex}", parsed_objects={"objects": ["InnerRing", "OuterRing"]}, ) product = Product( id=uuid.uuid4(), pim_id="P-1000", name="Bearing A", category_key="bearings", cad_file_id=cad_file.id, cad_file=cad_file, cad_part_materials=[ {"part_name": "InnerRing", "material": "Steel raw"}, {"part_name": "OuterRing", "material": "Steel raw"}, ], ) output_type = OutputType( id=uuid.uuid4(), name=f"Still-{uuid.uuid4().hex[:6]}", renderer="blender", output_format="png", render_settings={"width": 1600, "height": 900}, material_override=None, ) order = Order( id=uuid.uuid4(), order_number=f"ORD-{uuid.uuid4().hex[:8]}", status=OrderStatus.processing, created_by=user.id, ) line = OrderLine( id=uuid.uuid4(), order_id=order.id, product_id=product.id, product=product, output_type_id=output_type.id, output_type=output_type, render_status="pending", ) session.add_all([user, cad_file, product, output_type, order, line]) session.commit() return line def test_prepare_order_line_render_context_marks_line_processing_and_prefers_usd(sync_session, tmp_path, monkeypatch): from app.config import settings monkeypatch.setattr(settings, "upload_dir", str(tmp_path / "uploads")) upload_dir = Path(settings.upload_dir) upload_dir.mkdir(parents=True, exist_ok=True) line = _seed_order_line_graph(sync_session, tmp_path) usd_asset_path = upload_dir / "usd" / "bearing.usd" usd_asset_path.parent.mkdir(parents=True, exist_ok=True) usd_asset_path.write_text("USD", encoding="utf-8") sync_session.add( MediaAsset( id=uuid.uuid4(), cad_file_id=line.product.cad_file_id, product_id=line.product_id, asset_type=MediaAssetType.usd_master, storage_key="usd/bearing.usd", ) ) sync_session.commit() messages: list[str] = [] result = prepare_order_line_render_context( sync_session, str(line.id), emit=lambda order_line_id, message, level=None: messages.append(message), ) sync_session.refresh(line) assert result.is_ready assert result.usd_render_path == usd_asset_path assert result.glb_reuse_path is None assert result.part_colors == { "InnerRing": "Steel raw", "OuterRing": "Steel raw", } assert line.render_status == "processing" assert line.render_backend_used == "celery" assert line.render_started_at is not None assert any("Using USD master for render" in message for message in messages) def test_prepare_order_line_render_context_skips_closed_orders(sync_session, tmp_path, monkeypatch): from app.config import settings monkeypatch.setattr(settings, "upload_dir", str(tmp_path / "uploads")) line = _seed_order_line_graph(sync_session, tmp_path) line.order.status = OrderStatus.completed sync_session.commit() result = prepare_order_line_render_context(sync_session, str(line.id)) sync_session.refresh(line) assert result.status == "skip" assert result.reason == "order_closed" assert line.render_status == "cancelled" def test_resolve_order_line_template_context_uses_exact_template_and_override(sync_session, tmp_path, monkeypatch): from app.config import settings monkeypatch.setattr(settings, "upload_dir", str(tmp_path / "uploads")) line = _seed_order_line_graph(sync_session, tmp_path) line.material_override = "HARTOMAT_OVERRIDE" sync_session.add( AssetLibrary( id=uuid.uuid4(), name="Default Library", blend_file_path="/libraries/materials.blend", is_active=True, ) ) template = RenderTemplate( id=uuid.uuid4(), name="Bearing Studio", category_key="bearings", blend_file_path="/templates/bearing.blend", original_filename="bearing.blend", target_collection="Product", material_replace_enabled=False, lighting_only=True, is_active=True, output_types=[line.output_type], ) sync_session.add(template) sync_session.commit() monkeypatch.setattr( "app.domains.rendering.workflow_runtime_services.resolve_material_map", lambda raw_map: {key: f"resolved:{value}" for key, value in raw_map.items()}, ) setup = prepare_order_line_render_context(sync_session, str(line.id)) result = resolve_order_line_template_context(sync_session, setup) assert result.template is not None assert result.template.name == "Bearing Studio" assert result.material_library == "/libraries/materials.blend" assert result.override_material == "HARTOMAT_OVERRIDE" assert result.use_materials is True assert result.material_map == { "InnerRing": "HARTOMAT_OVERRIDE", "OuterRing": "HARTOMAT_OVERRIDE", }