"""IMP-88 (#88) u1 — Step 17 retry chain router rows + status surface. Stage 2 binding contract (unit u1, data-surface only): - NEW row `image_aspect_mismatch → image_fit` in ACTION_BY_CATEGORY. Closes the unmapped classifier emission gap at src/phase_z2_classifier.py:434-447 where image_aspect_mismatch was emitted with proposed_action=None (verified Stage 1). - REMAP `frame_capacity_mismatch → frame_internal_fit_candidate` (previously frame_reselect) per PHASE-Z-PIPELINE-OVERVIEW.md:321. frame_reselect remains a valid downstream action via the failure_router cascade (rerender_still_fails → frame_reselect). - NEW ACTION_RATIONALE rows for image_aspect_mismatch + frame_internal_fit_candidate (rationale text for trace surface). - NEW ACTION_IMPLEMENTATION_STATUS rows for image_fit + frame_internal_fit_candidate. layout_adjust is also registered. u1 initial state was MISSING for all three. u7 completion flips the rows to IMPLEMENTED once the end-to-end path (u3/u4/u5 planners + u6 dispatcher + u7 Step 17 entry) is wired (same convention as IMP-12 u7 cascade rows + IMP-35 u3 details_popup_escalation flip). Out of scope for u1 (locked in Stage 2 exit report): - failure_router cascade rows for the three actions → u2. - planner stubs (plan_layout_adjust / plan_image_fit / plan_frame_internal_fit_candidate) → u3 / u4 / u5. - salvage dispatcher branches + Step 17 entry triggers → u6 / u7. Post u7 completion (2026-05-24): status assertions in this file reflect the IMPLEMENTED end-state. Test names that previously referenced "_missing" are renamed to "_implemented_after_u7" so the surface contract is honest about the post-u7 state. """ from __future__ import annotations from src.phase_z2_router import ( ACTION_BY_CATEGORY, ACTION_IMPLEMENTATION_STATUS, ACTION_RATIONALE, route_action, route_fit_classification, ) # ─── ACTION_BY_CATEGORY rows ────────────────────────────────────── def test_image_aspect_mismatch_maps_to_image_fit(): """u1 — NEW row closes the classifier→router gap. Stage 1 verified that route_action('image_aspect_mismatch') returned proposed_action=None with implementation_status='unknown'. u1 must register the row so the classifier emission is routable. """ assert ACTION_BY_CATEGORY["image_aspect_mismatch"] == "image_fit" def test_frame_capacity_mismatch_remaps_to_frame_internal_fit_candidate(): """u1 — REMAP per PHASE-Z-PIPELINE-OVERVIEW.md:321. Spec lock: frame_internal_fit_candidate is the per-zone first-pass salvage inside the declared frame envelope. frame_reselect (V4 top-k alternate frame swap) remains downstream via the failure_router cascade (rerender_still_fails → frame_reselect). """ assert ACTION_BY_CATEGORY["frame_capacity_mismatch"] == "frame_internal_fit_candidate" def test_existing_action_by_category_rows_unchanged(): """u1 — non-IMP-88 rows must NOT be touched (regression guard). Only two edits are allowed in u1: NEW image_aspect_mismatch row and REMAP frame_capacity_mismatch row. Everything else is locked. """ assert ACTION_BY_CATEGORY["minor_overflow"] == "zone_ratio_retry" assert ACTION_BY_CATEGORY["moderate_overflow"] == "layout_adjust" assert ACTION_BY_CATEGORY["structural_minor_overflow"] == "zone_ratio_retry" assert ACTION_BY_CATEGORY["structural_major_overflow"] == "details_popup_escalation" assert ACTION_BY_CATEGORY["tabular_overflow"] == "details_popup_escalation" assert ACTION_BY_CATEGORY["layout_zone_mismatch"] == "layout_adjust" assert ACTION_BY_CATEGORY["hard_visual_fail"] == "abort" # ─── ACTION_RATIONALE rows ──────────────────────────────────────── def test_image_aspect_mismatch_rationale_present(): """u1 — trace surface must explain *why* image_aspect_mismatch routes onto image_fit (frame-scoped, no global image CSS shrink — honors feedback_phase_z_spacing_direction).""" rationale = ACTION_RATIONALE.get("image_aspect_mismatch", "") assert rationale.strip(), "image_aspect_mismatch rationale must be non-empty" assert "image" in rationale.lower() def test_frame_capacity_mismatch_rationale_updated_for_internal_fit(): """u1 — rationale text must reflect the new internal-fit-first routing. The text must no longer claim frame_reselect as the primary action for this category (it's now the downstream cascade step).""" rationale = ACTION_RATIONALE.get("frame_capacity_mismatch", "") assert rationale.strip(), "frame_capacity_mismatch rationale must be non-empty" # Mentions the new internal-fit direction. assert "internal" in rationale.lower() or "envelope" in rationale.lower() # ─── ACTION_IMPLEMENTATION_STATUS rows ──────────────────────────── def test_layout_adjust_status_implemented_after_u7(): """u1 registered layout_adjust row (initial MISSING). After u3 (plan_layout_adjust + apply_layout_adjust_layout_css) + u6 (salvage dispatcher branch) + u7 (cascade entry trigger) land the end-to-end deterministic path, the status flips to IMPLEMENTED.""" assert ACTION_IMPLEMENTATION_STATUS["layout_adjust"] == "IMPLEMENTED" def test_image_fit_status_implemented_after_u7(): """u1 registered image_fit row (initial MISSING). After u4 (plan_image_fit + apply_image_fit_css) + u7 (_attempt_step17_image_fit_single_pass entry) land the end-to-end deterministic path, the status flips to IMPLEMENTED.""" assert "image_fit" in ACTION_IMPLEMENTATION_STATUS assert ACTION_IMPLEMENTATION_STATUS["image_fit"] == "IMPLEMENTED" def test_frame_internal_fit_candidate_status_implemented_after_u7(): """u1 registered frame_internal_fit_candidate row (initial MISSING). After u5 (plan_frame_internal_fit_candidate + apply) + u6 (salvage dispatcher branch) + u7 (cascade entry trigger) land the end-to-end deterministic path, the status flips to IMPLEMENTED.""" assert "frame_internal_fit_candidate" in ACTION_IMPLEMENTATION_STATUS assert ACTION_IMPLEMENTATION_STATUS["frame_internal_fit_candidate"] == "IMPLEMENTED" def test_existing_action_implementation_status_rows_unchanged(): """u1 — non-IMP-88 status rows must NOT regress. zone_ratio_retry, cascade-only salvage actions, details_popup_escalation (IMP-35 u3), and frame_reselect must keep their current statuses.""" assert ACTION_IMPLEMENTATION_STATUS["zone_ratio_retry"] == "IMPLEMENTED" assert ACTION_IMPLEMENTATION_STATUS["details_popup_escalation"] == "IMPLEMENTED" assert ACTION_IMPLEMENTATION_STATUS["frame_reselect"] == "PARTIAL" assert ACTION_IMPLEMENTATION_STATUS["adapter_needed"] == "PARTIAL" assert ACTION_IMPLEMENTATION_STATUS["abort"] == "IMPLEMENTED" assert ACTION_IMPLEMENTATION_STATUS["cross_zone_redistribute"] == "IMPLEMENTED" assert ACTION_IMPLEMENTATION_STATUS["glue_compression"] == "IMPLEMENTED" assert ACTION_IMPLEMENTATION_STATUS["font_step_compression"] == "IMPLEMENTED" # ─── route_action + route_fit_classification integration ────────── def test_route_action_image_aspect_mismatch_returns_image_fit_implemented(): """u1 — route_action surface composes the new mapping correctly. Stage 1 evidence: previously this call returned proposed_action=None and implementation_status='unknown'. After u1 + u4 + u7, the call must return image_fit with status IMPLEMENTED (end-to-end deterministic path via plan_image_fit + apply_image_fit_css + Step 17 single-pass entry).""" routing = route_action("image_aspect_mismatch") assert routing["proposed_action"] == "image_fit" assert routing["implementation_status"] == "IMPLEMENTED" assert routing["mapping_source"] == "spec §4 ACTION_BY_CATEGORY" assert routing["rationale"], "rationale must be carried through route_action" def test_route_action_frame_capacity_mismatch_returns_frame_internal_fit_candidate_implemented(): """u1 — route_action surface reflects the REMAP. After u1 + u5 + u6 + u7 the status is IMPLEMENTED (end-to-end deterministic path via plan_frame_internal_fit_candidate + apply + salvage dispatcher branch + cascade entry trigger).""" routing = route_action("frame_capacity_mismatch") assert routing["proposed_action"] == "frame_internal_fit_candidate" assert routing["implementation_status"] == "IMPLEMENTED" assert routing["mapping_source"] == "spec §4 ACTION_BY_CATEGORY" def test_route_fit_classification_surfaces_imp88_actions_as_implemented(): """End-to-end: when classifier emits the two IMP-88 categories alongside an already-implemented one, route_fit_classification: - attaches proposed_action onto each row - lists all three actions in proposed_actions_summary - reports an empty missing_actions_pending_impl for the IMP-88 actions (u7 completion flipped image_fit + frame_internal_fit_candidate to IMPLEMENTED alongside layout_adjust) - all three rows count as IMPLEMENTED in the status summary.""" fit_classification = { "visual_check_passed": False, "classifications": [ { "source": "image_event", "zone_position": "bottom", "category": "image_aspect_mismatch", }, { "source": "composition", "zone_position": "top", "category": "frame_capacity_mismatch", }, { "source": "clipped_inner", "zone_position": "bottom", "category": "minor_overflow", }, ], } summary = route_fit_classification(fit_classification) assert summary["router_active"] is True assert summary["routed_count"] == 3 assert "image_fit" in summary["proposed_actions_summary"] assert "frame_internal_fit_candidate" in summary["proposed_actions_summary"] assert "zone_ratio_retry" in summary["proposed_actions_summary"] # After u7 completion, both new IMP-88 actions are IMPLEMENTED on the # router-surface — they no longer surface as missing pending impl. assert "image_fit" not in summary["missing_actions_pending_impl"] assert "frame_internal_fit_candidate" not in summary["missing_actions_pending_impl"] # All three (zone_ratio_retry IMPLEMENTED + 2 IMP-88 IMPLEMENTED) count # together. zone_ratio_retry was IMPLEMENTED since A3 cascade. assert summary["implementation_status_summary"].get("IMPLEMENTED", 0) == 3 assert summary["implementation_status_summary"].get("MISSING", 0) == 0 # Per-row enrichment carries the new proposed actions onto entries. cats = {c["category"]: c for c in fit_classification["classifications"]} assert cats["image_aspect_mismatch"]["proposed_action"] == "image_fit" assert ( cats["frame_capacity_mismatch"]["proposed_action"] == "frame_internal_fit_candidate" )