"""Headless render verification: bundle the fixture factory exactly like the runtime does and execute it in node (scene-graph smoke, no WebGL needed).""" from __future__ import annotations import json import shutil import subprocess from pathlib import Path import pytest from app import forge_bridge REPO_ROOT = Path(__file__).resolve().parents[1] ESBUILD = REPO_ROOT / "node_modules" / ".bin" / "esbuild" NODE = shutil.which("node") pytestmark = pytest.mark.skipif( not (ESBUILD.exists() and NODE), reason="node/esbuild not installed (run npm ci)") def test_fixture_factory_bundles_and_executes(tmp_path, factory_fixture_ts): entry = tmp_path / "entry.js" entry.write_text( 'export { createMugModel as makeModel, createMugLookDevLights as makeLights } ' f'from "{factory_fixture_ts}";\n' f'export {{ mountViewer }} from "{REPO_ROOT / "app" / "static" / "viewer-core.js"}";\n', encoding="utf-8") bundle = tmp_path / "model.bundle.js" link = tmp_path / "node_modules" link.symlink_to(REPO_ROOT / "node_modules", target_is_directory=True) build = subprocess.run( [str(ESBUILD), str(entry), "--bundle", "--format=esm", "--target=es2022", "--minify", f"--outfile={bundle}"], cwd=tmp_path, capture_output=True, text=True, timeout=120) assert build.returncode == 0, build.stderr assert bundle.stat().st_size > 100_000 smoke = subprocess.run( [NODE, str(REPO_ROOT / "scripts" / "node_smoke.mjs"), str(bundle), str(REPO_ROOT / "tests" / "fixtures" / "canned_spec.json")], capture_output=True, text=True, timeout=120) assert smoke.returncode == 0, f"{smoke.stdout}\n{smoke.stderr}" report = json.loads(smoke.stdout.splitlines()[0]) assert report["meshes"] >= 1 assert report["children"] >= 1 assert report["runtimeNodes"] >= 1 assert all(isinstance(v, (int, float)) for v in report["boundingBox"]) bad_spec = json.loads( (REPO_ROOT / "tests" / "fixtures" / "canned_spec.json").read_text( encoding="utf-8")) body = next(component for component in bad_spec["componentTree"] if component["id"] == "body") body["dimensions"]["width"] *= 2 bad_spec_path = tmp_path / "bad-spec.json" bad_spec_path.write_text(json.dumps(bad_spec), encoding="utf-8") rejected = subprocess.run( [NODE, str(REPO_ROOT / "scripts" / "node_smoke.mjs"), str(bundle), str(bad_spec_path)], capture_output=True, text=True, timeout=120) assert rejected.returncode == 1 assert "body dimensions disagree with spec" in rejected.stderr def test_axis_attachment_preserves_declared_dimensions(tmp_path, canned_spec): """Attachment endpoints orient axial primitives without silently replacing their declared physical dimensions.""" spec = json.loads(json.dumps(canned_spec)) handle = next(component for component in spec["componentTree"] if component["id"] == "handle") handle["primitive"] = "cylinder" # Deliberately disagree with the 0.06 m declared height. The compiler must # retain dimensions as the geometry contract even when endpoints are used # to position and orient the mesh. handle["attachment"]["localStart"] = [0.04, 0.07, 0.0] handle["attachment"]["localEnd"] = [0.04, 0.03, 0.0] preview = forge_bridge.prepare_hosted_preview(spec) spec_path = tmp_path / "compile-spec.json" spec_path.write_text(json.dumps(preview), encoding="utf-8") factory = tmp_path / "factory.ts" forge_bridge.generate_factory( spec_path, factory, pass_id=forge_bridge.HOSTED_PREVIEW_PASS) entry = tmp_path / "entry.js" entry.write_text( f'export {{ {forge_bridge.factory_export_name(factory.read_text(encoding="utf-8"))} ' f'as makeModel }} from "{factory}";\n', encoding="utf-8") bundle = tmp_path / "model.bundle.js" (tmp_path / "node_modules").symlink_to( REPO_ROOT / "node_modules", target_is_directory=True) build = subprocess.run( [str(ESBUILD), str(entry), "--bundle", "--format=esm", "--target=es2022", "--minify", f"--outfile={bundle}"], cwd=tmp_path, capture_output=True, text=True, timeout=120) assert build.returncode == 0, build.stderr smoke = subprocess.run( [NODE, str(REPO_ROOT / "scripts" / "node_smoke.mjs"), str(bundle), str(spec_path)], capture_output=True, text=True, timeout=120) assert smoke.returncode == 0, f"{smoke.stdout}\n{smoke.stderr}"