"""IMP-33 u3 — fallback prompt builder tests. Scope (Stage 2 plan, u3): - Prompt is built only when V4 route == 'ai_adaptation_required'. - System prompt declares MDX READ-ONLY and pins the u2 whitelist. - System prompt forbids the u2 forbidden kinds + frame_id swap. - User payload carries all 6 declared inputs and labels MDX READ_ONLY. """ from __future__ import annotations import json import pytest from src.phase_z2_ai_fallback.prompts import ( SYSTEM_PROMPT, V4_ROUTE_AI_ADAPTATION, build_ai_fallback_prompt, ) from src.phase_z2_ai_fallback.schema import FORBIDDEN_KINDS, ProposalKind def _v4(route: str = V4_ROUTE_AI_ADAPTATION) -> dict: return { "route": route, "cardinality": {"strict": 3}, "label": "restructure", "frame_id": 1171281190, "rank": 1, } def _inputs(route: str = V4_ROUTE_AI_ADAPTATION) -> dict: return { "v4_result": _v4(route), "frame_contract": {"template_id": "three_parallel_requirements"}, "frame_visual_html": "
", "figma_partial_json": {"nodes": []}, "internal_region": {"id": "region_top", "bbox": [0, 0, 1200, 320]}, "mdx_text": "# 대목차\n- 항목 1\n- 항목 2\n- 항목 3", } def test_system_prompt_declares_mdx_read_only() -> None: assert "READ-ONLY" in SYSTEM_PROMPT def test_system_prompt_lists_all_whitelisted_kinds() -> None: for kind in ProposalKind: assert kind.value in SYSTEM_PROMPT def test_system_prompt_forbids_all_forbidden_kinds() -> None: for forbidden in FORBIDDEN_KINDS: assert forbidden in SYSTEM_PROMPT def test_system_prompt_locks_frame_id_swap() -> None: assert "frame_id" in SYSTEM_PROMPT def test_build_prompt_returns_system_and_user() -> None: prompt = build_ai_fallback_prompt(**_inputs()) assert set(prompt.keys()) == {"system", "user"} assert prompt["system"] == SYSTEM_PROMPT def test_user_payload_carries_all_inputs_and_marks_mdx_read_only() -> None: prompt = build_ai_fallback_prompt(**_inputs()) payload = json.loads(prompt["user"]) assert payload["v4"]["route"] == V4_ROUTE_AI_ADAPTATION assert payload["v4"]["cardinality"] == {"strict": 3} assert payload["v4"]["frame_id"] == 1171281190 assert payload["frame_contract"]["template_id"] == "three_parallel_requirements" assert payload["frame_visual_html"] == "
" assert payload["figma_partial_json"] == {"nodes": []} assert payload["internal_region"]["id"] == "region_top" assert "mdx_text_READ_ONLY" in payload assert payload["mdx_text_READ_ONLY"].startswith("# 대목차") assert "mdx_text" not in payload # only the READ_ONLY key, not a writable alias @pytest.mark.parametrize( "route", ["direct_render", "deterministic_minor_adjustment", "design_reference_only", None] ) def test_non_ai_route_rejected(route) -> None: inputs = _inputs(route=route) if route is not None else _inputs() if route is None: inputs["v4_result"].pop("route") with pytest.raises(ValueError, match=V4_ROUTE_AI_ADAPTATION): build_ai_fallback_prompt(**inputs) def test_cardinality_signature_alias_accepted() -> None: """Some V4 callers expose ``cardinality_signature``; both keys must resolve.""" inputs = _inputs() inputs["v4_result"].pop("cardinality") inputs["v4_result"]["cardinality_signature"] = {"strict": 4} payload = json.loads(build_ai_fallback_prompt(**inputs)["user"]) assert payload["v4"]["cardinality"] == {"strict": 4}