feat: extract workflow runtime phase 3 foundation
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
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",
|
||||
}
|
||||
Reference in New Issue
Block a user