"""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