feat(IMP-09): PR 1 — col-axis solver + per-zone geometry mapper + retry gate
Stage 3 round 4 lock implementation: extend build_layout_css beyond
the horizontal-2-only dynamic path. Every layout_css return now
carries length-locked col-axis keys (widths_px, width_ratios,
dynamic_cols) matching the parsed css_areas grid (R rows, C cols),
so 2-D layouts (T / 2x2 in PR 2) and the unified
_compute_per_zone_geometry mapper can plug in without further
contract churn.
PR 1 scope:
- _parse_css_areas + _parse_fr_string + _compute_per_zone_geometry
(unified — 1-D and 2-D from the same code path)
- compute_zone_layout_cols (vertical-2 weight-only solver)
- _build_fr_default / _build_rows_dynamic / _build_cols_dynamic
(populate widths_px/heights_px on every return path)
- build_layout_css override branch keeps the warn-and-fallthrough
legacy for unsupported presets (PR 2 promotes to strict raise)
- retry gate in _attempt_zone_ratio_retry skips when dynamic_cols=True
or dynamic_rows=False, with explicit retry_skipped_reason
- Step 8 artifact gains zone_widths_px_planned /
zone_col_ratios_planned (top-level) + zone_width_px_planned /
zone_col_ratio_planned (per-zone)
- debug_zones width injection via _compute_per_zone_geometry
(replaces the legacy row-only zip)
Tests: tests/phase_z2/ — 47 new cases (parse / fr-string / cols solver /
per-zone geometry / build_layout_css contract / retry gate +
6 build_layout_css YAML fixtures + 3 retry_gate fixtures).
Verification: python -m pytest -q tests = 89 passed (was 42).
horizontal-2 grid CSS strings (areas/cols/rows) byte-identical to
legacy; only additive col-axis keys are introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
129
tests/phase_z2/test_retry_gate.py
Normal file
129
tests/phase_z2/test_retry_gate.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""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"]
|
||||
Reference in New Issue
Block a user