fix: align workflow material resolution with scene manifest
This commit is contained in:
@@ -5,10 +5,9 @@ import uuid
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine, select, text
|
||||
from sqlalchemy import select, 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
|
||||
@@ -32,25 +31,13 @@ from app.domains.rendering.workflow_runtime_services import (
|
||||
)
|
||||
from app.domains.tenants.models import Tenant
|
||||
|
||||
import app.models # noqa: F401
|
||||
from tests.db_test_utils import reset_public_schema_sync, resolve_test_db_url
|
||||
from tests.db_test_utils import sync_test_session as sync_test_session_ctx
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sync_session():
|
||||
engine = create_engine(resolve_test_db_url(async_driver=False))
|
||||
with engine.begin() as conn:
|
||||
reset_public_schema_sync(conn)
|
||||
Base.metadata.create_all(conn)
|
||||
|
||||
session = Session(engine)
|
||||
try:
|
||||
with sync_test_session_ctx() as session:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
with engine.begin() as conn:
|
||||
reset_public_schema_sync(conn)
|
||||
engine.dispose()
|
||||
|
||||
|
||||
def _seed_order_line_graph(session: Session, tmp_path: Path) -> OrderLine:
|
||||
@@ -358,7 +345,7 @@ def test_build_order_line_render_invocation_applies_output_and_line_overrides(tm
|
||||
assert invocation.transparent_bg is True
|
||||
assert invocation.cycles_device == "cuda"
|
||||
assert invocation.bg_color == "#202020"
|
||||
assert invocation.turntable_axis == "world_y"
|
||||
assert invocation.turntable_axis == "world_z"
|
||||
assert invocation.template_path == "/templates/studio.blend"
|
||||
assert invocation.target_collection == "Assembly"
|
||||
assert invocation.material_library_path == "/libraries/materials.blend"
|
||||
@@ -552,6 +539,70 @@ def test_resolve_order_line_template_context_uses_exact_template_and_override(sy
|
||||
}
|
||||
|
||||
|
||||
def test_resolve_order_line_template_context_supports_explicit_template_and_library_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)
|
||||
template = RenderTemplate(
|
||||
id=uuid.uuid4(),
|
||||
name="Forced Template",
|
||||
category_key=None,
|
||||
blend_file_path="/templates/forced.blend",
|
||||
original_filename="forced.blend",
|
||||
target_collection="ForcedCollection",
|
||||
material_replace_enabled=True,
|
||||
lighting_only=False,
|
||||
is_active=True,
|
||||
)
|
||||
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,
|
||||
template_id_override=str(template.id),
|
||||
material_library_path_override="/custom/library.blend",
|
||||
require_template=True,
|
||||
)
|
||||
|
||||
assert result.template is not None
|
||||
assert result.template.id == template.id
|
||||
assert result.material_library == "/custom/library.blend"
|
||||
assert result.use_materials is True
|
||||
assert result.material_map == {
|
||||
"InnerRing": "resolved:Steel raw",
|
||||
"OuterRing": "resolved:Steel raw",
|
||||
}
|
||||
|
||||
|
||||
def test_resolve_order_line_template_context_can_disable_material_resolution(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)
|
||||
setup = prepare_order_line_render_context(sync_session, str(line.id))
|
||||
|
||||
result = resolve_order_line_template_context(
|
||||
sync_session,
|
||||
setup,
|
||||
disable_materials=True,
|
||||
)
|
||||
|
||||
assert result.use_materials is False
|
||||
assert result.material_map is None
|
||||
|
||||
|
||||
def test_resolve_order_line_material_map_disables_materials_when_template_blocks_replacement(
|
||||
sync_session,
|
||||
tmp_path,
|
||||
@@ -615,6 +666,109 @@ def test_resolve_order_line_material_map_prefers_line_override_over_output_overr
|
||||
}
|
||||
|
||||
|
||||
def test_resolve_order_line_material_map_prefers_authoritative_scene_manifest_assignments(
|
||||
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)
|
||||
cad_file = line.product.cad_file
|
||||
assert cad_file is not None
|
||||
|
||||
cad_file.resolved_material_assignments = {
|
||||
"inner_ring": {
|
||||
"source_name": "InnerRing",
|
||||
"prim_path": "/Root/Assembly/inner_ring",
|
||||
"canonical_material": "HARTOMAT_010101_Steel-Bare",
|
||||
},
|
||||
"outer_ring": {
|
||||
"source_name": "OuterRing",
|
||||
"prim_path": "/Root/Assembly/outer_ring",
|
||||
"canonical_material": "HARTOMAT_020202_Rubber-Black",
|
||||
},
|
||||
}
|
||||
cad_file.manual_material_overrides = {
|
||||
"outer_ring": "HARTOMAT_020203_Rubber-Black-Gloss",
|
||||
}
|
||||
line.product.cad_part_materials = [
|
||||
{"part_name": "InnerRing", "material": "Legacy Steel"},
|
||||
{"part_name": "OuterRing", "material": "Legacy Rubber"},
|
||||
]
|
||||
sync_session.commit()
|
||||
|
||||
result = resolve_order_line_material_map(
|
||||
line,
|
||||
cad_file,
|
||||
line.product.cad_part_materials,
|
||||
material_library="/libraries/materials.blend",
|
||||
template=None,
|
||||
)
|
||||
|
||||
assert result.use_materials is True
|
||||
assert result.source_material_count == 4
|
||||
assert result.material_map == {
|
||||
"InnerRing": "HARTOMAT_010101_Steel-Bare",
|
||||
"inner_ring": "HARTOMAT_010101_Steel-Bare",
|
||||
"OuterRing": "HARTOMAT_020203_Rubber-Black-Gloss",
|
||||
"outer_ring": "HARTOMAT_020203_Rubber-Black-Gloss",
|
||||
}
|
||||
|
||||
|
||||
def test_resolve_order_line_material_map_keeps_legacy_source_name_fallback_without_scene_manifest(
|
||||
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)
|
||||
|
||||
result = resolve_order_line_material_map(
|
||||
line,
|
||||
line.product.cad_file,
|
||||
line.product.cad_part_materials,
|
||||
material_library="/libraries/materials.blend",
|
||||
template=None,
|
||||
)
|
||||
|
||||
assert result.use_materials is True
|
||||
assert result.source_material_count == 2
|
||||
assert result.material_map == {
|
||||
"InnerRing": "Steel raw",
|
||||
"OuterRing": "Steel raw",
|
||||
}
|
||||
|
||||
|
||||
def test_resolve_order_line_material_map_allows_node_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 = "LINE_OVERRIDE"
|
||||
line.output_type.material_override = "OUTPUT_OVERRIDE"
|
||||
sync_session.commit()
|
||||
|
||||
result = resolve_order_line_material_map(
|
||||
line,
|
||||
line.product.cad_file,
|
||||
line.product.cad_part_materials,
|
||||
material_library="/libraries/materials.blend",
|
||||
template=None,
|
||||
material_override="NODE_OVERRIDE",
|
||||
)
|
||||
|
||||
assert result.override_material == "NODE_OVERRIDE"
|
||||
assert result.use_materials is True
|
||||
assert result.material_map == {
|
||||
"InnerRing": "NODE_OVERRIDE",
|
||||
"OuterRing": "NODE_OVERRIDE",
|
||||
}
|
||||
|
||||
|
||||
def test_auto_populate_materials_for_cad_updates_only_blank_products_and_queues_once(sync_session, tmp_path):
|
||||
line = _seed_order_line_graph(sync_session, tmp_path)
|
||||
cad_file = line.product.cad_file
|
||||
@@ -699,6 +853,32 @@ def test_auto_populate_materials_for_cad_skips_when_materials_already_present(sy
|
||||
]
|
||||
|
||||
|
||||
def test_auto_populate_materials_for_cad_can_rewrite_existing_assignments(sync_session, tmp_path):
|
||||
line = _seed_order_line_graph(sync_session, tmp_path)
|
||||
cad_file = line.product.cad_file
|
||||
assert cad_file is not None
|
||||
|
||||
line.product.components = [
|
||||
{"part_name": "InnerRing", "material": "Ceramic"},
|
||||
{"part_name": "OuterRing", "material": "Polymer"},
|
||||
]
|
||||
sync_session.commit()
|
||||
|
||||
result = auto_populate_materials_for_cad(
|
||||
sync_session,
|
||||
str(cad_file.id),
|
||||
include_populated_products=True,
|
||||
)
|
||||
|
||||
sync_session.refresh(line.product)
|
||||
|
||||
assert result.updated_product_ids == [str(line.product.id)]
|
||||
assert line.product.cad_part_materials == [
|
||||
{"part_name": "InnerRing", "material": "Ceramic"},
|
||||
{"part_name": "OuterRing", "material": "Polymer"},
|
||||
]
|
||||
|
||||
|
||||
def test_resolve_cad_bbox_prefers_glb_over_step(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"app.domains.rendering.workflow_runtime_services.extract_bbox_from_glb",
|
||||
@@ -897,6 +1077,39 @@ def test_persist_order_line_output_canonicalizes_step_file_outputs(sync_session,
|
||||
assert asset.storage_key == f"renders/{line.id}/{expected_path.name}"
|
||||
|
||||
|
||||
def test_persist_order_line_output_classifies_blend_outputs_as_blend_assets(sync_session, tmp_path, monkeypatch):
|
||||
from app.config import settings
|
||||
|
||||
upload_dir = tmp_path / "uploads"
|
||||
monkeypatch.setattr(settings, "upload_dir", str(upload_dir))
|
||||
line = _seed_order_line_graph(sync_session, tmp_path)
|
||||
rendered = tmp_path / "exports" / "bearing_production.blend"
|
||||
rendered.parent.mkdir(parents=True, exist_ok=True)
|
||||
rendered.write_text("BLENDDATA", encoding="utf-8")
|
||||
|
||||
result = persist_order_line_output(
|
||||
sync_session,
|
||||
line,
|
||||
success=True,
|
||||
output_path=str(rendered),
|
||||
render_log={"artifact_type": "blend_production"},
|
||||
workflow_run_id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
sync_session.refresh(line)
|
||||
expected_path = Path(result.result_path or "")
|
||||
asset = sync_session.execute(
|
||||
select(MediaAsset).where(MediaAsset.id == uuid.UUID(result.asset_id))
|
||||
).scalar_one()
|
||||
|
||||
assert expected_path.exists()
|
||||
assert expected_path.suffix == ".blend"
|
||||
assert result.asset_type == MediaAssetType.blend_production
|
||||
assert asset.asset_type == MediaAssetType.blend_production
|
||||
assert asset.storage_key == f"renders/{line.id}/{expected_path.name}"
|
||||
assert asset.mime_type == "application/x-blender"
|
||||
|
||||
|
||||
def test_persist_order_line_output_checks_order_completion(sync_session, tmp_path, monkeypatch):
|
||||
from app.config import settings
|
||||
|
||||
|
||||
Reference in New Issue
Block a user