Diagnostic post-bench E2E (rapport docs/E2E_TEST_RUN_2026-05-08.md) :
1. BUG SILENCIEUX MAJEUR (api_stream.py:4549) — quand le pré-check OCR
rejette, mon code de rejet hier soir met x_pct=None / y_pct=None.
Le log structuré faisait result.get('x_pct', 0):.4f → None:.4f →
TypeError → réponse "analysis_error" qui MASQUE le vrai motif
"rejected_text_mismatch". Conséquence : pendant toute la session
du 7 mai soir, les rejets pré-check ont été silencieusement
transformés en erreurs analyse → cascade locale Léa V1 → clic au pif.
Fix : `(result.get('x_pct') or 0):.4f` traite None | None | 0
uniformément.
2. FLAG ENV pré-check OFF par défaut — le pré-check
_validate_text_at_position introduit hier soir a 2 défauts
identifiés par le bench E2E sur 8 click_anchor :
* radius_px=200 trop petit pour les tabs à 2 tokens (Examens
cliniques, Synthèse Urgences) — OCR voit un crop tronqué
"Maquette POC ler en cours Codage Statistiques" qui n'inclut
pas "Examens" → fuzzy match 1/2 = 50% < seuil 0.60 → REJET.
À radius 300/400 le mot est inclus → match passe.
* min_token_ratio=0.60 trop strict pour cibles 2 tokens.
Solution démo : flag env RPA_ENABLE_TEXT_PRECHECK (défaut "false").
Le pré-check est désactivé par défaut → retour au comportement
stable d'avant-hier (hybrid_text_direct ≥ 0.80 utilisé direct,
exemption drift préservée). Code et fonction _validate_text_at_position
conservés en place pour reprise post-démo après calibrage radius
adaptatif (≈ 0.17 × min(screen_w, screen_h)) et token_ratio descendu
à 0.50.
Pour ré-activer en dev/test : `RPA_ENABLE_TEXT_PRECHECK=true`
dans .env.local ou env du service rpa-streaming.
Inclus aussi :
- docs/E2E_TEST_RUN_2026-05-08.md (rapport agent test E2E ~1700 mots)
- tests/e2e/urgence_aiva_demo_expected.yaml (tolérances re-écrites)
- tests/e2e/fixtures/urgence_aiva_demo/live/*.png (8 fixtures
recapturées headless 1920x1080 pour itérer demain)
- _ocr_inventory.json + _run_resolve_results.json (raw runs)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
Run E2E Urgence_aiva_demo — 8 mai 2026
Audit ingénieur test/automation senior réalisé en J0 démo GHT Sud 95.
Objectif : exécuter
tools/test_replay_e2e.pysur des fixtures pertinentes (vrais screens de la maquette Easily Assure contenant les textes cibles), comparer les résolutions step-par-step à la baseline attendue, identifier les régressions concrètes introduites par les patches serveur du 7 mai soir (cureb584bbabc+ pré-check OCR + exemption hybrid_text), et proposer des correctifs précis.AUCUN code serveur n'a été modifié. Lecture + harness + rapport uniquement.
TL;DR (synthèse pour décision avant démo)
- Cascade fonctionnelle sur 5/6 cibles testables (
hybrid_text_directrésout25003284,Imagerie,Notes médicales,Codage,Coller dossier patientlorsque la fixture représente le bon écran). - Régression confirmée : pour
Examens cliniquesetSynthèse Urgences(deux tabs en haut d'écran), le pre-check OCR àradius_px=200voit un crop trop étroit pour capter le mot cible → REJET → exception non rattrapée dans le log → réponse fallbackanalysis_error. Touche au minimum 2 steps sur 6 démo. - 2 correctifs chirurgicaux proposés (radius proportionnel à la résolution écran, garde NoneType sur le format string). Effort ~10 lignes, risque très faible. Détails §5.
- Pour la démo dans la journée : 2 chemins sont défendables —
(A) appliquer les correctifs (10 minutes, faible risque, valider en
retesting harness), ou (B) ne rien toucher et compter sur la
policy serveur qui transformera l'
analysis_erroren pause supervisée + Plan B (fallback recorded coords). Recommandation : (A) si possible, sinon (B) avec briefing préalable.
1. Inventaire fixtures
1.1 Diagnostic des heartbeats sur disque
Premier réflexe : utiliser les heartbeat_*.png du PC Windows.
Échec total — toutes les fixtures inspectées (300+ heartbeats des
bg_DESKTOP-58D5CAC_windows depuis mars 2026, sessions sess_* du 5
mai) montrent l'explorateur Windows ou Chrome lambda, pas la
maquette Easily Assure. Le workflow Urgence_aiva_demo a été
construit le 7 mai 2026 — il n'existe pas de heartbeat capturé
durant un usage réel de cette maquette.
Inventaire OCR (EasyOCR fr+en) sur 30 heartbeats stratifiés :
0 fixture ne contient un texte cible. Voir
tests/e2e/fixtures/urgence_aiva_demo/_ocr_inventory.json et
_ocr_inventory_may5.json.
1.2 Solution adoptée — fixtures live
Capture headless Chrome en 1920×1080 et 2560×1600 directement contre
la maquette en ligne (https://urgence.labs.laurinebazin.design,
auth basic lea:Medecin2026!), une fixture par écran d'intérêt :
| Step | by_text | Fixture (1920×1080) | OCR cible présent ? |
|---|---|---|---|
| 3 | 25003284 |
live/landing.png |
OK |
| 8 | Examens cliniques |
live/dossier_motif.png |
OK |
| 10 | Imagerie |
live/dossier_examens-cliniques.png |
OK |
| 12 | Notes médicales |
live/dossier_imagerie.png |
OK |
| 14 | Synthèse Urgences |
live/dossier_notes-medicales.png |
OK |
| 17 | Codage |
live/dossier_synthese-urgences.png |
OK |
| 18 | Coller ou saisir le dossier patient |
live/dossier_codage.png (proxy) |
NON (page aiva-vision absente) |
| 20 | Justification de la décision |
live/dossier_codage.png (proxy) |
NON |
Limitations connues : la maquette ne route pas correctement les hash
URL (#examens-cliniques, #imagerie, ...) — tous les onglets
renvoient le même HTML. L'OCR confirme néanmoins que les 6 onglets
sont visibles dans le bandeau, ce qui suffit pour valider la
résolution by_text sur ces tabs. Les steps 18 et 20 ciblent la
page aiva-vision (en aval du clic sur "Codage >"), non capturée
ici — voir §6.
1.3 Anchor images comme fixtures alternatives
J'ai aussi téléchargé les 8 images d'ancres depuis VWB
(/api/v3/anchor/<id>/image) sous
tests/e2e/fixtures/urgence_aiva_demo/step*.png (2560×1600).
Elles contiennent toutes leur by_text mais sont des crops
zoomés (la position est non-représentative). Elles servent à valider
qu'hybrid_text_direct fonctionne (étape 0.5) mais leur drift par
rapport aux coords enregistrées est artefactuel — voir un précédent
run dans _run_resolve_results.json.
2. Run du harness
2.1 Méthode
Plutôt que tools/test_replay_e2e.py qui force le replay du
workflow complet (et bouclerait à cause des extract_text serveur,
pause_for_human, etc.), j'ai utilisé un appel direct ciblé à
/api/v1/traces/stream/replay/resolve_target avec, pour chaque
step click_anchor :
screenshot_b64= la fixture du steptarget_spec= exactement ce que VWB compose (by_text,by_text_source: "ocr",anchor_image_base64,anchor_id,bounding_box,screen_resolution)fallback_x_pct/_y_pct= centre normalisé de la bbox de l'ancre (= les coords enregistrées)strict_mode = True(replay sessions)
Script : /tmp/run_resolve_per_step.py (non versionné).
ATTENTION REPRO : la clé est
anchor_image_base64, pasanchor_image_b64. Sans cette clé, le serveur tombe en mode non-strict (has_anchor=False), saute l'étape 0.5hybrid_text_directet tape direct VLM puis ScreenAnalyzer (qui retournescreen_analyzer_unavailable). Premier run totalement faux à cause de cette typo — corrigé.
2.2 Résultats sur fixtures live (1920×1080)
| # | by_text | resolved | méthode | score | pos résolue | recorded | reason | ms |
|---|---|---|---|---|---|---|---|---|
| 1 | 25003284 |
True | hybrid_text_direct |
1.000 | (0.0303, 0.1988) | (0.4928, 0.4512) | drift IGNORÉ (exemption) | 1543 |
| 2 | Examens cliniques |
False | fallback |
0.000 | (0.4980, 0.4928) | (0.498, 0.4928) | analysis_error |
1420 |
| 3 | Imagerie |
True | hybrid_text_direct |
0.800 | (0.2256, 0.1267) | (0.498, 0.4928) | drift IGNORÉ | 1372 |
| 4 | Notes médicales |
True | hybrid_text_direct |
0.800 | (0.2227, 0.1259) | (0.202, 0.28) | drift OK | 976 |
| 5 | Synthèse Urgences |
False | fallback |
0.000 | (0.2705, 0.2794) | (0.2705, 0.2794) | analysis_error |
1341 |
| 6 | Codage |
True | hybrid_text_direct |
0.800 | (0.1392, 0.0538) | (0.3189, 0.2281) | drift IGNORÉ | 1253 |
| 7 | Coller ou saisir le dossier patient |
False | strict_vlm_template_failed |
0.000 | (0.0748, 0.4412) | - | vlm_and_template_all_failed (fixture invalide — page absente) |
4233 |
| 8 | Justification de la décision |
False | strict_vlm_template_failed |
0.000 | (0.6482, 0.6228) | - | idem | 3586 |
Score final côté cascade : 5 OK / 2 FAIL régression / 1 FAIL attendu (fixture mauvaise page) sur 8 quand on n'évalue que les steps avec fixture représentative. Régression brute = 2/6 = 33 % d'échecs sur les onglets démo.
3. Divergences vs baseline
3.1 Bug #1 — Pre-check OCR rejette à tort sur Examens cliniques et Synthèse Urgences (radius trop petit)
Logs serveur (steps 8 et 14) :
Pre-check OCR REJET : 'Examens cliniques' attendu @ (0.2256, 0.1267) via hybrid_text_direct
mais OCR voit 'Maquette POC ler en cours Codage Statistiques Catherine Néle)le 14/03/1947 77 an' (80ms)
Reproduction isolée via _validate_text_at_position (script de
test inline) — sensibilité au radius :
| Cible | r=100 | r=150 | r=200 (actuel) | r=250 | r=300 | r=400 |
|---|---|---|---|---|---|---|
| Examens cliniques | 0/2 | 0/2 | 1/2 (50 %) | 2/2 OK | 2/2 OK | 2/2 OK |
| Synthèse Urgences | 0/2 | 0/2 | 0/2 (0 %) | 1/2 | 2/2 OK | 2/2 OK |
| Notes médicales | 1/2 | 2/2 OK | 2/2 OK | OK | OK | OK |
| Imagerie | 1/1 OK | 1/1 OK | 1/1 OK | OK | OK | OK |
Sur 2560×1600 (resolution Windows réelle de Dom), même phénomène
mais déplacé : Examens cliniques reste FAIL jusqu'à r=400 (le tab
"Examens cliniques" est physiquement plus large en pixels qu'à
1920×1080).
Cause profonde : radius_px=200 est fixé en pixels absolus
(resolve_engine.py:2246), or les éléments UI (largeur d'un tab)
varient avec la résolution. Pour des cibles courtes (1 token,
type "Imagerie") c'est OK ; pour des cibles à 2 tokens (Examens cliniques, Synthèse Urgences) sur des bandeaux d'onglets à mi-écran
en haut, le crop tronque.
Aggravant : le seuil fuzzy à 0.60 exige 100 % des tokens pour les
cibles à 2 tokens (60 % de 2 = 1.2 → arrondi sup → 2/2). Si OCR
rate un token sur deux, REJET sec.
3.2 Bug #2 — Crash log RESOLVE_EXIT sur résultat None
Quand le pre-check rejette, result est remplacé par
(api_stream.py:4534-4542) :
result = {
"resolved": False,
"method": "rejected_text_mismatch",
"reason": ...,
"x_pct": None,
"y_pct": None,
}
Puis le log (api_stream.py:4549) :
f"coords=({result.get('x_pct', 0):.4f}, {result.get('y_pct', 0):.4f}) "
→ result.get('x_pct', 0) retourne None (la clé EXISTE et vaut
None — la valeur par défaut 0 n'est utilisée que si la clé est
absente). None:.4f lève TypeError: unsupported format string passed to NoneType.__format__.
Conséquence : exception remontée → _fallback_response("analysis_error", str(e)) retourné côté client → la cascade côté replay_engine.py
voit resolved=False, reason="analysis_error" au lieu de
reason="rejected_text_mismatch". La couche supérieure ne peut donc
plus traiter le rejet sémantique pour ce qu'il est — elle voit une
erreur d'analyse système.
Cumul des deux bugs : le pre-check OCR fait perdre le clic en cascade, là où il aurait dû seulement rejeter ce candidat et laisser la cascade continuer (VLM, SoM, template).
3.3 Drift exemption — fonctionne correctement
L'exemption hybrid_text_direct ≥ 0.80 fonctionne nominalement : 4 résolutions sur 5 ont un drift > 0.20 mais sont acceptées. Logs :
Drift (0.463, 0.252) > 0.20 IGNORÉ : score=1.000 sur hybrid_text_direct — résultat visuel fiable, on l'utilise
Aucun cas observé où l'exemption ait fait passer un faux positif
visible. Sur les fixtures testées, l'OCR direct trouve toujours
le bon texte exact (score 1.0) ou le bon avec OCR un peu bruité
(0.8). À surveiller en démo réelle si plusieurs occurrences du même
texte coexistent sur l'écran (ex : tableau patients avec plusieurs
IPP commençant par "2500..." — risque que 25003284 soit confondu
avec un voisin lexical).
4. Reproduction en isolation
cd /home/dom/ai/rpa_vision_v3 && source .venv/bin/activate
# Fixtures live (à recapturer à chaque démo si la maquette change)
mkdir -p tests/e2e/fixtures/urgence_aiva_demo/live
google-chrome --headless --disable-gpu --no-sandbox --window-size=1920,1080 \
--user-data-dir=/tmp/chrome_e2e \
--screenshot=tests/e2e/fixtures/urgence_aiva_demo/live/dossier_motif.png \
'https://lea:Medecin2026!@urgence.labs.laurinebazin.design/dossier.html?id=25003284'
# Test ciblé d'un step (exemple : step 8 Examens cliniques)
python3 - <<'PY'
import sys; sys.path.insert(0, '.')
from agent_v0.server_v1.resolve_engine import _validate_text_at_position
from PIL import Image
fp = 'tests/e2e/fixtures/urgence_aiva_demo/live/dossier_motif.png'
sw, sh = Image.open(fp).size
for r in (200, 250, 300, 350):
ok, obs, ms = _validate_text_at_position(fp, 0.2256, 0.1267, 'Examens cliniques', sw, sh, radius_px=r)
print(f'r={r} → valid={ok} ({ms:.0f}ms) obs={obs[:80]!r}')
PY
5. Correctifs proposés (NON appliqués)
Correctif #1 — Radius proportionnel à la résolution + fuzzy 0.50
Fichier : agent_v0/server_v1/resolve_engine.py
Avant (ligne 2246) :
def _validate_text_at_position(
screenshot_path: str,
x_pct: float,
y_pct: float,
expected_text: str,
screen_width: int,
screen_height: int,
radius_px: int = 200,
) -> tuple:
Après :
def _validate_text_at_position(
screenshot_path: str,
x_pct: float,
y_pct: float,
expected_text: str,
screen_width: int,
screen_height: int,
radius_px: Optional[int] = None,
) -> tuple:
# Radius proportionnel à la dimension écran la plus petite (≈ 17 % d'écran).
# Sur 1920×1080 → 184 px ; sur 2560×1600 → 272 px ; sur 3840×2160 → 367 px.
# Couvre les bandeaux d'onglets type Easily Assure tout en restant
# localement sémantique (pas la moitié d'écran).
if radius_px is None:
radius_px = int(0.17 * min(screen_width, screen_height))
Effet attendu sur la run : Examens cliniques à r=204 (au lieu de
200) reste tronqué côté droit ; à r=272 sur 2560×1600 c'est OK.
Combiné avec le correctif fuzzy ↓ ça passe.
Avant (ligne 2285) :
is_valid = _text_match_fuzzy(expected_text, observed, min_token_ratio=0.60)
Après :
is_valid = _text_match_fuzzy(expected_text, observed, min_token_ratio=0.50)
Justification : pour cibles à 2 tokens (Examens cliniques, Synthèse Urgences, Notes médicales), 0.60 force 2/2 (= exact). 0.50 autorise
1/2 — suffisant pour valider que le bon zone OCR est probable, sans
sacrifier la spécificité (un token rare comme "synthèse" ou "examens"
suffit). Pour cibles à 4+ tokens (Coller ou saisir le dossier patient), 0.50 demande 2/4 — cohérent avec le commentaire historique
de la fonction.
Risque : faux positif rare où un mot d'une cible apparaît dans une zone sans rapport. Mitigé par le fait que :
- Le pre-check est appelé sur la zone où la cascade a déjà résolu (donc visuellement fortement filtrée).
- Le seuil de score amont (
hybrid_text_direct ≥ 0.80) garantit déjà que le mot exact a été identifié.
Steps impactés : 8 (Examens cliniques), 14 (Synthèse Urgences) → résolution OK au lieu d'échec.
Correctif #2 — Garde NoneType sur le format string
Fichier : agent_v0/server_v1/api_stream.py
Avant (ligne 4549) :
f"coords=({result.get('x_pct', 0):.4f}, {result.get('y_pct', 0):.4f}) "
Après :
f"coords=({(result.get('x_pct') or 0):.4f}, {(result.get('y_pct') or 0):.4f}) "
Ou plus explicite et défensif pour les autres champs :
_x = result.get('x_pct') if result else None
_y = result.get('y_pct') if result else None
logger.info(
f"[REPLAY] RESOLVE_EXIT session={request.session_id} "
f"resolved={(result or {}).get('resolved', False)} "
f"method='{(result or {}).get('method', 'none')}' "
f"coords=({(_x if _x is not None else 0):.4f}, {(_y if _y is not None else 0):.4f}) "
f"score={(result or {}).get('score', 0)} "
f"from_memory={bool((result or {}).get('from_memory', False))} "
f"reason='{(result or {}).get('reason', '')}'"
)
Effet attendu : pas d'exception. Le client reçoit le vrai
{resolved: False, method: 'rejected_text_mismatch', reason: ...}
au lieu d'un masque analysis_error. La couche supérieure peut
décider de retenter avec une autre méthode plutôt que de partir en
pause supervisée.
Risque : nul. Pure défensive contre None. Effort < 5 lignes.
Correctif #3 (optionnel, à n'appliquer qu'après #1+#2) — Fallback de résolution post-rejet
Quand le pre-check rejette, ne pas tomber direct en resolved: False.
Continuer la cascade (VLM Quick Find, SoM) qui peut lever l'ambiguïté.
Idée : dans api_stream.py après le bloc pre-check (ligne ~4543),
si result.method == "rejected_text_mismatch", ré-appeler
_resolve_target_sync avec target_spec["__skip_ocr_direct"] = True
pour forcer VLM/SoM. Trop intrusif pour le jour-J — à reporter.
6. Limitations & angles morts
-
Steps 18 et 20 non couverts : la fixture utilisée (
dossier_codage.png) ne contient pas la page aiva-vision (textarea + bouton Justification). Pour les tester, il faudrait :- soit cliquer "Codage >" et capturer la page aval (scriptable avec puppeteer/playwright, ~1h),
- soit simuler un replay réel sur la maquette en démo et enregistrer les heartbeats au passage. À planifier post-démo.
-
Fixtures statiques : la maquette peut évoluer (Laurine peut modifier le CSS/HTML à tout moment). Re-capturer
tests/e2e/fixtures/urgence_aiva_demo/live/*.pngavant chaque démo majeure. -
Pas de test de la cascade VLM/SoM : tous les steps testés ici ont passé sur
hybrid_text_direct(étape 0.5). La cascade VLM, SoM, template_matching et ScreenAnalyzer n'a pas été stressée. Le serveur a montré qu'elle est invocable (steps 7-8 sont allés jusqu'au bout dustrict_vlm_template_failed). Mais le timing exact, les seuils, la cohérence des coords sur ces chemins alternatifs — non couverts ici. Idéal : ajouter une fixture délibérément sans le texte cible (juste l'icône) pour forcer template_matching, et mesurer le score. -
Drift exemption pas testée en mode adversarial : aucun cas où l'exemption a fait passer un mauvais clic. Sur la maquette d'Easily Assure, les textes cibles sont uniques. Sur un DPI réel (ex : 8 patients avec des IPP qui commencent par "2500..."), il faut vérifier que
hybrid_text_directretourne le bon match et pas le premier rencontré. À tester en démo. -
Ce rapport est INCOMPLET sur le point demandé "appel direct à
_resolve_by_ocr_textpuis_validate_text_at_positionavec paramètres variés" : fait pour la validation seulement. Le vrai test paramétrique de_resolve_by_ocr_text(variations de seuil fuzzy interne, normalisation, langue OCR) reste à faire — peu prioritaire car les scores actuels (0.8–1.0) sont sains.
7. YAML attendu mis à jour
Voir tests/e2e/urgence_aiva_demo_expected.yaml (re-écrit ce jour)
— format basé sur tolérances (range x_pct, y_pct, score_min) plutôt
que coordonnées rigides, pour ne pas casser à chaque ré-OCR.
8. Prochaines actions recommandées
- Maintenant (avant démo si fenêtre disponible) : appliquer les correctifs #1 et #2. Re-tester le harness. Risque très faible.
- Pendant démo : si Synthèse Urgences ou Examens cliniques
échouent, c'est l'
analysis_error— Plan B (recorded coords ou pause supervisée) prend le relais. Briefing à Amina sur ce point. - Post-démo : capturer un replay réel complet, sauvegarder les
heartbeats, alimenter
tests/e2e/fixtures/urgence_aiva_demo/pour avoir des fixtures dossier+aiva-vision authentiques. Valider_run_resolve_results.jsoncomme baseline non-régressive. - Plus tard : intégrer le harness dans
pytestavec marqueur@pytest.mark.e2e(fixture par YAML, comparaison avec tolérances). 1h d'effort.
Auteur : Claude (agent test/automation senior). Aucune modification de code ; rapport seul. Reproductions : voir §4. Fichiers livrés :
docs/E2E_TEST_RUN_2026-05-08.md(ce rapport)tests/e2e/urgence_aiva_demo_expected.yaml(YAML attendus mis à jour)tests/e2e/fixtures/urgence_aiva_demo/live/*.png(fixtures recapturées de la maquette en ligne)tests/e2e/fixtures/urgence_aiva_demo/_run_resolve_results.json(dernier run brut)