"""Tests fail-close P0-1 : le moteur ne doit JAMAIS être invoqué si CamemBERT-bio est indisponible et que use_local_ner=True. Garantie de sécurité : EngineUnavailableError est levée AVANT l'appel du moteur, ce qui empêche la production d'un document potentiellement non anonymisé. """ from pathlib import Path import pytest from gui_v6.engine_bridge import ( EngineSettings, EngineUnavailableError, NerManagers, make_process_fn, ) def _managers_with_broken_camembert(settings): def boom(): raise RuntimeError("model.onnx absent") return NerManagers( settings, factories={"camembert": boom, "eds": boom, "gliner": boom}, caps_provider=lambda: {}, ) def test_process_fn_raises_when_mandatory_ner_unavailable(): settings = EngineSettings(use_local_ner=True) managers = _managers_with_broken_camembert(settings) called = {"engine": False} def fake_engine(*a, **k): called["engine"] = True return {"pdf": "x"} fn = make_process_fn(settings, managers=managers, engine=fake_engine) with pytest.raises(EngineUnavailableError): fn(Path("doc.pdf"), Path("/tmp/out")) assert called["engine"] is False def test_process_fn_runs_when_ner_ok(): settings = EngineSettings(use_local_ner=True) managers = NerManagers( settings, factories={"camembert": lambda: object(), "eds": lambda: None, "gliner": lambda: None}, caps_provider=lambda: {}, ) fn = make_process_fn(settings, managers=managers, engine=lambda *a, **k: {"pdf": "ok"}) assert fn(Path("d.pdf"), Path("/tmp/out")) == {"pdf": "ok"} def test_process_fn_skips_ner_guard_when_local_ner_disabled(): # use_local_ner=False : on ne charge pas le NER et on NE bloque PAS, # même si les factories échoueraient (symétrique du garde-fou fail-close). settings = EngineSettings(use_local_ner=False) managers = _managers_with_broken_camembert(settings) fn = make_process_fn(settings, managers=managers, engine=lambda *a, **k: {"pdf": "ok"}) assert fn(Path("d.pdf"), Path("/tmp/out")) == {"pdf": "ok"}