"""IMP-33 u5 — AI fallback validator tests. Scope (Stage 2 plan, u5): - schema re-validation (defence-in-depth) - builder whitelist (BUILDER_OPTIONS_PATCH) - dropped-slot guard (PARTIAL_OVERRIDES / SLOT_MAPPING_PROPOSAL must keep every declared sub_zone slot present) - frame-swap guard (no payload.frame_id mutation; V4 rank-1 protected) - Internal Region containment (payload.region_id must match declared id) """ from __future__ import annotations import pytest from src.phase_z2_ai_fallback import AiFallbackProposal, ProposalKind from src.phase_z2_ai_fallback.validate import ( AiFallbackValidationError, validate_proposal, ) _FRAME_CONTRACT = { "frame_id": 1171281190, "sub_zones": [ {"id": "pillar_1", "accepts": ["text_block"]}, {"id": "pillar_2", "accepts": ["text_block"]}, {"id": "pillar_3", "accepts": ["text_block"]}, ], "payload": { "builder_options": { "item_parser": "pillar_item", "array_root": "pillars", "role_field": "color_class", }, }, } _REGION = {"id": "zone_top.region_a"} def _make(kind: ProposalKind, payload: dict) -> AiFallbackProposal: return AiFallbackProposal(proposal_kind=kind, payload=payload) def test_builder_options_patch_accepts_whitelisted_keys() -> None: proposal = _make( ProposalKind.BUILDER_OPTIONS_PATCH, {"item_parser": "alt_pillar_item"}, ) validate_proposal(proposal, frame_contract=_FRAME_CONTRACT) def test_builder_options_patch_rejects_unknown_key() -> None: proposal = _make( ProposalKind.BUILDER_OPTIONS_PATCH, {"item_parser": "x", "padding_px": 10}, ) with pytest.raises(AiFallbackValidationError, match="builder whitelist"): validate_proposal(proposal, frame_contract=_FRAME_CONTRACT) def test_partial_overrides_requires_all_declared_slots() -> None: proposal = _make( ProposalKind.PARTIAL_OVERRIDES, {"slots": {"pillar_1": "a", "pillar_2": "b"}}, ) with pytest.raises(AiFallbackValidationError, match="dropped-slot guard"): validate_proposal(proposal, frame_contract=_FRAME_CONTRACT) def test_partial_overrides_with_all_slots_passes() -> None: proposal = _make( ProposalKind.PARTIAL_OVERRIDES, {"slots": {"pillar_1": "a", "pillar_2": "b", "pillar_3": "c"}}, ) validate_proposal(proposal, frame_contract=_FRAME_CONTRACT) def test_slot_mapping_proposal_requires_slots_dict() -> None: proposal = _make(ProposalKind.SLOT_MAPPING_PROPOSAL, {"slots": []}) with pytest.raises(AiFallbackValidationError, match="dropped-slot guard"): validate_proposal(proposal, frame_contract=_FRAME_CONTRACT) def test_frame_swap_guard_rejects_mismatched_frame_id() -> None: proposal = _make( ProposalKind.BUILDER_OPTIONS_PATCH, {"frame_id": 9999, "item_parser": "x"}, ) with pytest.raises(AiFallbackValidationError, match="frame-swap guard"): validate_proposal(proposal, frame_contract=_FRAME_CONTRACT) def test_frame_swap_guard_accepts_matching_frame_id() -> None: proposal = _make( ProposalKind.PARTIAL_OVERRIDES, { "frame_id": 1171281190, "slots": {"pillar_1": "a", "pillar_2": "b", "pillar_3": "c"}, }, ) validate_proposal(proposal, frame_contract=_FRAME_CONTRACT) def test_internal_region_containment_rejects_mismatch() -> None: proposal = _make( ProposalKind.PARTIAL_OVERRIDES, { "slots": {"pillar_1": "a", "pillar_2": "b", "pillar_3": "c"}, "region_id": "zone_bottom.region_x", }, ) with pytest.raises(AiFallbackValidationError, match="Internal Region"): validate_proposal( proposal, frame_contract=_FRAME_CONTRACT, internal_region=_REGION, ) def test_internal_region_containment_accepts_match() -> None: proposal = _make( ProposalKind.PARTIAL_OVERRIDES, { "slots": {"pillar_1": "a", "pillar_2": "b", "pillar_3": "c"}, "region_id": "zone_top.region_a", }, ) validate_proposal( proposal, frame_contract=_FRAME_CONTRACT, internal_region=_REGION, ) def test_internal_region_check_skipped_when_no_region_supplied() -> None: proposal = _make( ProposalKind.PARTIAL_OVERRIDES, { "slots": {"pillar_1": "a", "pillar_2": "b", "pillar_3": "c"}, "region_id": "zone_top.region_a", }, ) validate_proposal(proposal, frame_contract=_FRAME_CONTRACT)