"""issue #15 — density/readability gate 테스트 (Gitea #98 추천 1·2). 계약: - payload 결정론 + 측정 높이만 사용 (final.html 영향 0 — 리포팅 계층) - 임계값은 mdx 01~05 실측 캘리브레이션: dense<16 / tight<45 px/line (사람 판정 정렬: mdx03 105.8=ready, mdx02 40~44=cramped, mdx01 11.5=dense) - dense/tight 는 design_readiness 의 warnings(needs_review) — not_ready 아님 - presentation_ready 는 density 실패 시 False (#98 추천 1: 신호 통합) """ from __future__ import annotations from src.phase_z2_pipeline import ( _compute_density_gate, _compute_design_readiness, _estimate_max_card_lines, ) def _overflow(heights: dict[str, int]) -> dict: return {"passed": True, "zones": [ {"position": p, "clientHeight": h} for p, h in heights.items() ]} # ── _estimate_max_card_lines ──────────────────────────────────────── def test_repeat_slot_pattern_lines(): payload = { "title": "t", "_slot_count": 3, "pill_1_label": "라벨", "pill_1_body": ["a", "b", "c"], "pill_2_label": "라벨", "pill_2_body": ["a"], "pill_3_label": "", "pill_3_body": [], } # pill_1: label 1 + body 3 = 4 assert _estimate_max_card_lines(payload) == 4 def test_list_of_dict_columns_lines(): payload = {"pillars": [ {"label": "A", "lines": ["1", "2", "3"]}, {"label": "B", "lines": ["1"]}, ]} assert _estimate_max_card_lines(payload) == 4 # label 1 + 3 lines def test_br_joined_string_lines(): payload = {"row_1_body": "가

다"} assert _estimate_max_card_lines(payload) == 3 def test_non_card_payload_not_measurable(): assert _estimate_max_card_lines({"title": "t", "body_text": "x"}) == 0 # ── _compute_density_gate (실측 캘리브레이션 fixture) ───────────────── def _zone(pos: str, payload: dict, template="three_persona_benefits") -> dict: return {"position": pos, "template_id": template, "slot_payload": payload} def test_mdx03_calibration_comfortable(): """mdx03 left: 585px, pillar 최대 5라인 → 105.8px/line = comfortable.""" zones = [_zone("left", {"pillars": [ {"label": "L", "lines": ["1", "2", "3", "4"]}, ]})] gate = _compute_density_gate(zones, _overflow({"left": 585})) assert gate["passed"] is True assert gate["zones"][0]["verdict"] == "comfortable" assert gate["zones"][0]["px_per_line"] == 105.8 def test_mdx02_calibration_tight(): """mdx02 bottom: 339px, intro 7라인 → 40.4px/line = tight (사람: cramped).""" zones = [_zone("bottom", {"intro_sections": [ {"label": "a", "lines": ["1", "2", "3", "4", "5", "6"]}, # 7 lines {"label": "b", "lines": ["1", "2"]}, ]})] gate = _compute_density_gate(zones, _overflow({"bottom": 339})) assert gate["passed"] is False assert gate["worst_verdict"] == "tight" assert gate["zones"][0]["px_per_line"] == 40.4 def test_mdx01_calibration_dense(): """mdx01 bottom-right: 286px 표 20라인 → 11.5px/line = dense (#98 사람 판정 'bottom comparison dense').""" zones = [_zone("bottom-right", {"rows": [ {"cell": "
".join(str(i) for i in range(20))}, ]}, template="bim_dx_comparison_table")] gate = _compute_density_gate(zones, _overflow({"bottom-right": 286})) assert gate["worst_verdict"] == "dense" assert gate["zones"][0]["px_per_line"] == 11.5 def test_unmeasurable_zone_does_not_fail_gate(): zones = [_zone("top", {"title": "only-title"})] gate = _compute_density_gate(zones, _overflow({"top": 300})) assert gate["passed"] is True assert gate["zones"][0]["verdict"] == "not_measurable" def test_missing_height_not_measurable(): zones = [_zone("top", {"pill_1_label": "L", "pill_1_body": ["a"]})] gate = _compute_density_gate(zones, _overflow({})) assert gate["zones"][0]["verdict"] == "not_measurable" assert gate["passed"] is True # ── design_readiness 통합 ──────────────────────────────────────────── def _readiness(density_gate): return _compute_design_readiness( units=[], rendered_text_coverage={"passed": True}, applied_render_consistency={"passed": True}, density_gate=density_gate, ) def test_tight_zone_downgrades_to_needs_review(): gate = {"passed": False, "zones": [ {"position": "bottom", "template_id": "t", "verdict": "tight", "px_per_line": 40.4, "estimated_max_lines": 7, "zone_height_px": 339}, ]} r = _readiness(gate) assert r["status"] == "needs_review" # not_ready 아님 (사람 판정 정렬) assert any(w["code"] == "density_tight" for w in r["warnings"]) def test_dense_zone_also_needs_review_not_not_ready(): gate = {"passed": False, "zones": [ {"position": "br", "template_id": "t", "verdict": "dense", "px_per_line": 11.5, "estimated_max_lines": 20, "zone_height_px": 286}, ]} r = _readiness(gate) assert r["status"] == "needs_review" assert any(w["code"] == "density_dense" for w in r["warnings"]) def test_comfortable_gate_keeps_ready(): gate = {"passed": True, "zones": [ {"position": "left", "verdict": "comfortable"}, ]} r = _readiness(gate) assert r["status"] == "ready" def test_density_gate_none_backward_compat(): r = _compute_design_readiness( units=[], rendered_text_coverage={"passed": True}, applied_render_consistency={"passed": True}, ) assert r["status"] == "ready"