"""IMP-#85 u1 — mapper missing-builder dispatch raises BuilderMissingError. Scope (Stage 2 lock): - `BuilderMissingError` exists and is a subclass of `FitError`. - `map_with_contract` raises `BuilderMissingError` when `contract.payload.builder` references an unknown registry entry, OR when `payload.builder` is empty/missing. - Because it subclasses `FitError`, the existing pipeline `except FitError` route in `src/phase_z2_pipeline.py` continues to catch the failure and emit an `adapter_needed` record instead of a hard crash (mdx04 `sw_dependency_four_problems` / `cards_4_grid` regression evidence). """ from __future__ import annotations from types import SimpleNamespace import pytest from src.phase_z2_mapper import ( BuilderMissingError, FitError, PAYLOAD_BUILDERS, map_with_contract, ) def _make_section(raw_content: str = "- a\n- b\n- c"): return SimpleNamespace( section_id="test-sec", raw_content=raw_content, title="t", order=1, ) def test_builder_missing_error_is_fit_error_subclass(): assert issubclass(BuilderMissingError, FitError) def test_unknown_builder_raises_builder_missing_error(): unknown = "definitely_not_a_registered_builder" assert unknown not in PAYLOAD_BUILDERS contract = { "template_id": "fake_contract_unknown_builder", "source_shape": "top_bullets", "cardinality": {}, "payload": {"builder": unknown}, } with pytest.raises(BuilderMissingError) as exc: map_with_contract(_make_section(), contract) assert unknown in str(exc.value) assert "fake_contract_unknown_builder" in str(exc.value) def test_missing_builder_field_raises_builder_missing_error(): contract = { "template_id": "fake_contract_missing_builder_field", "source_shape": "top_bullets", "cardinality": {}, "payload": {}, } with pytest.raises(BuilderMissingError) as exc: map_with_contract(_make_section(), contract) assert "missing payload.builder" in str(exc.value) def test_builder_missing_error_caught_by_fit_error_handler(): """Pipeline 의 `except FitError` 경로가 그대로 잡아주는지 검증. 실제 pipeline import 없이 동일 패턴을 재현하여 subclass 의 의도된 routing 효과(adapter_needed) 가 깨지지 않는지 확인. """ contract = { "template_id": "fake_contract_routing_check", "source_shape": "top_bullets", "cardinality": {}, "payload": {"builder": "no_such_builder"}, } caught = False try: map_with_contract(_make_section(), contract) except FitError: caught = True assert caught, "BuilderMissingError must propagate through `except FitError`"