From 2c7881c5cc8cca8e67d58e129873ab99e96c1116 Mon Sep 17 00:00:00 2001 From: kyeongmin Date: Fri, 3 Jul 2026 12:48:57 +0900 Subject: [PATCH] =?UTF-8?q?test(#13):=20=EA=B8=B0=EC=A1=B4=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=2011=EA=B1=B4=20=EC=88=98=EB=B3=B5=20=E2=80=94=20#98?= =?UTF-8?q?=20=EC=8A=A4=ED=8D=BC=ED=8A=B8=EC=9D=98=20=EC=9D=98=EB=8F=84?= =?UTF-8?q?=EB=90=9C=20=EB=B3=80=EA=B2=BD=EC=97=90=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B3=84=EC=95=BD=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 11건 전수 분류 결과 프로덕션 결함 0, 전부 '의도된 #98 변경 대비 낡은 테스트': - A. compute_zone_layout_cols (1건): Task 11d min-width floor(≤260px) 계약으로 갱신 - B. build_layout_css fixtures (3건): floor 반영값으로 재생성 (fixture refresh = 설계된 lock surface) - C. imp36 self-fire (1건): auto-fit collapsed 0px track 을 점유 트랙 카운트에서 제외 - D. imp88 salvage (5건): T19d(silent layout mutation 금지) 차단 계약으로 재작성 — 차단 터미널/feasibility 선행 차단/전진 금지/trace 필드 4개 측면 + 정책 차단 체인 - E. step17 case_c (1건): 실행 2단 + T28.5 no_font_shrink 정책 차단 터미널로 갱신 - 89a_pre_baseline_sha.json 재캡처 (capture 스크립트 docstring 이 명시한 선행 drift — #98 스퍼트 기간 누적, 정규 절차) 검증 게이트: pytest tests/phase_z2 + phase_z2_ai_fallback + regression = 990 passed 0 failed 참고: T19d 차단으로 _attempt_salvage_chain 의 layout_adjust 실행 코드는 도달 불가 dead code 상태 — 제거는 #16 (height-fit 재설계) 에서 판단 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../left-1-right-2_default.yaml | 10 +- .../left-2-right-1_default.yaml | 10 +- .../build_layout_css/vertical-2_weighted.yaml | 10 +- .../phase_z2/test_compute_zone_layout_cols.py | 16 +- .../test_imp36_overflow_chain_self_fire.py | 6 +- .../test_phase_z2_pipeline_salvage_imp88.py | 213 +++++++----------- .../test_phase_z2_step17_salvage_chain.py | 30 +-- .../fixtures/89a_pre_baseline_sha.json | 26 +-- 8 files changed, 146 insertions(+), 175 deletions(-) diff --git a/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_default.yaml b/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_default.yaml index e18e447..3890944 100644 --- a/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_default.yaml +++ b/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_default.yaml @@ -19,20 +19,20 @@ input: override_zone_geometries: null expected_layout_css: areas: '"left right-top" "left right-bottom"' - cols: 777px 389px + cols: 691px 475px rows: 286px 285px heights_px: - 286 - 285 widths_px: - - 777 - - 389 + - 691 + - 475 ratios: - 0.489 - 0.487 width_ratios: - - 0.658 - - 0.33 + - 0.586 + - 0.403 computation: 2d_dynamic_aggregated dynamic_rows: true dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_default.yaml b/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_default.yaml index 90157f8..2124f04 100644 --- a/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_default.yaml +++ b/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_default.yaml @@ -19,20 +19,20 @@ input: override_zone_geometries: null expected_layout_css: areas: '"left-top right" "left-bottom right"' - cols: 389px 777px + cols: 475px 691px rows: 286px 285px heights_px: - 286 - 285 widths_px: - - 389 - - 777 + - 475 + - 691 ratios: - 0.489 - 0.487 width_ratios: - - 0.33 - - 0.658 + - 0.403 + - 0.586 computation: 2d_dynamic_aggregated dynamic_rows: true dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/vertical-2_weighted.yaml b/tests/phase_z2/fixtures/build_layout_css/vertical-2_weighted.yaml index a146315..6b9e71d 100644 --- a/tests/phase_z2/fixtures/build_layout_css/vertical-2_weighted.yaml +++ b/tests/phase_z2/fixtures/build_layout_css/vertical-2_weighted.yaml @@ -14,18 +14,18 @@ input: override_zone_geometries: null expected_layout_css: areas: '"left right"' - cols: 816px 350px + cols: 712px 454px rows: 1fr heights_px: - 585 widths_px: - - 816 - - 350 + - 712 + - 454 ratios: - 1.0 width_ratios: - - 0.692 - - 0.297 + - 0.603 + - 0.385 computation: content_weight_distribution_cols dynamic_rows: false dynamic_cols: true diff --git a/tests/phase_z2/test_compute_zone_layout_cols.py b/tests/phase_z2/test_compute_zone_layout_cols.py index d487b5b..80aaf93 100644 --- a/tests/phase_z2/test_compute_zone_layout_cols.py +++ b/tests/phase_z2/test_compute_zone_layout_cols.py @@ -2,7 +2,8 @@ Column-axis weight-only solver. Mirrors compute_zone_layout for rows. No min_width_px contract exists in frame_contracts.yaml (verified -during Stage 2), so column distribution is purely content_weight. +during Stage 2). Task 11d (#98) 이후: per-zone min-width floor(≤260px) +예약 후 잔여분을 content_weight 비례 배분 (0px track 방지). """ from __future__ import annotations @@ -37,12 +38,21 @@ def test_two_equal_zones_split_evenly(): def test_asymmetric_weights_distribute_by_ratio(): + # Task 11d (#98) — 순수 비례가 아니라 per-zone min-width floor(≤260px) 를 + # 먼저 예약한 뒤 잔여분만 weight 비례 배분한다 ("keep every occupied grid + # track renderable"). 0.8/0.2 두 zone 기준 기대값을 floor 계약으로 검증. zones = [_zone("left", 0.8), _zone("right", 0.2)] result = compute_zone_layout_cols(zones) available = SLIDE_BODY_WIDTH - GRID_GAP assert sum(result["widths_px"]) == available - # left should be ~4x right - assert result["widths_px"][0] > result["widths_px"][1] * 3 + min_width_px = min(260, max(1, available // 2)) + distributable = available - min_width_px * 2 + expected_left = min_width_px + int(round(distributable * 0.8)) + assert result["widths_px"][0] == expected_left + # floor 위에서도 좌우 우열은 유지되어야 한다. + assert result["widths_px"][0] > result["widths_px"][1] + # weight_shares 는 raw 비율 그대로 (floor 미반영) 기록된다. + assert result["weight_shares"] == [0.8, 0.2] def test_zero_weight_guard_equal_split(): diff --git a/tests/phase_z2/test_imp36_overflow_chain_self_fire.py b/tests/phase_z2/test_imp36_overflow_chain_self_fire.py index 8539e16..711b548 100644 --- a/tests/phase_z2/test_imp36_overflow_chain_self_fire.py +++ b/tests/phase_z2/test_imp36_overflow_chain_self_fire.py @@ -231,7 +231,11 @@ def _measure(drv, frame: dict, html_path: Path) -> dict: "return el ? getComputedStyle(el).fontSize : null;", frame["text_sel"], ) - tracks = [t for t in (cols_tpl or "").split() if t] + # Task 11 (#98) — 일부 family 가 repeat(3, 1fr) → repeat(auto-fit, minmax()) + # 로 전환 (dynamic cardinality). auto-fit 의 collapsed track 은 computed + # style 에 0px 로 나타나므로, IMP-36 P1 계약("점유 트랙 수")은 non-zero + # track 만 센다. + tracks = [t for t in (cols_tpl or "").split() if t and t != "0px"] return {"cols": cols_tpl, "tracks": len(tracks), "lh": lh, "fs": fs} diff --git a/tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py b/tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py index fff7117..34173af 100644 --- a/tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py +++ b/tests/phase_z2/test_phase_z2_pipeline_salvage_imp88.py @@ -16,25 +16,22 @@ Stage 2 binding contract (u6): planner emits a frame-scoped CSS rule via `apply_frame_internal_fit_candidate_css`. +T19d (#98) 정정 — layout_adjust 는 silent topology mutation 금지로 +터미널 차단됨 (_attempt_salvage_chain 의 T19d 분기). 구 u6 의 +"layout_adjust 실행" 계약(옛 2/3/4/7/8번 테스트)은 차단 계약으로 대체. + Test surfaces (8 tests): 1. `_SALVAGE_FAIL_BY_ACTION` map registers the two new actions with the failure_type names the failure_router (u2) cascade rows expect. - 2. `layout_adjust` PASS — out_path promoted with the swapped render, - step records new_layout_preset, cascade exits. - 3. `layout_adjust` infeasible (no sibling for `single` preset) — step - records failure_reason without invoking render_slide; cascade - advances to frame_internal_fit_candidate. - 4. `layout_adjust` rerender FAIL — cascade advances to - frame_internal_fit_candidate (which then PASSes via patched - envelope). + 2. layout_adjust 차단 터미널 — step 없음, render 없음, out_path 보존. + 3. 차단은 feasibility 판정보다 먼저 (plan_layout_adjust 미호출). + 4. 차단 시 frame_internal_fit_candidate 로 전진하지 않음. 5. `frame_internal_fit_candidate` PASS via patched envelope — out_path - promoted with CSS-overlay candidate. + promoted with CSS-overlay candidate (direct entry 경로는 존속). 6. `frame_internal_fit_candidate` no-envelope — step records envelope_present=False; cascade exits via frame_reselect terminal. - 7. Full 5-step cascade all fail — loop cap (range(len(map))=5) - respected; exactly 5 steps recorded; out_path preserved. - 8. `layout_adjust` uses no CSS-overlay path (css_override field is - absent from the layout_adjust step; new_layout_preset is present). + 7. CSS-overlay 3단 전부 실패 → layout_adjust 차단 터미널 (steps=3). + 8. 차단 터미널 trace 필드 observability (terminal_action / rationale). """ from __future__ import annotations @@ -146,21 +143,23 @@ def test_salvage_fail_map_registers_imp88_actions(): assert "image_fit" not in _SALVAGE_FAIL_BY_ACTION -# ── 2. layout_adjust PASS branch ──────────────────────────────────── +# ── 2. layout_adjust — T19d(#98) 차단 터미널 ──────────────────────── +# T19d 이후 layout_adjust 는 실행되지 않는다: "retry/salvage path 가 explicit +# layout override 없이 final.html 의 zone topology 를 바꾸면 안 된다" (silent +# topology mutation 금지). 아래 테스트들은 구 IMP-88 u6 의 실행 계약 대신 +# 차단 계약을 잠근다. -def test_layout_adjust_pass_promotes_final_html(project_tmp, monkeypatch): - """initial_failure_type=font_step_insufficient routes to layout_adjust; - plan_layout_adjust swaps horizontal-2 → vertical-2 (rows ↔ cols, swap - priority 0). render_slide is invoked with the NEW preset; overflow - passes → out_path promoted; step records new_layout_preset; cascade - exits with salvage_passed=True. No CSS overlay path was used.""" +def test_layout_adjust_blocked_terminal_preserves_final_html(project_tmp, monkeypatch): + """initial_failure_type=font_step_insufficient 는 layout_adjust 로 라우팅 + 되지만, T19d 차단으로 step 기록 없이 즉시 터미널 종료한다. render 미호출, + out_path 원본 보존, salvage_passed=False.""" out_path = project_tmp / "final.html" out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8") counter = _patch_render(monkeypatch) monkeypatch.setattr( _pz_pipeline, "run_overflow_check", - lambda p: {"passed": True, "fail_reasons": []}, + lambda p: pytest.fail("layout_adjust 차단 시 overflow_check 호출 금지"), ) trace = _attempt_salvage_chain( @@ -173,100 +172,65 @@ def test_layout_adjust_pass_promotes_final_html(project_tmp, monkeypatch): ) assert trace["salvage_attempted"] is True - assert trace["salvage_passed"] is True - assert len(trace["salvage_steps"]) == 1 - step0 = trace["salvage_steps"][0] - assert step0["action"] == "layout_adjust" - assert step0["passed"] is True - assert step0["new_layout_preset"] == "vertical-2" - assert step0["plan"]["feasible"] is True - # layout_adjust uses the distinct render path — NOT the CSS-overlay path. - assert "css_override" not in step0 - # render_slide was invoked exactly once with the NEW preset. - assert counter["n"] == 1 - promoted = out_path.read_text(encoding="utf-8") - assert "ORIGINAL_BEFORE_SALVAGE" not in promoted - assert "data-rendered-preset='vertical-2'" in promoted + assert trace["salvage_passed"] is False + assert trace["salvage_steps"] == [] + assert trace["salvage_terminal_action"] == ( + "layout_adjust_blocked_no_silent_layout_mutation" + ) + assert "kept original layout" in trace["salvage_terminal_rationale"] + assert counter["n"] == 0 + assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE" -# ── 3. layout_adjust infeasible (no sibling) ──────────────────────── +# ── 3. layout_adjust 차단은 feasibility 판정보다 먼저 ──────────────── -def test_layout_adjust_infeasible_no_sibling_cascade_advances(project_tmp, monkeypatch): - """`single` preset has no render-ready unit_count=1 sibling (catalog - design — single and grid-2x2 have no swap target). layout_adjust - returns feasible=False; render_slide is NOT invoked; cascade - advances to frame_internal_fit_candidate. Patched get_contract - returns no envelope → that branch also infeasible → cascade exits - at the frame_reselect terminal action.""" +def test_layout_adjust_blocked_before_feasibility_check(project_tmp, monkeypatch): + """T19d 차단은 plan_layout_adjust 호출 이전에 발생한다 — swap sibling 이 + 존재하는 horizontal-2 (feasible 했을 케이스)에서도 step 기록 없이 동일 + 터미널. plan 계산/렌더 어느 쪽도 실행되지 않음을 잠근다.""" out_path = project_tmp / "final.html" out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8") counter = _patch_render(monkeypatch) monkeypatch.setattr( - _pz_pipeline, "run_overflow_check", - lambda p: pytest.fail( - "run_overflow_check must not run when no candidate is emitted" - ), - ) - monkeypatch.setattr( - _pz_mapper, "get_contract", lambda _tid: None, + _pz_pipeline, "plan_layout_adjust", + lambda **kw: pytest.fail("T19d 차단 시 plan_layout_adjust 호출 금지"), ) trace = _attempt_salvage_chain( run_dir=project_tmp, out_path=out_path, slide_title="imp88-u6", slide_footer=None, - zones_data=[{"position": "primary", "template_id": "t-only", - "content_weight": {"score": 1.0}}], - layout_preset="single", layout_css=_LAYOUT_CSS_GATE_PASS, - cascade_inputs={**_ci_image(), "zone_position": "primary"}, + zones_data=_horizontal_zones(), + layout_preset="horizontal-2", layout_css=_LAYOUT_CSS_GATE_PASS, + cascade_inputs=_ci_image(), initial_failure_type="font_step_insufficient", gap_px=14, ) - assert trace["salvage_attempted"] is True assert trace["salvage_passed"] is False - actions = [s["action"] for s in trace["salvage_steps"]] - assert actions == ["layout_adjust", "frame_internal_fit_candidate"] - s0, s1 = trace["salvage_steps"] - assert s0["plan"]["feasible"] is False - assert "no render-ready" in (s0["plan"]["failure_reason"] or "") - assert s0["new_layout_preset"] is None - assert s1["plan"]["feasible"] is False - assert s1["plan"]["envelope_present"] is False - # No candidate ever rendered (layout_adjust infeasible → no render call; - # frame_internal_fit_candidate infeasible → no render call). + assert trace["salvage_steps"] == [] + assert trace["salvage_terminal_action"] == ( + "layout_adjust_blocked_no_silent_layout_mutation" + ) assert counter["n"] == 0 - # frame_reselect is the next routing target after - # frame_internal_fit_candidate_insufficient — not in salvage map → terminal. - assert trace.get("salvage_terminal_action") == "frame_reselect" - # Original final.html unchanged. assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE" -# ── 4. layout_adjust rerender FAIL → frame_internal_fit_candidate PASS ── +# ── 4. layout_adjust 차단 시 frame_internal_fit 로 전진하지 않음 ───── -def test_layout_adjust_fail_cascade_to_frame_internal_fit_pass(project_tmp, monkeypatch): - """layout_adjust feasible but post-swap overflow persists → - failure_type=layout_adjust_insufficient → routes to - frame_internal_fit_candidate. Patched contract provides an envelope - variant that absorbs the excess; that branch PASSes → out_path - promoted with the CSS-overlay candidate.""" +def test_layout_adjust_blocked_no_cascade_advance(project_tmp, monkeypatch): + """T19d 차단은 cascade 전진(layout_adjust_insufficient → frame_internal_ + fit_candidate)이 아니라 즉시 터미널이다. feasible envelope contract 가 + 준비돼 있어도 frame_internal_fit_candidate 는 실행되지 않는다 — 구 + IMP-88 u6 의 '4. rerender FAIL → cascade advance' 계약을 대체.""" out_path = project_tmp / "final.html" out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8") counter = _patch_render(monkeypatch) - - # First overflow check (after layout_adjust render) FAILS, - # second (after frame_internal_fit CSS overlay) PASSes. - overflow_results = iter([ - {"passed": False, "fail_reasons": ["zone overflow persists"]}, - {"passed": True, "fail_reasons": []}, - ]) monkeypatch.setattr( _pz_pipeline, "run_overflow_check", - lambda p: next(overflow_results), + lambda p: pytest.fail("차단 터미널에서 overflow_check 호출 금지"), ) - - # Stub contract with a feasible internal_envelope variant covering 40px excess. + # envelope 이 준비된 contract 라도 도달 자체가 없어야 한다. stub_contract = { "internal_envelope": { "variants": [ @@ -289,20 +253,13 @@ def test_layout_adjust_fail_cascade_to_frame_internal_fit_pass(project_tmp, monk initial_failure_type="font_step_insufficient", gap_px=14, ) - assert trace["salvage_passed"] is True - assert len(trace["salvage_steps"]) == 2 - s0, s1 = trace["salvage_steps"] - assert s0["action"] == "layout_adjust" - assert s0["passed"] is False - assert s0["plan"]["feasible"] is True - assert s1["action"] == "frame_internal_fit_candidate" - assert s1["passed"] is True - assert s1["plan"]["selected_variant"] == "internal_grid_row" - assert s1["css_override"] - assert 'data-template-id="t-top"' in s1["css_override"] - # Two render_slide calls (one per dispatched branch). - assert counter["n"] == 2 - assert "ORIGINAL_BEFORE_SALVAGE" not in out_path.read_text(encoding="utf-8") + assert trace["salvage_passed"] is False + assert trace["salvage_steps"] == [] + assert trace["salvage_terminal_action"] == ( + "layout_adjust_blocked_no_silent_layout_mutation" + ) + assert counter["n"] == 0 + assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE" # ── 5. frame_internal_fit_candidate PASS (direct entry) ───────────── @@ -393,15 +350,15 @@ def test_frame_internal_fit_candidate_no_envelope_cascade_terminal(project_tmp, assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE" -# ── 7. Loop cap respected — 5 stages all fail ─────────────────────── +# ── 7. CSS-overlay 3단 실패 후 layout_adjust 차단 터미널 ───────────── -def test_full_5_step_cascade_all_fail_loop_cap_respected(project_tmp, monkeypatch): - """Start at donor_slack_insufficient and force every cascade stage to - fail: cross_zone (no fit_analysis), glue (excess > envelope), font_step - (no headroom), layout_adjust (single → no sibling), frame_internal_fit - (no contract). Loop iterates exactly len(_SALVAGE_FAIL_BY_ACTION)=5 - times → salvage_steps has 5 entries in the exact cascade order.""" +def test_cascade_stops_at_font_step_policy_block(project_tmp, monkeypatch): + """donor_slack_insufficient 에서 시작: cross_zone(fail) → glue(fail) → + font_step 은 T28.5 no_font_shrink 정책 차단 — blocked step 을 기록하고 + 터미널 종료한다. layout_adjust / frame_internal_fit_candidate 는 도달 + 불가 (구 '5단 loop cap' 계약을 대체 — 실행 가능 cascade 는 2단 + 정책 + 차단 2종).""" out_path = project_tmp / "final.html" out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8") counter = _patch_render(monkeypatch) @@ -416,7 +373,6 @@ def test_full_5_step_cascade_all_fail_loop_cap_respected(project_tmp, monkeypatc trace = _attempt_salvage_chain( run_dir=project_tmp, out_path=out_path, slide_title="imp88-u6", slide_footer=None, - # single preset → layout_adjust will be infeasible (no sibling) zones_data=[{"position": "primary", "template_id": "t-only", "content_weight": {"score": 1.0}}], layout_preset="single", layout_css=_LAYOUT_CSS_GATE_PASS, @@ -432,38 +388,37 @@ def test_full_5_step_cascade_all_fail_loop_cap_respected(project_tmp, monkeypatc ) assert trace["salvage_passed"] is False - assert len(trace["salvage_steps"]) == 5 + assert len(trace["salvage_steps"]) == 3 actions = [s["action"] for s in trace["salvage_steps"]] assert actions == [ "cross_zone_redistribute", "glue_compression", "font_step_compression", - "layout_adjust", - "frame_internal_fit_candidate", ] - # All 5 are infeasible — no candidate rendering anywhere. + # 어떤 branch 도 candidate 렌더 없음. assert counter["n"] == 0 - # Loop exhausted at cap (no mid-cascade terminal_action since each - # next_action stayed in _SALVAGE_FAIL_BY_ACTION through 5 stages). - assert "salvage_terminal_action" not in trace + # font_step 은 T28.5 정책 차단 step (blocked_by_policy) + 터미널. + assert trace["salvage_steps"][2]["blocked_by_policy"] == "no_font_shrink" + assert trace["salvage_terminal_action"] == ( + "font_step_compression_blocked_no_font_shrink" + ) assert out_path.read_text(encoding="utf-8") == "ORIGINAL_BEFORE_SALVAGE" -# ── 8. layout_adjust uses the distinct render path (no CSS overlay) ── +# ── 8. 차단 터미널 trace 필드 observability ────────────────────────── -def test_layout_adjust_step_has_no_css_overlay_field(project_tmp, monkeypatch): - """layout_adjust's render path is qualitatively different from the - CSS-overlay planners (glue / font_step / cross_zone / frame_internal_fit): - it calls render_slide with the NEW preset + remapped zones_data + new - layout_css. The step dict therefore omits `css_override` and surfaces - `new_layout_preset` instead — observability for downstream classifiers.""" +def test_layout_adjust_block_trace_fields(project_tmp, monkeypatch): + """T19d 차단 터미널의 관측 계약: salvage_terminal_action 은 고정 문자열, + salvage_terminal_rationale 은 '왜 layout 을 유지했는지'를 설명하며, + salvage_steps 에 layout_adjust entry 가 존재하지 않는다 (구 '8. distinct + render path' 관측 계약을 대체).""" out_path = project_tmp / "final.html" out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8") _patch_render(monkeypatch) monkeypatch.setattr( _pz_pipeline, "run_overflow_check", - lambda p: {"passed": True, "fail_reasons": []}, + lambda p: pytest.fail("차단 터미널에서 overflow_check 호출 금지"), ) trace = _attempt_salvage_chain( @@ -475,9 +430,9 @@ def test_layout_adjust_step_has_no_css_overlay_field(project_tmp, monkeypatch): initial_failure_type="font_step_insufficient", gap_px=14, ) - step0 = trace["salvage_steps"][0] - assert step0["action"] == "layout_adjust" - # Distinct render path observability contract: - assert "css_override" not in step0 - assert "new_layout_preset" in step0 - assert "candidate_path" in step0 + assert trace["salvage_terminal_action"] == ( + "layout_adjust_blocked_no_silent_layout_mutation" + ) + rationale = trace["salvage_terminal_rationale"] + assert "zone positions" in rationale and "kept original layout" in rationale + assert all(s["action"] != "layout_adjust" for s in trace["salvage_steps"]) diff --git a/tests/phase_z2/test_phase_z2_step17_salvage_chain.py b/tests/phase_z2/test_phase_z2_step17_salvage_chain.py index efebc32..f57dedd 100644 --- a/tests/phase_z2/test_phase_z2_step17_salvage_chain.py +++ b/tests/phase_z2/test_phase_z2_step17_salvage_chain.py @@ -203,13 +203,12 @@ def test_case_c_all_three_fail_revert_preserved(project_tmp, monkeypatch): False, out_path is unchanged (original final.html intact — (b)-revert preserved). - IMP-88 u6 extends the cascade depth from 3 to 5 stages (layout_adjust + - frame_internal_fit_candidate added). When all stages are infeasible the - cascade now runs through all five — the empty zones_data carried by - _kwargs() makes plan_layout_adjust infeasible (length mismatch) and the - empty resulting template_id makes plan_frame_internal_fit_candidate - infeasible (no contract). The (b)-revert contract this test locks - (out_path untouched + salvage_passed=False) is unchanged. + IMP-88 u6 는 cascade 를 5단으로 확장했으나, T19d (#98) 가 layout_adjust + 를 silent topology mutation 금지로 터미널 차단함 — 실행 가능한 cascade + 는 CSS-overlay 3단(cross_zone/glue/font_step)이고, 3단 전부 실패 시 + layout_adjust 진입 시점에 차단 터미널로 종료된다. The (b)-revert + contract this test locks (out_path untouched + salvage_passed=False) + is unchanged. """ out_path = project_tmp / "final.html" out_path.write_text("ORIGINAL_BEFORE_SALVAGE", encoding="utf-8") @@ -240,21 +239,24 @@ def test_case_c_all_three_fail_revert_preserved(project_tmp, monkeypatch): assert trace["salvage_attempted"] is True assert trace["salvage_passed"] is False - # IMP-88 u6 — cascade depth extended from 3 to 5; see _SALVAGE_FAIL_BY_ACTION. - assert len(trace["salvage_steps"]) == 5 + # T28.5 / T19d (#98) — font_step 은 no_font_shrink 정책 차단(blocked + # step 기록 + 터미널), layout_adjust 는 silent topology mutation 금지 + # (도달 불가). cross_zone/glue 실패 후 font_step 차단에서 종료 — + # IMP-88 u6 의 5단 깊이는 실행 2단 + 정책 차단으로 재편됨. + assert len(trace["salvage_steps"]) == 3 actions = [s["action"] for s in trace["salvage_steps"]] assert actions == [ "cross_zone_redistribute", "glue_compression", "font_step_compression", - "layout_adjust", - "frame_internal_fit_candidate", ] + assert trace["salvage_steps"][2]["blocked_by_policy"] == "no_font_shrink" + assert trace["salvage_terminal_action"] == ( + "font_step_compression_blocked_no_font_shrink" + ) for step in trace["salvage_steps"]: assert step["passed"] is False - # layout_adjust uses a distinct render path → its step dict has no - # css_override key (new_layout_preset is the observability field - # instead). All other branches use the shared CSS-overlay path. + # 전 step shared CSS-overlay path — css_override 는 미배출(None). assert step.get("css_override") is None assert step["failure_reason"] # No CSS / candidate emitted anywhere → no render_slide calls either. diff --git a/tests/regression/fixtures/89a_pre_baseline_sha.json b/tests/regression/fixtures/89a_pre_baseline_sha.json index 59f90ee..649b665 100644 --- a/tests/regression/fixtures/89a_pre_baseline_sha.json +++ b/tests/regression/fixtures/89a_pre_baseline_sha.json @@ -2,7 +2,7 @@ "schema_version": 2, "axis": "IMP-89 89-a u4 — final.html SHA baseline captured via FULL run_phase_z2_mvp1 pipeline (flag OFF / default)", "description": "Frozen SHA-256 of `final.html` bytes (the artifact written to disk at src/phase_z2_pipeline.py:5994-5996) captured by running the full Phase Z pipeline end-to-end for each mdx 01-05 under PHASE_Z_B4_MAPPER_SOURCE=OFF. Under flag OFF the 89-a selector `_select_mapper_template_id(plan, T)` returns `T` verbatim, so the mapper input is byte-identical to the pre-89-a legacy call shape `map_mdx_to_slots(section, unit.frame_template_id)` — the rendered HTML and therefore the final.html SHA match the pre-89-a baseline. The u4 regression test runs the same pipeline shape under flag OFF and asserts SHA equality. Regenerate only when an upstream mapper/render/template delta is deliberately reviewed and accepted.", - "captured_at_utc": "2026-05-27T04:19:30Z", + "captured_at_utc": "2026-07-03T03:39:01Z", "renderer": { "entrypoint": "src.phase_z2_pipeline.run_phase_z2_mvp1", "write_site": "src/phase_z2_pipeline.py:5994-5996", @@ -19,37 +19,37 @@ "01.mdx": { "mdx_file": "01.mdx", "run_id": "89a_baseline_01", - "final_html_size_bytes": 29959, - "sha256": "62d793166ca4cdd8d8d1d134d8659b43dce95bb958e7efd6ce0e72619b3c335d", + "final_html_size_bytes": 38731, + "sha256": "749ebb626e284a62d37b5be32af97c61ae2a5b8d4226d1c3fa90f04c843d9fef", "pipeline_exit_code": null }, "02.mdx": { "mdx_file": "02.mdx", "run_id": "89a_baseline_02", - "final_html_size_bytes": 25789, - "sha256": "333defb22f5da940b0bb3b3563730d12bd901d7b5d870a2aaef3f0c929d2dad1", + "final_html_size_bytes": 42127, + "sha256": "835d67eb7003bbcc55f3ea9ea73e6c781bb3b51d021a860200876c1d93cecfd2", "pipeline_exit_code": null }, "03.mdx": { "mdx_file": "03.mdx", "run_id": "89a_baseline_03", - "final_html_size_bytes": 40550, - "sha256": "d84e32b402cfbdcab6025b584b5e2657766f11b3fc110c217be0551e39f3f516", + "final_html_size_bytes": 40217, + "sha256": "44abaaf8fd0737002188c7af0ae3bd0dd281b313f74b8a50ddfd4aa0ddef3eb7", "pipeline_exit_code": null }, "04.mdx": { "mdx_file": "04.mdx", "run_id": "89a_baseline_04", - "final_html_size_bytes": 28725, - "sha256": "da3668d76fc582a1a26ad930b54a8abb7079969ebf91e79cfe7dd273ec4a0e95", - "pipeline_exit_code": null + "final_html_size_bytes": 40206, + "sha256": "8ba35bc6642b0e651cd46b9216070bd49052b016807611d9771fd630ca7a2613", + "pipeline_exit_code": 1 }, "05.mdx": { "mdx_file": "05.mdx", "run_id": "89a_baseline_05", - "final_html_size_bytes": 21739, - "sha256": "5baa8478a1316403dd3e7b7bccaaaee20fe6ac7738ea7ddd2af2f19b4b294f2c", - "pipeline_exit_code": 1 + "final_html_size_bytes": 36844, + "sha256": "397f238df2d23c7f3371d9032de5fbae4fd37aca116bb8b62e8fd36c4255c26d", + "pipeline_exit_code": null } }, "total_files": 5