File size: 4,560 Bytes
39ff632
 
 
 
 
 
 
 
 
 
 
 
a35b2b8
 
39ff632
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324b9a7
 
39ff632
 
 
 
 
 
 
324b9a7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a35b2b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
"""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}"