Condition du GO-CONDITIONNEL Qwen sur le lot engine capabilities (cb3b767/890edb3/5e5f0bd) : un profil YAML forçant enable_eds/enable_gliner ne doit pas déclencher un chargement voué à l'échec silencieux. NerManagers.ensure_loaded() applique désormais un garde-fou via la sonde engine_capabilities.capabilities_map() (injectable) AVANT toute tentative de load EDS/GLiNER : si le moteur optionnel demandé est indisponible dans le build courant → warning + désactivation forcée dans les réglages runtime. Best-effort (sonde en échec ⇒ réglages inchangés, les try/except de load protègent déjà). Sonde légère (find_spec), aucun import lourd. CamemBERT (requis) inchangé. Diff limité au garde-fou + tests cibles. TDD : 4 tests (test_gui_v6_engine_bridge.py) — eds/gliner indispo désactivés et jamais chargés, moteur dispo conservé, fail-safe sonde. 282 unit passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
240 lines
8.0 KiB
Python
240 lines
8.0 KiB
Python
"""Tests du pont moteur G3-A : kwargs corrects, managers lazy, engine injecté.
|
|
|
|
Aucun vrai manager, aucun modèle, aucun réseau : tout est injecté via factories.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from gui_v6.engine_bridge import (
|
|
EngineSettings,
|
|
ManagerState,
|
|
NerManagers,
|
|
build_engine_kwargs,
|
|
make_process_fn,
|
|
)
|
|
|
|
|
|
class FakeManager:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
|
|
def _counting_factories(counter, fail_camembert=False):
|
|
def camembert():
|
|
counter["camembert"] += 1
|
|
if fail_camembert:
|
|
raise RuntimeError("modèle absent")
|
|
return FakeManager("camembert")
|
|
|
|
def eds():
|
|
counter["eds"] += 1
|
|
return FakeManager("eds")
|
|
|
|
def gliner():
|
|
counter["gliner"] += 1
|
|
return FakeManager("gliner")
|
|
|
|
return {"camembert": camembert, "eds": eds, "gliner": gliner}
|
|
|
|
|
|
# -- kwargs ----------------------------------------------------------------
|
|
|
|
def test_kwargs_defaults_v5_like():
|
|
settings = EngineSettings(config_path=Path("/tmp/cfg.yml"), ogc_label="OCG")
|
|
kwargs = build_engine_kwargs(settings, managers=None)
|
|
assert kwargs["make_vector_redaction"] is False
|
|
assert kwargs["also_make_raster_burn"] is True
|
|
assert kwargs["config_path"] == Path("/tmp/cfg.yml")
|
|
assert kwargs["ogc_label"] == "OCG"
|
|
# Sans managers : pas de NER.
|
|
assert kwargs["use_hf"] is False
|
|
|
|
|
|
def test_kwargs_with_loaded_managers():
|
|
settings = EngineSettings(enable_eds=True, enable_gliner=True)
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
managers = NerManagers(settings, factories=_counting_factories(counter))
|
|
managers.ensure_loaded()
|
|
kwargs = build_engine_kwargs(settings, managers)
|
|
assert kwargs["use_hf"] is True
|
|
assert kwargs["camembert_manager"].name == "camembert"
|
|
assert kwargs["ner_manager"].name == "eds"
|
|
assert kwargs["gliner_manager"].name == "gliner"
|
|
|
|
|
|
def test_kwargs_ner_disabled():
|
|
settings = EngineSettings(use_local_ner=False)
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
managers = NerManagers(settings, factories=_counting_factories(counter))
|
|
managers.ensure_loaded()
|
|
kwargs = build_engine_kwargs(settings, managers)
|
|
assert kwargs["use_hf"] is False
|
|
assert counter["camembert"] == 0 # NER désactivé : rien chargé
|
|
|
|
|
|
# -- lazy loading ----------------------------------------------------------
|
|
|
|
def test_managers_not_loaded_on_init():
|
|
settings = EngineSettings()
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
NerManagers(settings, factories=_counting_factories(counter))
|
|
# Aucune factory appelée à la construction.
|
|
assert counter == {"camembert": 0, "eds": 0, "gliner": 0}
|
|
|
|
|
|
def test_managers_load_once_and_state():
|
|
settings = EngineSettings(enable_eds=True)
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
managers = NerManagers(settings, factories=_counting_factories(counter))
|
|
assert managers.state == ManagerState.NOT_LOADED
|
|
assert managers.ensure_loaded() == ManagerState.READY
|
|
assert managers.ensure_loaded() == ManagerState.READY # idempotent
|
|
assert counter["camembert"] == 1 # chargé une seule fois
|
|
assert counter["eds"] == 1
|
|
assert counter["gliner"] == 0 # non activé
|
|
|
|
|
|
def test_managers_unavailable_when_camembert_fails():
|
|
settings = EngineSettings()
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
managers = NerManagers(settings, factories=_counting_factories(counter, fail_camembert=True))
|
|
assert managers.ensure_loaded() == ManagerState.UNAVAILABLE
|
|
assert managers.use_hf is False
|
|
|
|
|
|
def test_optional_manager_failure_is_tolerated():
|
|
settings = EngineSettings(enable_gliner=True)
|
|
|
|
def factories():
|
|
def camembert():
|
|
return FakeManager("camembert")
|
|
|
|
def gliner():
|
|
raise RuntimeError("gliner ko")
|
|
|
|
def eds():
|
|
return FakeManager("eds")
|
|
|
|
return {"camembert": camembert, "eds": eds, "gliner": gliner}
|
|
|
|
managers = NerManagers(settings, factories=factories())
|
|
assert managers.ensure_loaded() == ManagerState.READY # gliner ko ne bloque pas
|
|
assert managers.use_hf is True
|
|
|
|
|
|
# -- garde-fou capabilities runtime ----------------------------------------
|
|
|
|
|
|
class _FakeCap:
|
|
"""Capability minimale pour injecter une sonde dans les tests."""
|
|
|
|
def __init__(self, available, reason="(test)"):
|
|
self.available = available
|
|
self.reason = reason
|
|
|
|
|
|
def _caps_provider(eds_ok, gliner_ok):
|
|
def provider():
|
|
return {
|
|
"camembert": _FakeCap(True),
|
|
"eds": _FakeCap(eds_ok),
|
|
"gliner": _FakeCap(gliner_ok),
|
|
}
|
|
|
|
return provider
|
|
|
|
|
|
def test_guard_disables_unavailable_eds_before_load():
|
|
# Profil/config forçant EDS alors que le moteur n'est pas embarqué.
|
|
settings = EngineSettings(enable_eds=True)
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
managers = NerManagers(
|
|
settings,
|
|
factories=_counting_factories(counter),
|
|
caps_provider=_caps_provider(eds_ok=False, gliner_ok=True),
|
|
)
|
|
assert managers.ensure_loaded() == ManagerState.READY
|
|
assert settings.enable_eds is False # désactivation forcée
|
|
assert counter["eds"] == 0 # jamais tenté de charger
|
|
assert managers.as_kwargs()["ner_manager"] is None
|
|
|
|
|
|
def test_guard_disables_unavailable_gliner_before_load():
|
|
settings = EngineSettings(enable_gliner=True)
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
managers = NerManagers(
|
|
settings,
|
|
factories=_counting_factories(counter),
|
|
caps_provider=_caps_provider(eds_ok=True, gliner_ok=False),
|
|
)
|
|
assert managers.ensure_loaded() == ManagerState.READY
|
|
assert settings.enable_gliner is False
|
|
assert counter["gliner"] == 0
|
|
assert managers.as_kwargs()["gliner_manager"] is None
|
|
|
|
|
|
def test_guard_keeps_available_engine_enabled():
|
|
settings = EngineSettings(enable_eds=True, enable_gliner=True)
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
managers = NerManagers(
|
|
settings,
|
|
factories=_counting_factories(counter),
|
|
caps_provider=_caps_provider(eds_ok=True, gliner_ok=True),
|
|
)
|
|
assert managers.ensure_loaded() == ManagerState.READY
|
|
assert settings.enable_eds is True
|
|
assert settings.enable_gliner is True
|
|
assert counter["eds"] == 1
|
|
assert counter["gliner"] == 1
|
|
|
|
|
|
def test_guard_failsafe_when_probe_raises():
|
|
settings = EngineSettings(enable_eds=True)
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
|
|
def boom():
|
|
raise RuntimeError("probe ko")
|
|
|
|
managers = NerManagers(
|
|
settings, factories=_counting_factories(counter), caps_provider=boom
|
|
)
|
|
# Best-effort : une sonde en échec ne bloque pas le chargement et ne
|
|
# modifie pas les réglages (les try/except de load protègent déjà).
|
|
assert managers.ensure_loaded() == ManagerState.READY
|
|
assert settings.enable_eds is True
|
|
assert counter["eds"] == 1
|
|
|
|
|
|
# -- make_process_fn -------------------------------------------------------
|
|
|
|
def test_process_fn_calls_engine_with_kwargs(tmp_path):
|
|
settings = EngineSettings()
|
|
counter = {"camembert": 0, "eds": 0, "gliner": 0}
|
|
managers = NerManagers(settings, factories=_counting_factories(counter))
|
|
captured = {}
|
|
|
|
def fake_engine(doc_path, out_dir, **kwargs):
|
|
captured["doc"] = doc_path
|
|
captured["out"] = out_dir
|
|
captured["kwargs"] = kwargs
|
|
return {"status": "ok"}
|
|
|
|
fn = make_process_fn(settings, managers=managers, engine=fake_engine)
|
|
# Avant tout traitement, aucun manager chargé.
|
|
assert counter["camembert"] == 0
|
|
|
|
result = fn(tmp_path / "doc.pdf", tmp_path / "out")
|
|
|
|
assert result == {"status": "ok"}
|
|
assert captured["doc"] == tmp_path / "doc.pdf"
|
|
assert captured["kwargs"]["make_vector_redaction"] is False
|
|
assert captured["kwargs"]["also_make_raster_burn"] is True
|
|
assert captured["kwargs"]["use_hf"] is True
|
|
assert captured["kwargs"]["camembert_manager"].name == "camembert"
|
|
# Le chargement n'a eu lieu qu'à l'appel de process_fn.
|
|
assert counter["camembert"] == 1
|