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