"""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 L’ENREGISTREMENT") 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"