"""IMP-09 PR 1 — retry gate tests (_attempt_zone_ratio_retry early exit). Stage 3 round 4 lock §2-A: row-axis retry must skip when layout has dynamic_cols=True (2-D topology) OR dynamic_rows=False (fr_default sink). The horizontal-2 path (dynamic_rows=True, dynamic_cols=False) must still proceed through the gate. These tests exercise the gate by routing the request through _attempt_zone_ratio_retry with router_active=True + proposed zone_ratio_retry — but with layout_css fields that should trip the gate. We confirm the early skip by asserting retry_attempted==False and retry_skipped_reason content. """ from __future__ import annotations from pathlib import Path import pytest from src.phase_z2_pipeline import _attempt_zone_ratio_retry _ROUTER_ACTIVE = { "router_active": True, "proposed_actions_summary": ["zone_ratio_retry"], } def _dummy_kwargs(layout_css: dict, tmp_path: Path) -> dict: """All params required by _attempt_zone_ratio_retry. Only `layout_css` and `router_decision` matter pre-gate.""" return { "run_dir": tmp_path, "out_path": tmp_path / "final.html", "slide_title": "test", "slide_footer": None, "zones_data": [], "debug_zones": [], "layout_preset": "horizontal-2", "layout_css": layout_css, "overflow": {}, "fit_classification": {}, "router_decision": _ROUTER_ACTIVE, "gap_px": 14, } def test_vertical_2_dynamic_cols_skips_retry(tmp_path): layout_css = { "areas": '"left right"', "cols": "583px 583px", "rows": "1fr", "heights_px": [585], "widths_px": [583, 583], "ratios": [1.0], "width_ratios": [0.494, 0.494], "dynamic_rows": False, "dynamic_cols": True, } trace = _attempt_zone_ratio_retry(**_dummy_kwargs(layout_css, tmp_path)) assert trace["retry_attempted"] is False assert "dynamic_cols" in trace["retry_skipped_reason"] assert "IMP-09" in trace["retry_skipped_reason"] def test_fr_default_sink_skips_retry(tmp_path): # PR 1 single / T-shape / 2x2 fall through to fr_default and must # not enter row-only retry plan. layout_css = { "areas": '"top top" "bottom-left bottom-right"', "cols": "1fr 1fr", "rows": "1fr 1fr", "heights_px": [285, 286], "widths_px": [583, 583], "ratios": [0.487, 0.489], "width_ratios": [0.494, 0.494], "dynamic_rows": False, "dynamic_cols": False, } trace = _attempt_zone_ratio_retry(**_dummy_kwargs(layout_css, tmp_path)) assert trace["retry_attempted"] is False assert "fr_default_from_preset" in trace["retry_skipped_reason"] def test_horizontal_2_dynamic_rows_passes_gate(tmp_path): """horizontal-2 with dynamic_rows=True must pass the gate. The test does not need plan_zone_ratio_retry to succeed; it only asserts the gate did not early-skip with one of the new skip reasons.""" layout_css = { "areas": '"top" "bottom"', "cols": "1fr", "rows": "333px 238px", "heights_px": [333, 238], "widths_px": [1180], "ratios": [0.569, 0.407], "width_ratios": [1.0], "dynamic_rows": True, "dynamic_cols": False, } # plan_zone_ratio_retry will return None because debug_zones is # empty, so retry_attempted=True but plan==None. trace = _attempt_zone_ratio_retry(**_dummy_kwargs(layout_css, tmp_path)) assert trace["retry_attempted"] is True # The gate was passed; skip reason (if any) is the legacy # plan-failure reason, not the new gate reasons. skip_reason = trace.get("retry_skipped_reason") if skip_reason is not None: assert "dynamic_cols" not in skip_reason assert "fr_default_from_preset" not in skip_reason def test_router_inactive_skips_before_gate(tmp_path): """When router_active=False, the early skip happens before the new IMP-09 gate. Verify the existing behavior is unchanged.""" layout_css = { "areas": '"left right"', "dynamic_rows": False, "dynamic_cols": True, "heights_px": [585], "widths_px": [583, 583], "ratios": [1.0], "width_ratios": [0.5, 0.5], } kwargs = _dummy_kwargs(layout_css, tmp_path) kwargs["router_decision"] = {"router_active": False} trace = _attempt_zone_ratio_retry(**kwargs) assert trace["retry_attempted"] is False assert "router_active=False" in trace["retry_skipped_reason"]