"""Tests pour `_should_reject_on_text_mismatch` — patch 2026-05-23 : distinguer `observed=''` (OCR n'a rien lu, ambigu) de `observed='X'` (autre texte lu = mismatch confirmé) dans le pré-check OCR. Brief Codex 2026-05-23 08:55 : le crop bbox SoM précis (50 × 48 px) sur un onglet Notepad moderne donne `observed=''` car EasyOCR n'a pas suffisamment de signal (texte peu contrasté, zone trop petite). Le patch précédent rejetait ce cas comme mismatch — alors qu'aucune preuve d'un mauvais clic n'existe. On ne rejette plus que quand l'OCR a effectivement lu autre chose que la cible attendue. Le faux succès OBS Studio reste bloqué : (1) son OCR retournait `'ue audio disponible GUI OBS Studio…'` = non-vide → rejet conservé ; (2) la garde drift agent posée sur ANCHOR-TM bloque déjà ce match. """ from __future__ import annotations import sys from pathlib import Path ROOT = Path(__file__).parent.parent.parent sys.path.insert(0, str(ROOT)) from agent_v0.server_v1.resolve_engine import ( # noqa: E402 _should_reject_on_text_mismatch, ) class TestShouldRejectOnTextMismatch: def test_valid_passes(self): """Cas nominal : OCR a vu la cible → on ne rejette pas.""" assert not _should_reject_on_text_mismatch( is_valid=True, observed="Enregistrer sous", ) def test_invalid_with_text_rejects(self): """Cas 0745 historique : OCR voit '9 ?' qui ne matche pas 'Enregistrer sous' → rejet confirmé.""" assert _should_reject_on_text_mismatch( is_valid=False, observed="9 ?", ) def test_invalid_with_obs_studio_rejects(self): """Cas 0756 : OCR voit du texte OBS Studio → rejet confirmé.""" assert _should_reject_on_text_mismatch( is_valid=False, observed="ue audio disponible GUI OBS Studio", ) def test_invalid_with_empty_observed_does_not_reject(self): """Cas 0855 : OCR n'a rien lu (zone trop petite/peu contrastée) → ambigu, pas un mismatch confirmé. On préserve la résolution serveur — la garde drift agent protège en aval.""" assert not _should_reject_on_text_mismatch( is_valid=False, observed="", ) def test_invalid_with_whitespace_only_does_not_reject(self): """Espace seul = équivalent vide pour notre logique.""" assert not _should_reject_on_text_mismatch( is_valid=False, observed=" ", ) def test_invalid_with_newline_only_does_not_reject(self): assert not _should_reject_on_text_mismatch( is_valid=False, observed="\n\t", ) def test_invalid_with_none_observed_does_not_reject(self): """Robustesse : observed None (cas dégénéré OCR-lib absente) ne doit pas planter.""" assert not _should_reject_on_text_mismatch( is_valid=False, observed=None, ) def test_valid_with_empty_passes(self): """is_valid=True avec observed vide — ne peut normalement pas arriver via _text_match_fuzzy (qui retourne False sur vide) mais on garde la logique cohérente : si is_valid=True, on ne rejette pas, peu importe observed.""" assert not _should_reject_on_text_mismatch( is_valid=True, observed="", )