feat(IMP-15): 실행-3 — classifier consumes image+table events
Issue #47 (IMP-15 실행-3 axis 3): extend `classify_visual_runtime_check` to consume the `image_events[]` and `table_events[]` arrays produced by `run_overflow_check` (실행-1/2) and widen `visual_check_passed`. Changes (src/phase_z2_classifier.py): - Remove `overflow.passed=True` early-return so image/table event scans always run, even when zone-level overflow was clean. - Deferred import of `IMAGE_ASPECT_DELTA_TOL` and `TABLE_SCROLL_TOL_PX` from `phase_z2_pipeline` (circular-safe SSoT; no duplicate literals). - New `image_events` scan emits `image_aspect_mismatch` when `delta is not None AND |delta| > IMAGE_ASPECT_DELTA_TOL` (delta=None ⇒ skip, image not loaded). - New `table_events` scan emits `tabular_overflow` when `wrapper_clipped_index is None AND (excess_x or excess_y > TABLE_SCROLL_TOL_PX)` (wrapper-clipped tables deduped against the existing zone cascade). - `visual_check_passed = overflow.passed AND not classifications` — any image/table classification now flips the gate. Guardrails preserved: - §3.2 8-rule zone cascade (clipped_inner / zone-self) untouched — the new emitters are ADDITIONAL. - `placement_diagnostics`, `categories_seen`, `unclassified_signals` return-shape preserved. - No `pipeline.py` production changes; no router action or `debug.json` passthrough changes. Tests (tests/phase_z2/test_phase_z2_visual_classifier.py — new): - `test_image_aspect_mismatch_emits_classification` (|delta|>TOL fires) - `test_image_aspect_delta_below_tol_no_classification` (≤TOL skipped) - `test_standalone_table_overflow_emits_classification` (wrapper_clipped_index=None, excess>TOL fires) - `test_table_dedup_when_wrapper_clipped` (wrapper_clipped_index set ⇒ no `tabular_overflow` emit) All 4 pure-dict (no Selenium / chromedriver / pipeline execution). Tolerances imported from `phase_z2_pipeline` (SSoT enforced via test import — no classifier-local literals). Verification (Stage 4): - New classifier tests: 4/4 PASS. - Regression `tests/phase_z2/` excluding new file: 93/93 PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
116
tests/phase_z2/test_phase_z2_visual_classifier.py
Normal file
116
tests/phase_z2/test_phase_z2_visual_classifier.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""IMP-15 실행-3 (Gitea issue #47) — classifier consumer pure-dict tests.
|
||||
|
||||
`classify_visual_runtime_check` was widened to consume the new
|
||||
``image_events[]`` / ``table_events[]`` arrays produced by ``run_overflow_check``
|
||||
(IMP-15 실행-1/2). The consumer must:
|
||||
|
||||
* emit ``image_aspect_mismatch`` when ``|delta| > IMAGE_ASPECT_DELTA_TOL`` and
|
||||
skip when ``delta is None`` or ``|delta| <= IMAGE_ASPECT_DELTA_TOL``;
|
||||
* emit ``tabular_overflow`` when a table self-overflows beyond
|
||||
``TABLE_SCROLL_TOL_PX`` and ``wrapper_clipped_index is None`` — and dedupe
|
||||
when the table sits under a wrapper already on the clipped-wrapper map
|
||||
(``wrapper_clipped_index`` non-null);
|
||||
* flip ``visual_check_passed`` to False whenever any classification fires, even
|
||||
if zone-level overflow was clean (``overflow["passed"]=True``).
|
||||
|
||||
All four cases are pure-dict — no Selenium / chromedriver dependency.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from src.phase_z2_classifier import classify_visual_runtime_check
|
||||
from src.phase_z2_pipeline import IMAGE_ASPECT_DELTA_TOL, TABLE_SCROLL_TOL_PX
|
||||
|
||||
|
||||
def _base_overflow(**overrides) -> dict:
|
||||
"""Minimal clean overflow result; tests overlay image/table events."""
|
||||
base = {
|
||||
"passed": True,
|
||||
"slide": {"overflowed": False},
|
||||
"slide_body": {"overflowed": False},
|
||||
"zones": [],
|
||||
"image_events": [],
|
||||
"table_events": [],
|
||||
}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
|
||||
# ─── image_events scan ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_image_aspect_mismatch_emits_classification():
|
||||
"""|delta| > IMAGE_ASPECT_DELTA_TOL ⇒ emit + flip visual_check_passed."""
|
||||
delta = IMAGE_ASPECT_DELTA_TOL + 0.05
|
||||
overflow = _base_overflow(image_events=[{
|
||||
"zone_position": "top",
|
||||
"zone_template_id": "f1b",
|
||||
"src": "img/sample.png",
|
||||
"natural_ratio": 2.0,
|
||||
"rendered_ratio": 2.0 * (1.0 + delta),
|
||||
"delta": delta,
|
||||
}])
|
||||
result = classify_visual_runtime_check(overflow, debug_zones=[])
|
||||
assert result["visual_check_passed"] is False
|
||||
assert result["categories_seen"] == ["image_aspect_mismatch"]
|
||||
assert len(result["classifications"]) == 1
|
||||
cls = result["classifications"][0]
|
||||
assert cls["category"] == "image_aspect_mismatch"
|
||||
assert cls["source"] == "image_event"
|
||||
assert cls["zone_position"] == "top"
|
||||
assert cls["delta"] == delta
|
||||
|
||||
|
||||
def test_image_aspect_delta_below_tol_no_classification():
|
||||
"""|delta| <= IMAGE_ASPECT_DELTA_TOL ⇒ skip (no false positive)."""
|
||||
delta = IMAGE_ASPECT_DELTA_TOL / 2.0
|
||||
overflow = _base_overflow(image_events=[{
|
||||
"zone_position": "top",
|
||||
"zone_template_id": "f1b",
|
||||
"src": "img/sample.png",
|
||||
"natural_ratio": 2.0,
|
||||
"rendered_ratio": 2.0 * (1.0 + delta),
|
||||
"delta": delta,
|
||||
}])
|
||||
result = classify_visual_runtime_check(overflow, debug_zones=[])
|
||||
assert result["visual_check_passed"] is True
|
||||
assert result["categories_seen"] == []
|
||||
assert result["classifications"] == []
|
||||
|
||||
|
||||
# ─── table_events scan ───────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_standalone_table_overflow_emits_classification():
|
||||
"""wrapper_clipped_index=None AND excess > TOL ⇒ emit tabular_overflow."""
|
||||
excess = TABLE_SCROLL_TOL_PX + 10
|
||||
overflow = _base_overflow(table_events=[{
|
||||
"zone_position": "bottom_l",
|
||||
"zone_template_id": "f13b",
|
||||
"wrapper_clipped_index": None,
|
||||
"excess_x": 0,
|
||||
"excess_y": excess,
|
||||
}])
|
||||
result = classify_visual_runtime_check(overflow, debug_zones=[])
|
||||
assert result["visual_check_passed"] is False
|
||||
assert result["categories_seen"] == ["tabular_overflow"]
|
||||
assert len(result["classifications"]) == 1
|
||||
cls = result["classifications"][0]
|
||||
assert cls["category"] == "tabular_overflow"
|
||||
assert cls["source"] == "table_event"
|
||||
assert cls["zone_position"] == "bottom_l"
|
||||
assert cls["excess_y"] == excess
|
||||
|
||||
|
||||
def test_table_dedup_when_wrapper_clipped():
|
||||
"""wrapper_clipped_index non-null ⇒ skip (dedupe with clipped_inner cascade)."""
|
||||
overflow = _base_overflow(table_events=[{
|
||||
"zone_position": "bottom_l",
|
||||
"zone_template_id": "f13b",
|
||||
"wrapper_clipped_index": 0,
|
||||
"excess_x": 0,
|
||||
"excess_y": TABLE_SCROLL_TOL_PX + 50,
|
||||
}])
|
||||
result = classify_visual_runtime_check(overflow, debug_zones=[])
|
||||
assert result["visual_check_passed"] is True
|
||||
assert result["categories_seen"] == []
|
||||
assert result["classifications"] == []
|
||||
Reference in New Issue
Block a user