File size: 6,442 Bytes
756cdab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""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"