Files
C.E.L_Slide_test2/tests/phase_z2/test_retry_gate.py
kyeongmin 201099e53b 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>
2026-05-16 12:03:23 +09:00

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"]