feat(#85): IMP catalog builder invariant + VP runtime gate (u1~u7)
- u1: BuilderMissingError(FitError) — narrow exception aligned with pipeline catch - u2: load_frame_contracts catalog invariant + VP skip + CatalogInvariantError - u3a: audit CLI I1~I3 (partial existence / declared builder / registry membership) - u3b: audit CLI I4 (slot_payload refs vs declared/generated payload keys) - u4: lookup_v4_candidates VP filter (lookup_v4_all_judgments raw telemetry untouched) - u5: catalog invariant regression coverage + temp non-VP failure fixtures - u6: mdx04 VP routing fixture tests (sw_dependency_four_problems excluded from live) - u7: tests/conftest.py env isolation + mdx03/mdx04/mdx05 subprocess smoke Targeted 74 PASS (12.31s). Full regression 1063 PASS (87.70s). Audit CLI clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
85
tests/test_phase_z2_mapper_builder_missing.py
Normal file
85
tests/test_phase_z2_mapper_builder_missing.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""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`"
|
||||
Reference in New Issue
Block a user