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>
130 lines
4.4 KiB
Python
130 lines
4.4 KiB
Python
"""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"]
|