Picarones / tests /adapters /vlm /test_s4_vlm_adapters.py
Claude
feat(sprint-S4-batch2-4): coverage des vues HTML, adapters VLM, corpus_service, job_runner
756cdab unverified
Raw
History Blame
6.44 kB
"""Sprint S4.8 โ€” couverture des 4 adapters VLM.
Avant S4 : ``adapters/vlm/{anthropic,mistral,ollama,openai}_vlm.py``
ร  0% direct (testรฉs transitivement).
Cible : 80%+ โ€” vรฉrifie le contrat MRO + ``input_types`` /
``output_types`` + ``name`` propre ร  chaque adapter, sans appeler
les SDK rรฉels (qui exigeraient des clรฉs API et du rรฉseau).
"""
from __future__ import annotations
import pytest
from picarones.domain.artifacts import ArtifactType
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# Liste des adapters ร  tester avec leur identifiant attendu
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
_VLM_CASES = [
("anthropic_vlm", "picarones.adapters.vlm.anthropic_vlm",
"AnthropicVLMAdapter"),
("mistral_vlm", "picarones.adapters.vlm.mistral_vlm",
"MistralVLMAdapter"),
("ollama_vlm", "picarones.adapters.vlm.ollama_vlm",
"OllamaVLMAdapter"),
("openai_vlm", "picarones.adapters.vlm.openai_vlm",
"OpenAIVLMAdapter"),
]
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 1. Contrat de base : input/output types, name, MRO
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@pytest.mark.parametrize(
"expected_name,module_path,class_name", _VLM_CASES,
)
class TestVLMAdapterContract:
def test_input_types_is_image(
self, expected_name: str, module_path: str, class_name: str,
) -> None:
import importlib
module = importlib.import_module(module_path)
adapter_cls = getattr(module, class_name)
adapter = adapter_cls(model="any-model", config={})
assert ArtifactType.IMAGE in adapter.input_types
def test_output_types_is_raw_text(
self, expected_name: str, module_path: str, class_name: str,
) -> None:
import importlib
module = importlib.import_module(module_path)
adapter_cls = getattr(module, class_name)
adapter = adapter_cls(model="any-model", config={})
assert ArtifactType.RAW_TEXT in adapter.output_types
def test_name_is_distinct_per_adapter(
self, expected_name: str, module_path: str, class_name: str,
) -> None:
import importlib
module = importlib.import_module(module_path)
adapter_cls = getattr(module, class_name)
adapter = adapter_cls(model="any-model", config={})
assert adapter.name == expected_name
def test_mro_baseVLMAdapter_first(
self, expected_name: str, module_path: str, class_name: str,
) -> None:
"""Le garde-fou ``__init_subclass__`` exige
``BaseVLMAdapter`` AVANT le LLM sibling dans le MRO. On
vรฉrifie qu'une instance correctement dรฉfinie a bien
``BaseVLMAdapter`` parmi ses ancรชtres et que ``input_types``
vient bien de lui (et pas du LLM)."""
import importlib
from picarones.adapters.vlm.base import BaseVLMAdapter
module = importlib.import_module(module_path)
adapter_cls = getattr(module, class_name)
assert issubclass(adapter_cls, BaseVLMAdapter)
# MRO : BaseVLMAdapter doit venir avant BaseLLMAdapter
# (ร  travers la chaรฎne d'hรฉritage, on vรฉrifie indirectement
# que ``input_types`` est l'IMAGE ; dรฉjร  testรฉ plus haut).
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 2. Transcription prompt configurable
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class TestTranscriptionPromptConfigurable:
def test_custom_prompt_via_config(self) -> None:
from picarones.adapters.vlm.openai_vlm import OpenAIVLMAdapter
adapter = OpenAIVLMAdapter(
model="gpt-4o",
config={"transcription_prompt": "Custom prompt for testing."},
)
# Doit pouvoir instancier sans erreur ; le prompt est consommรฉ
# par ``execute``.
assert adapter.name == "openai_vlm"
def test_default_prompt_used_when_none_provided(self) -> None:
from picarones.adapters.vlm.openai_vlm import OpenAIVLMAdapter
adapter = OpenAIVLMAdapter(model="gpt-4o", config={})
# Pas de plantage ร  l'init โ€” le dรฉfaut est utilisรฉ.
assert adapter is not None
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
# 3. MRO guard โ€” ordre incorrect โ†’ TypeError
# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class TestMROGuardRaisesOnSwap:
"""Le garde-fou ``__init_subclass__`` doit lever ``TypeError``
quand on dรฉclare le LLM sibling AVANT ``BaseVLMAdapter``.
Reproduction du bug que le garde protรจge : si l'ordre est
inversรฉ, ``input_types`` viendrait du LLM (= RAW_TEXT) au
lieu de IMAGE, et le pipeline silencieusement passerait du
texte au VLM."""
def test_swapped_parents_raises_typeerror(self) -> None:
from picarones.adapters.llm.openai_adapter import OpenAIAdapter
from picarones.adapters.vlm.base import BaseVLMAdapter
with pytest.raises(TypeError):
# Ordre INVERSE โ€” BaseVLMAdapter en deuxiรจme.
class _BadVLM(OpenAIAdapter, BaseVLMAdapter): # type: ignore[misc]
@property
def name(self) -> str:
return "bad"