Spaces:
Sleeping
Sleeping
Claude
feat(sprint-S4-batch2-4): coverage des vues HTML, adapters VLM, corpus_service, job_runner
756cdab unverified | """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 | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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] | |
| def name(self) -> str: | |
| return "bad" | |