Files
rpa_vision_v3/tests/integration/test_validator_step10.py
Dom 1b4e64960b feat(validator): R1 MVP P0 — OcrRoiChecker + orchestrator (flag OFF default)
Package core/validation/ minimal :
- result.py : Verdict, FailureCategory, ValidationResult
- pixel_diff_checker.py : wrapper de ReplayVerifier.verify_action
- ocr_roi_checker.py : ROI 80px autour du clic, détecte WRONG_APPLICATION
  via SUSPECT_TOKENS (edge/https/explorateur de fichiers/…)
- orchestrator.py : Validator dispatch action_type → checkers + agrégation

Wiring api_stream.py:3646 derrière RPA_VALIDATOR_V2_ENABLED (OFF par défaut).
Si verdict ≠ COMPLETE, override report.success=False et expose failure_category
dans result_entry. Zero régression flag OFF.

Tests :
- tests/unit/test_validator_v2.py : 13 tests (Checkers + Validator + sérialisation)
- tests/integration/test_validator_step10.py : 2 tests reproduisant le bug
  replay_sess_4c38dbb8 / act_raw_6c1432b3 (clic Enregistrer fait basculer
  vers Explorateur de fichiers) — Validator retourne WRONG_APPLICATION

Activation pour test live : RPA_VALIDATOR_V2_ENABLED=true

Cf. docs/recherche/SPEC_VALIDATOR_MATRICE.md, AXE_B2_DEEP_VALIDATOR.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 17:52:06 +02:00

131 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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