"""Repro offline du bug fonctionnel : replay_sess_4c38dbb8 / act_raw_6c1432b3. L'agent rapporte success=True après avoir cliqué sur le bouton "Enregistrer" du dialog "Enregistrer sous", mais la fenêtre active après le clic est "rpa_vision : Explorateur de fichiers" — l'app a basculé hors du Bloc-notes. Le Validator MVP P0 doit attribuer failure_category=WRONG_APPLICATION via OcrRoiChecker (token suspect 'explorateur de fichiers' dans la ROI) et donc override success → False. Stratégie de fixture : - screenshot_after synthétique : 800×600 avec "rpa_vision : Explorateur de fichiers" au centre (= bug observé : la fenêtre est passée à l'Explorateur). - screenshot_before : dialog "Enregistrer sous" (texte centré). - action : click_anchor sur "Enregistrer" au centre (x_pct=0.5, y_pct=0.5). - OCR injecté : fake qui retourne le texte du screenshot_after. """ from __future__ import annotations import base64 import io import pytest pytestmark = [pytest.mark.integration] def _png_b64(img) -> str: buf = io.BytesIO() img.save(buf, format="PNG") return base64.b64encode(buf.getvalue()).decode("ascii") def _make_screenshot(text: str, color=(245, 245, 245), size=(1920, 1080)): """Screenshot 1920x1080 avec un texte centré (visible dans la ROI 80px).""" from PIL import Image, ImageDraw img = Image.new("RGB", size, color=color) draw = ImageDraw.Draw(img) cx, cy = size[0] // 2, size[1] // 2 draw.text((cx - 200, cy - 8), text, fill=(0, 0, 0)) return img @pytest.fixture def bug_step10_fixtures(): """Reproduit la situation act_raw_6c1432b3 sans OCR réel. L'OCR est mocké pour retourner ce que verrait EasyOCR sur le screenshot after. """ before = _png_b64(_make_screenshot("Enregistrer sous")) after = _png_b64(_make_screenshot("rpa_vision : Explorateur de fichiers")) action = { "type": "click", "action_id": "act_raw_6c1432b3", "by_text": "Enregistrer", "target_spec": { "by_text": "Enregistrer", "window_title": "Enregistrer sous", }, # Position normalisée au centre du screen (où le bouton "Enregistrer" # était attendu d'après replay_sess_4c38dbb8.failures.jsonl) "x_pct": 0.5289, "y_pct": 0.7913, } # L'agent rapporte success=True (c'est le bug : pixel-diff legacy ne discrimine pas) result = { "success": True, "actual_position": {"x_pct": 0.5289, "y_pct": 0.7913}, } return before, after, action, result def test_validator_detects_wrong_application_on_act_raw_6c1432b3(bug_step10_fixtures): """Le Validator doit retourner WRONG_APPLICATION malgré success=True client.""" from core.validation import OcrRoiChecker, Validator, Verdict, FailureCategory before, after, action, result = bug_step10_fixtures # OCR fake : on simule que EasyOCR lit dans la ROI le titre de la fenêtre # active après le clic (l'Explorateur de fichiers a pris le focus). def fake_ocr(crop): # On suppose que la ROI 80×80 autour du clic au milieu-bas tombe # sur la zone du texte. Pour le test, on retourne directement le # texte qui ferait foi. return "rpa_vision : Explorateur de fichiers" ocr_click = OcrRoiChecker(ocr_fn=fake_ocr, radius_px=80) # Construit le même Validator que api_stream._get_validator_v2() validator = Validator(checkers={"click": [ocr_click]}) vr = validator.validate( action=action, result=result, screenshot_before=before, screenshot_after=after, context={}, ) # Verdict attendu : TERMINATE / WRONG_APPLICATION (token 'explorateur de fichiers') assert vr.verdict == Verdict.TERMINATE, ( f"Verdict attendu TERMINATE, obtenu {vr.verdict} (reasoning={vr.reasoning})" ) assert vr.failure_category == FailureCategory.WRONG_APPLICATION assert vr.confidence >= 0.85 assert "explorateur" in vr.reasoning.lower() or "explorateur" in vr.raw_evidence.get("roi_text", "").lower() def test_validator_complete_when_correct_window_active(bug_step10_fixtures): """Sanity : si l'OCR voit bien 'Enregistrer' dans la ROI, le verdict est COMPLETE.""" from core.validation import OcrRoiChecker, Validator, Verdict before, after_bad, action, result = bug_step10_fixtures after_good = _png_b64(_make_screenshot("Document enregistre - Bloc-notes")) def fake_ocr(crop): return "Bouton Enregistrer cliqué — Bloc-notes" validator = Validator( checkers={"click": [OcrRoiChecker(ocr_fn=fake_ocr, radius_px=80)]}, ) vr = validator.validate( action=action, result=result, screenshot_before=before, screenshot_after=_png_b64(_make_screenshot("après save Bloc-notes")), context={}, ) assert vr.verdict == Verdict.COMPLETE assert vr.failure_category is None