File size: 6,121 Bytes
36b02e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c42f749
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36b02e9
 
 
 
 
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""Phase A4 — codegen integration, offline (SPEC_OPTION_A.md §10).

Exercises app.codegen_assemble (the backend post-processing layer) with CANNED
model build_spec output — no live Modal. The full server-side path
(build_spec text -> kernel -> repair -> assembler -> contract) must yield JSON
that passes the existing frontend contract and the metric gate. Also covers
passthrough, primitive fallback, and model-free regeneration. The LIVE model
call is FLAGGED for the user (not tested here).
"""
import importlib.util
import json
import sys
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

import app  # noqa: E402
from geometry import metrics  # noqa: E402

_spec = importlib.util.spec_from_file_location("schema_mod", ROOT / "tests" / "test_schema.py")
_schema = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_schema)
validate_shape = _schema.validate_shape

FIX = ROOT / "tests" / "fixtures" / "codegen"
SPECS = {"octahedron": "spec_octahedron.json",
         "cube": "spec_cube.json",
         "cylindrical_vent": "spec_cylindrical_vent.json"}


def raw_spec(name):
    return (FIX / SPECS[name]).read_text()


def regularity(name):
    return json.loads(raw_spec(name))["regularity"]


# --------------------------------------------------------------------------- #
# full codegen path on canned build_spec output                               #
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("name", list(SPECS))
def test_codegen_assemble_produces_valid_contract(name):
    out = app.codegen_assemble(raw_spec(name))
    contract = json.loads(out)
    assert validate_shape(contract) == []
    verts = contract["geometry"]["vertices"]
    faces = [f["indices"] for f in contract["geometry"]["faces"]]
    ok, fails = metrics.passes(verts, faces, regularity(name))
    assert ok, fails


# --------------------------------------------------------------------------- #
# passthrough of non-build_spec responses                                     #
# --------------------------------------------------------------------------- #
def test_guided_response_passthrough():
    raw = json.dumps({"type": "guided_response", "message": "hi", "suggestions": ["cube"]})
    assert json.loads(app.codegen_assemble(raw)) == json.loads(raw)


def test_error_passthrough():
    raw = json.dumps({"error": "endpoint_unreachable"})
    assert json.loads(app.codegen_assemble(raw)) == json.loads(raw)


def test_malformed_becomes_error():
    assert json.loads(app.codegen_assemble("not json at all"))["error"] == "model_invalid_json"


def test_non_buildspec_dict_passthrough():
    raw = json.dumps({"shape_id": "x", "geometry": {"vertices": [], "faces": []}})
    assert json.loads(app.codegen_assemble(raw)) == json.loads(raw)


# --------------------------------------------------------------------------- #
# generator failure -> primitive fallback                                     #
# --------------------------------------------------------------------------- #
def test_broken_generator_falls_back_to_primitive():
    spec = json.loads(raw_spec("octahedron"))
    spec["generator"]["code"] = "def build(p):\n    raise ValueError('boom')\n"
    out = app.codegen_assemble(json.dumps(spec))
    contract = json.loads(out)
    assert validate_shape(contract) == []  # recovered via primitives.REGISTRY['octahedron']
    assert contract["shape_id"] == "octahedron"


def test_broken_generator_unknown_shape_errors():
    spec = json.loads(raw_spec("octahedron"))
    spec["shape_id"] = "no_such_primitive"
    spec["generator"]["code"] = "def build(p):\n    raise ValueError('boom')\n"
    out = app.codegen_assemble(json.dumps(spec))
    assert json.loads(out)["error"] == "model_invalid_json"


# --------------------------------------------------------------------------- #
# caching for model-free regeneration                                         #
# --------------------------------------------------------------------------- #
def test_assemble_caches_spec_for_regen():
    app._SPEC_CACHE.pop("k1", None)
    app.codegen_assemble(raw_spec("cylindrical_vent"), cache_key="k1")
    assert "k1" in app._SPEC_CACHE
    # Model-free regen: re-run the cached generator with more vanes.
    from geometry import assembler
    contract, _ = assembler.build(app._SPEC_CACHE["k1"], {"vanes": 12, "height": 2.0})
    assert validate_shape(contract) == []
    assert len([c for c in contract["components"] if c["id"].startswith("vane_")]) == 12


# --------------------------------------------------------------------------- #
# model-free regeneration helper (_regen_modelfree)                           #
# --------------------------------------------------------------------------- #
def test_regen_modelfree_good_spec():
    spec = json.loads(raw_spec("cylindrical_vent"))
    out = app._regen_modelfree(spec, {"vanes": 10, "height": 2.0})
    contract = json.loads(out)
    assert validate_shape(contract) == []
    assert len([c for c in contract["components"] if c["id"].startswith("vane_")]) == 10


def test_regen_modelfree_broken_generator_falls_back_to_primitive():
    spec = json.loads(raw_spec("octahedron"))
    spec["generator"]["code"] = "def build(p):\n    raise ValueError('x')\n"
    out = app._regen_modelfree(spec, None)  # shape_id 'octahedron' is a primitive
    assert out is not None and validate_shape(json.loads(out)) == []


def test_regen_modelfree_broken_unknown_returns_none():
    spec = json.loads(raw_spec("octahedron"))
    spec["shape_id"] = "no_such_primitive"
    spec["generator"]["code"] = "def build(p):\n    raise ValueError('x')\n"
    assert app._regen_modelfree(spec, None) is None


# --------------------------------------------------------------------------- #
# default path is unchanged when the flag is off                              #
# --------------------------------------------------------------------------- #
def test_codegen_flag_defaults_off():
    assert app.CODEGEN is False