Files
rpa_vision_v3/tests/unit/test_dialog_resolver.py
Dom 84d2d4a667 feat(dialog): R2 MVP P0 — DialogResolver + catalogue 10 entrées (flag OFF default)
- agent_v0/server_v1/core/dialog/ : catalogue compact + DialogResolver
  stateless (match titre + evidence, trichotomie stricte auto/pause/skip).
- 10 entrées P0 : confirm-save-overwrite, notepad-unsaved-changes,
  windows-file-explorer (fallback replay 4c38dbb8), easily-save/overwrite/
  confirm-action/clinical-warning, windows-uac, windows-hello-credui,
  edge-update.
- Validateur déclaratif `system_modals_cannot_be_overridden` : rejette
  toute surcharge auto/skip sur modaux SYSTÈME (windows-/defender-).
- Endpoint POST /api/v1/dialog/resolve derrière flag
  RPA_DIALOG_RESOLVER_ENABLED (OFF par défaut → 503). Aucun
  rebranchement côté agent_v1 (executor.py inchangé, P1 plus tard).
- 25 tests pytest passants (19 unit + 6 intégration HTTP).

Spec : docs/recherche/SPEC_POPUPS_CATALOGUE.md §2bis / §3.

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

228 lines
8.0 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.
"""Tests unitaires DialogResolver — R2 MVP P0.
Couvre :
- match par titre seul (catalogue de base).
- match titre + evidence (Bloc-notes : titre ambigu).
- pas de match → policy conservative "pause".
- override déclaratif autorisé pour Easily/non-système.
- validateur sécurité : refuse `auto`/`skip` sur préfixes
`windows-` / `defender-` (modaux SYSTÈME).
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
_ROOT = str(Path(__file__).resolve().parents[2])
if _ROOT not in sys.path:
sys.path.insert(0, _ROOT)
from agent_v0.server_v1.core.dialog import ( # noqa: E402
DeclarativeOverride,
DialogResolver,
KNOWN_DIALOGS,
SystemModalOverrideError,
system_modals_cannot_be_overridden,
)
pytestmark = pytest.mark.unit
class TestCatalog:
"""Le catalogue P0 contient au moins les 10 entrées requises."""
REQUIRED_IDS = {
"confirm-save-overwrite",
"notepad-unsaved-changes",
"windows-file-explorer",
"easily-save-unconfirmed",
"easily-overwrite-file",
"easily-confirm-action",
"easily-clinical-warning",
"windows-uac",
"windows-hello-credui",
"edge-update",
}
def test_catalog_has_p0_entries(self):
assert self.REQUIRED_IDS.issubset(KNOWN_DIALOGS.keys())
def test_system_modals_not_overridable(self):
"""UAC + Hello + file-explorer system : `declarative_override` False."""
for modal_id in ("windows-uac", "windows-hello-credui"):
assert KNOWN_DIALOGS[modal_id].declarative_override is False
assert KNOWN_DIALOGS[modal_id].policy == "pause"
def test_clinical_warning_not_overridable(self):
"""Clinical warning : décision médicale, jamais auto."""
spec = KNOWN_DIALOGS["easily-clinical-warning"]
assert spec.policy == "pause"
assert spec.declarative_override is False
class TestResolveByTitleOnly:
"""Match sur titre seul, sans evidence."""
def setup_method(self):
self.resolver = DialogResolver()
def test_confirm_save_overwrite_fr(self):
res = self.resolver.resolve("Confirmer l'enregistrement")
assert res.matched
assert res.dialog_id == "confirm-save-overwrite"
assert res.policy == "auto"
assert res.action is not None
assert res.action["button_label"] == "Oui"
def test_confirm_save_overwrite_en(self):
res = self.resolver.resolve("Confirm Save As")
assert res.matched
assert res.dialog_id == "confirm-save-overwrite"
def test_windows_uac_pause(self):
res = self.resolver.resolve(
"Contrôle de compte d'utilisateur",
evidence_texts=["Voulez-vous autoriser cette application"],
)
assert res.matched
assert res.dialog_id == "windows-uac"
assert res.policy == "pause"
assert res.action is None
def test_windows_file_explorer_fallback_pause(self):
"""Cas replay 4c38dbb8 — fallback file-explorer → pause humaine."""
res = self.resolver.resolve("rpa_vision : Explorateur de fichiers")
assert res.matched
assert res.dialog_id == "windows-file-explorer"
assert res.policy == "pause"
def test_no_match_returns_pause(self):
res = self.resolver.resolve("Fenêtre métier inconnue 42")
assert res.matched is False
assert res.dialog_id == ""
assert res.policy == "pause"
class TestResolveByTitleAndEvidence:
"""Match qui exige une evidence en plus du titre (Bloc-notes)."""
def setup_method(self):
self.resolver = DialogResolver()
def test_notepad_unsaved_changes_with_evidence(self):
res = self.resolver.resolve(
"Bloc-notes",
evidence_texts=[
"Voulez-vous enregistrer les modifications ?",
"Ne pas enregistrer",
],
)
assert res.matched
assert res.dialog_id == "notepad-unsaved-changes"
assert res.policy == "auto"
assert res.action["button_label"] == "Enregistrer"
def test_notepad_without_evidence_does_not_match(self):
"""Titre `Bloc-notes` seul est trop générique : pas de match."""
res = self.resolver.resolve("test.txt - Bloc-notes")
# Pas d'evidence → la spec evidence-required ne matche pas → pas matched.
assert res.matched is False
assert res.policy == "pause"
def test_case_insensitive_and_accent_resilient(self):
"""Normalisation : casse + apostrophes typographiques tolérées."""
res = self.resolver.resolve("CONFIRMER LENREGISTREMENT")
assert res.matched
assert res.dialog_id == "confirm-save-overwrite"
class TestSecurityValidator:
"""Validateur déclaratif : interdit auto/skip sur SYSTÈME."""
def test_uac_auto_override_raises(self):
override = DeclarativeOverride(dialog_id="windows-uac", policy="auto")
with pytest.raises(SystemModalOverrideError):
system_modals_cannot_be_overridden(override)
def test_uac_skip_override_raises(self):
override = DeclarativeOverride(dialog_id="windows-uac", policy="skip")
with pytest.raises(SystemModalOverrideError):
system_modals_cannot_be_overridden(override)
def test_defender_auto_override_raises(self):
override = DeclarativeOverride(
dialog_id="defender-smartscreen-app", policy="auto",
)
with pytest.raises(SystemModalOverrideError):
system_modals_cannot_be_overridden(override)
def test_uac_pause_override_passes(self):
"""`pause` reste valide même sur SYSTÈME (no-op)."""
override = DeclarativeOverride(dialog_id="windows-uac", policy="pause")
assert system_modals_cannot_be_overridden(override) is override
def test_easily_auto_override_passes(self):
"""Easily n'est pas SYSTÈME → surcharge auto autorisée par le validateur."""
override = DeclarativeOverride(
dialog_id="easily-save-unconfirmed",
policy="auto",
button_label="Enregistrer",
)
assert system_modals_cannot_be_overridden(override) is override
class TestDeclarativeOverrideAtResolve:
"""Le DialogResolver applique l'override quand la spec l'autorise."""
def setup_method(self):
self.resolver = DialogResolver()
def test_override_applied_on_overridable_spec(self):
"""edge-update : default `skip`, surchargeable en `auto`."""
override = DeclarativeOverride(
dialog_id="edge-update",
policy="auto",
button_label="Plus tard",
)
res = self.resolver.resolve(
"Microsoft Edge",
evidence_texts=["Microsoft Edge a été mis à jour"],
declarative_override=override,
)
assert res.matched
assert res.policy == "auto"
assert res.action is not None
assert res.action["button_label"] == "Plus tard"
def test_override_rejected_at_resolve_for_system_modal(self):
"""Defense in depth : resolve() refuse aussi auto sur SYSTÈME."""
override = DeclarativeOverride(dialog_id="windows-uac", policy="auto")
with pytest.raises(SystemModalOverrideError):
self.resolver.resolve(
"Contrôle de compte d'utilisateur",
evidence_texts=["Voulez-vous autoriser cette application"],
declarative_override=override,
)
def test_override_ignored_on_non_overridable_clinical_warning(self):
"""Clinical warning : surcharge ignorée silencieusement (declarative_override=False)."""
override = DeclarativeOverride(
dialog_id="easily-clinical-warning",
policy="auto",
button_label="OK",
)
res = self.resolver.resolve(
"Avertissement clinique",
evidence_texts=["Allergie connue"],
declarative_override=override,
)
assert res.matched
# La spec déclare declarative_override=False → policy reste pause.
assert res.policy == "pause"