merge(R2): DialogResolver MVP P0 (worktree a86565d0)

This commit is contained in:
Dom
2026-05-24 17:53:35 +02:00
7 changed files with 995 additions and 0 deletions

View File

@@ -0,0 +1,227 @@
"""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"