Spaces:
Running
feat(domain): Sprint A14-S52 — hiérarchie d'erreurs unifiée (fix audit #7 + #11)
Browse filesAvant S52 :
- BaseLLMAdapter.execute levait OCRAdapterError (sémantiquement faux).
- BaseVLMAdapter.execute levait OCRAdapterError (sémantiquement faux).
- JobStoreError héritait de Exception (un caller `except
PicaronesError` ratait silencieusement les erreurs JobStore).
- Pas de racine commune pour catcher 'toute erreur d'adapter'.
Après S52 :
picarones/domain/errors.py
--------------------------
Nouvelle classe AdapterStepError(PicaronesError) — racine commune
des 3 sous-classes d'adapter (OCR/LLM/VLM).
picarones/adapters/ocr/base.py
------------------------------
OCRAdapterError hérite désormais de AdapterStepError au lieu de
PicaronesError directement (pas de breaking change pour les callers
qui catchaient PicaronesError).
picarones/adapters/llm/base.py
------------------------------
- Nouvelle classe LLMAdapterError(AdapterStepError) exportée.
- BaseLLMAdapter.execute lève LLMAdapterError (au lieu de
OCRAdapterError importé depuis ocr.base).
- Suppression de l'import croisé ocr.base → llm.
picarones/adapters/vlm/base.py
------------------------------
- Nouvelle classe VLMAdapterError(AdapterStepError) exportée.
- BaseVLMAdapter.execute lève VLMAdapterError.
picarones/adapters/storage/job_store.py
---------------------------------------
JobStoreError hérite désormais de PicaronesError (au lieu de
Exception). Un caller `except PicaronesError` attrape désormais
les erreurs de persistance jobs.
Tests S52 (9 nouveaux)
----------------------
- TestErrorInheritance : OCR/LLM/VLM AdapterErrors héritent de
AdapterStepError ET de PicaronesError ; JobStoreError de
PicaronesError.
- TestPolymorphicCatch : except AdapterStepError attrape les 3
sous-classes ; except PicaronesError attrape tout (y compris
JobStoreError).
Migration tests existants
-------------------------
- tests/adapters/llm/test_sprint_a14_s44 : OCRAdapterError →
LLMAdapterError (4 substitutions).
- tests/adapters/vlm/test_sprint_a14_s45 : OCRAdapterError →
VLMAdapterError (4 substitutions).
Tests : 268 passed dans tests/adapters/ + tests/domain/, 0
régression.
Lint : All checks passed.
https://claude.ai/code/session_011XQZNitg1rCgia8ZD1a2hP
- picarones/adapters/llm/base.py +21 -5
- picarones/adapters/ocr/base.py +7 -2
- picarones/adapters/storage/job_store.py +11 -2
- picarones/adapters/vlm/base.py +14 -6
- picarones/domain/errors.py +12 -0
- tests/adapters/llm/test_sprint_a14_s44_llm_step_executor.py +5 -5
- tests/adapters/vlm/test_sprint_a14_s45_vlm_adapters.py +5 -5
- tests/domain/test_sprint_a14_s52_error_hierarchy.py +67 -0
|
@@ -138,6 +138,22 @@ def log_http_error(
|
|
| 138 |
)
|
| 139 |
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
@dataclass
|
| 142 |
class LLMResult:
|
| 143 |
"""Résultat produit par un appel LLM."""
|
|
@@ -341,22 +357,21 @@ class BaseLLMAdapter(ABC):
|
|
| 341 |
from pathlib import Path
|
| 342 |
import base64
|
| 343 |
|
| 344 |
-
from picarones.adapters.ocr.base import OCRAdapterError
|
| 345 |
from picarones.domain.artifacts import Artifact, ArtifactType
|
| 346 |
|
| 347 |
if ArtifactType.RAW_TEXT not in inputs:
|
| 348 |
-
raise
|
| 349 |
f"{self.name} : input RAW_TEXT manquant.",
|
| 350 |
)
|
| 351 |
text_artifact = inputs[ArtifactType.RAW_TEXT]
|
| 352 |
if text_artifact.uri is None:
|
| 353 |
-
raise
|
| 354 |
f"{self.name} : artefact RAW_TEXT "
|
| 355 |
f"{text_artifact.id!r} sans URI.",
|
| 356 |
)
|
| 357 |
text_path = Path(text_artifact.uri)
|
| 358 |
if not text_path.exists():
|
| 359 |
-
raise
|
| 360 |
f"{self.name} : fichier texte introuvable {text_path!r}.",
|
| 361 |
)
|
| 362 |
|
|
@@ -379,7 +394,7 @@ class BaseLLMAdapter(ABC):
|
|
| 379 |
|
| 380 |
result = self.complete(prompt, image_b64=image_b64)
|
| 381 |
if not result.success:
|
| 382 |
-
raise
|
| 383 |
f"{self.name} : LLM a échoué ({result.error}).",
|
| 384 |
)
|
| 385 |
|
|
@@ -411,6 +426,7 @@ class BaseLLMAdapter(ABC):
|
|
| 411 |
|
| 412 |
__all__ = [
|
| 413 |
"BaseLLMAdapter",
|
|
|
|
| 414 |
"LLMResult",
|
| 415 |
"log_http_error",
|
| 416 |
"normalize_llm_content",
|
|
|
|
| 138 |
)
|
| 139 |
|
| 140 |
|
| 141 |
+
from picarones.domain.errors import AdapterStepError
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class LLMAdapterError(AdapterStepError):
|
| 145 |
+
"""Erreur typée pour un échec d'adapter LLM (Sprint S52).
|
| 146 |
+
|
| 147 |
+
Hérite de ``AdapterStepError`` (commune avec OCR et VLM) → un
|
| 148 |
+
caller peut catcher ``AdapterStepError`` pour toute erreur
|
| 149 |
+
d'adapter sans connaître la sous-classe.
|
| 150 |
+
|
| 151 |
+
Avant S52, ``BaseLLMAdapter.execute`` levait ``OCRAdapterError``
|
| 152 |
+
par confusion sémantique — c'était noté dans l'audit comme issue
|
| 153 |
+
#11 (hiérarchie incohérente).
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
|
| 157 |
@dataclass
|
| 158 |
class LLMResult:
|
| 159 |
"""Résultat produit par un appel LLM."""
|
|
|
|
| 357 |
from pathlib import Path
|
| 358 |
import base64
|
| 359 |
|
|
|
|
| 360 |
from picarones.domain.artifacts import Artifact, ArtifactType
|
| 361 |
|
| 362 |
if ArtifactType.RAW_TEXT not in inputs:
|
| 363 |
+
raise LLMAdapterError(
|
| 364 |
f"{self.name} : input RAW_TEXT manquant.",
|
| 365 |
)
|
| 366 |
text_artifact = inputs[ArtifactType.RAW_TEXT]
|
| 367 |
if text_artifact.uri is None:
|
| 368 |
+
raise LLMAdapterError(
|
| 369 |
f"{self.name} : artefact RAW_TEXT "
|
| 370 |
f"{text_artifact.id!r} sans URI.",
|
| 371 |
)
|
| 372 |
text_path = Path(text_artifact.uri)
|
| 373 |
if not text_path.exists():
|
| 374 |
+
raise LLMAdapterError(
|
| 375 |
f"{self.name} : fichier texte introuvable {text_path!r}.",
|
| 376 |
)
|
| 377 |
|
|
|
|
| 394 |
|
| 395 |
result = self.complete(prompt, image_b64=image_b64)
|
| 396 |
if not result.success:
|
| 397 |
+
raise LLMAdapterError(
|
| 398 |
f"{self.name} : LLM a échoué ({result.error}).",
|
| 399 |
)
|
| 400 |
|
|
|
|
| 426 |
|
| 427 |
__all__ = [
|
| 428 |
"BaseLLMAdapter",
|
| 429 |
+
"LLMAdapterError",
|
| 430 |
"LLMResult",
|
| 431 |
"log_http_error",
|
| 432 |
"normalize_llm_content",
|
|
@@ -57,12 +57,17 @@ from abc import ABC, abstractmethod
|
|
| 57 |
from typing import Any
|
| 58 |
|
| 59 |
from picarones.domain.artifacts import Artifact, ArtifactType
|
| 60 |
-
from picarones.domain.errors import
|
| 61 |
|
| 62 |
|
| 63 |
-
class OCRAdapterError(
|
| 64 |
"""Erreur typée pour un échec d'adapter OCR du nouveau monde.
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
Le ``PipelineExecutor`` capture cette exception (et toute autre)
|
| 67 |
et marque le step correspondant comme failed avec
|
| 68 |
``StepResult.error`` renseigné. Les callers downstream
|
|
|
|
| 57 |
from typing import Any
|
| 58 |
|
| 59 |
from picarones.domain.artifacts import Artifact, ArtifactType
|
| 60 |
+
from picarones.domain.errors import AdapterStepError
|
| 61 |
|
| 62 |
|
| 63 |
+
class OCRAdapterError(AdapterStepError):
|
| 64 |
"""Erreur typée pour un échec d'adapter OCR du nouveau monde.
|
| 65 |
|
| 66 |
+
Hérite de ``AdapterStepError`` (Sprint S52) qui hérite de
|
| 67 |
+
``PicaronesError``. Un caller peut catcher
|
| 68 |
+
``AdapterStepError`` pour toute erreur d'adapter (OCR/LLM/VLM)
|
| 69 |
+
sans connaître la sous-classe.
|
| 70 |
+
|
| 71 |
Le ``PipelineExecutor`` capture cette exception (et toute autre)
|
| 72 |
et marque le step correspondant comme failed avec
|
| 73 |
``StepResult.error`` renseigné. Les callers downstream
|
|
@@ -126,8 +126,17 @@ class JobRecord:
|
|
| 126 |
return self.status in _LIVE_STATUSES
|
| 127 |
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
|
| 133 |
class JobStore:
|
|
|
|
| 126 |
return self.status in _LIVE_STATUSES
|
| 127 |
|
| 128 |
|
| 129 |
+
from picarones.domain.errors import PicaronesError
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class JobStoreError(PicaronesError):
|
| 133 |
+
"""Erreur de persistance SQLite côté JobStore.
|
| 134 |
+
|
| 135 |
+
Sprint S52 : hérite désormais de ``PicaronesError`` (avant
|
| 136 |
+
héritait directement d'``Exception`` — un caller qui faisait
|
| 137 |
+
``except PicaronesError`` ratait silencieusement les erreurs
|
| 138 |
+
JobStore).
|
| 139 |
+
"""
|
| 140 |
|
| 141 |
|
| 142 |
class JobStore:
|
|
@@ -33,12 +33,20 @@ from pathlib import Path
|
|
| 33 |
from typing import Any
|
| 34 |
|
| 35 |
from picarones.adapters.llm.base import BaseLLMAdapter
|
| 36 |
-
from picarones.adapters.ocr.base import OCRAdapterError
|
| 37 |
from picarones.domain.artifacts import Artifact, ArtifactType
|
|
|
|
| 38 |
|
| 39 |
logger = logging.getLogger(__name__)
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
class BaseVLMAdapter(BaseLLMAdapter):
|
| 43 |
"""Adapter VLM qui transcrit une IMAGE en RAW_TEXT.
|
| 44 |
|
|
@@ -84,18 +92,18 @@ class BaseVLMAdapter(BaseLLMAdapter):
|
|
| 84 |
``{RAW_TEXT: Artifact}``.
|
| 85 |
"""
|
| 86 |
if ArtifactType.IMAGE not in inputs:
|
| 87 |
-
raise
|
| 88 |
f"{self.name} : input IMAGE manquant.",
|
| 89 |
)
|
| 90 |
image_artifact = inputs[ArtifactType.IMAGE]
|
| 91 |
if image_artifact.uri is None:
|
| 92 |
-
raise
|
| 93 |
f"{self.name} : artefact image "
|
| 94 |
f"{image_artifact.id!r} sans URI.",
|
| 95 |
)
|
| 96 |
image_path = Path(image_artifact.uri)
|
| 97 |
if not image_path.exists():
|
| 98 |
-
raise
|
| 99 |
f"{self.name} : image introuvable {image_path!r}.",
|
| 100 |
)
|
| 101 |
|
|
@@ -109,7 +117,7 @@ class BaseVLMAdapter(BaseLLMAdapter):
|
|
| 109 |
|
| 110 |
result = self.complete(prompt, image_b64=image_b64)
|
| 111 |
if not result.success:
|
| 112 |
-
raise
|
| 113 |
f"{self.name} : VLM a échoué ({result.error}).",
|
| 114 |
)
|
| 115 |
|
|
@@ -134,4 +142,4 @@ class BaseVLMAdapter(BaseLLMAdapter):
|
|
| 134 |
}
|
| 135 |
|
| 136 |
|
| 137 |
-
__all__ = ["BaseVLMAdapter"]
|
|
|
|
| 33 |
from typing import Any
|
| 34 |
|
| 35 |
from picarones.adapters.llm.base import BaseLLMAdapter
|
|
|
|
| 36 |
from picarones.domain.artifacts import Artifact, ArtifactType
|
| 37 |
+
from picarones.domain.errors import AdapterStepError
|
| 38 |
|
| 39 |
logger = logging.getLogger(__name__)
|
| 40 |
|
| 41 |
|
| 42 |
+
class VLMAdapterError(AdapterStepError):
|
| 43 |
+
"""Erreur typée pour un échec d'adapter VLM (Sprint S52).
|
| 44 |
+
|
| 45 |
+
Hérite de ``AdapterStepError`` (commune avec OCR et LLM).
|
| 46 |
+
Avant S52, les VLM levaient ``OCRAdapterError`` par confusion.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
|
| 50 |
class BaseVLMAdapter(BaseLLMAdapter):
|
| 51 |
"""Adapter VLM qui transcrit une IMAGE en RAW_TEXT.
|
| 52 |
|
|
|
|
| 92 |
``{RAW_TEXT: Artifact}``.
|
| 93 |
"""
|
| 94 |
if ArtifactType.IMAGE not in inputs:
|
| 95 |
+
raise VLMAdapterError(
|
| 96 |
f"{self.name} : input IMAGE manquant.",
|
| 97 |
)
|
| 98 |
image_artifact = inputs[ArtifactType.IMAGE]
|
| 99 |
if image_artifact.uri is None:
|
| 100 |
+
raise VLMAdapterError(
|
| 101 |
f"{self.name} : artefact image "
|
| 102 |
f"{image_artifact.id!r} sans URI.",
|
| 103 |
)
|
| 104 |
image_path = Path(image_artifact.uri)
|
| 105 |
if not image_path.exists():
|
| 106 |
+
raise VLMAdapterError(
|
| 107 |
f"{self.name} : image introuvable {image_path!r}.",
|
| 108 |
)
|
| 109 |
|
|
|
|
| 117 |
|
| 118 |
result = self.complete(prompt, image_b64=image_b64)
|
| 119 |
if not result.success:
|
| 120 |
+
raise VLMAdapterError(
|
| 121 |
f"{self.name} : VLM a échoué ({result.error}).",
|
| 122 |
)
|
| 123 |
|
|
|
|
| 142 |
}
|
| 143 |
|
| 144 |
|
| 145 |
+
__all__ = ["BaseVLMAdapter", "VLMAdapterError"]
|
|
@@ -57,9 +57,21 @@ class CorpusSpecError(PicaronesError):
|
|
| 57 |
"""
|
| 58 |
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
__all__ = [
|
| 61 |
"PicaronesError",
|
| 62 |
"ArtifactValidationError",
|
| 63 |
"ProjectionError",
|
| 64 |
"CorpusSpecError",
|
|
|
|
| 65 |
]
|
|
|
|
| 57 |
"""
|
| 58 |
|
| 59 |
|
| 60 |
+
class AdapterStepError(PicaronesError):
|
| 61 |
+
"""Racine commune des erreurs d'adapter (OCR / LLM / VLM) — Sprint S52.
|
| 62 |
+
|
| 63 |
+
Permet à un caller (typiquement le ``PipelineExecutor``) de
|
| 64 |
+
catcher *« toute erreur d'adapter »* sans avoir à connaître la
|
| 65 |
+
sous-classe spécifique. Les sous-classes ``OCRAdapterError``,
|
| 66 |
+
``LLMAdapterError``, ``VLMAdapterError`` héritent toutes de
|
| 67 |
+
``AdapterStepError``.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
|
| 71 |
__all__ = [
|
| 72 |
"PicaronesError",
|
| 73 |
"ArtifactValidationError",
|
| 74 |
"ProjectionError",
|
| 75 |
"CorpusSpecError",
|
| 76 |
+
"AdapterStepError",
|
| 77 |
]
|
|
@@ -24,7 +24,7 @@ from pathlib import Path
|
|
| 24 |
import pytest
|
| 25 |
|
| 26 |
from picarones.adapters.llm.base import BaseLLMAdapter
|
| 27 |
-
from picarones.adapters.
|
| 28 |
from picarones.domain.artifacts import Artifact, ArtifactType
|
| 29 |
from picarones.pipeline.types import RunContext
|
| 30 |
|
|
@@ -184,7 +184,7 @@ class TestLLMExecuteNominal:
|
|
| 184 |
class TestLLMExecuteErrors:
|
| 185 |
def test_missing_raw_text_raises(self) -> None:
|
| 186 |
adapter = _StubLLMAdapter()
|
| 187 |
-
with pytest.raises(
|
| 188 |
adapter.execute(
|
| 189 |
inputs={},
|
| 190 |
params={},
|
|
@@ -199,7 +199,7 @@ class TestLLMExecuteErrors:
|
|
| 199 |
type=ArtifactType.RAW_TEXT,
|
| 200 |
uri=None,
|
| 201 |
)
|
| 202 |
-
with pytest.raises(
|
| 203 |
adapter.execute(
|
| 204 |
inputs={ArtifactType.RAW_TEXT: artifact},
|
| 205 |
params={},
|
|
@@ -208,7 +208,7 @@ class TestLLMExecuteErrors:
|
|
| 208 |
|
| 209 |
def test_text_path_not_existing_raises(self) -> None:
|
| 210 |
adapter = _StubLLMAdapter()
|
| 211 |
-
with pytest.raises(
|
| 212 |
adapter.execute(
|
| 213 |
inputs={ArtifactType.RAW_TEXT: _make_text_artifact(
|
| 214 |
"/nonexistent/x.txt",
|
|
@@ -223,7 +223,7 @@ class TestLLMExecuteErrors:
|
|
| 223 |
adapter = _StubLLMAdapter(raise_on_call=True, config={
|
| 224 |
"max_retries": 0, # pas de retry pour accélérer le test
|
| 225 |
})
|
| 226 |
-
with pytest.raises(
|
| 227 |
adapter.execute(
|
| 228 |
inputs={ArtifactType.RAW_TEXT: _make_text_artifact(str(text_path))},
|
| 229 |
params={},
|
|
|
|
| 24 |
import pytest
|
| 25 |
|
| 26 |
from picarones.adapters.llm.base import BaseLLMAdapter
|
| 27 |
+
from picarones.adapters.llm.base import LLMAdapterError
|
| 28 |
from picarones.domain.artifacts import Artifact, ArtifactType
|
| 29 |
from picarones.pipeline.types import RunContext
|
| 30 |
|
|
|
|
| 184 |
class TestLLMExecuteErrors:
|
| 185 |
def test_missing_raw_text_raises(self) -> None:
|
| 186 |
adapter = _StubLLMAdapter()
|
| 187 |
+
with pytest.raises(LLMAdapterError, match="RAW_TEXT manquant"):
|
| 188 |
adapter.execute(
|
| 189 |
inputs={},
|
| 190 |
params={},
|
|
|
|
| 199 |
type=ArtifactType.RAW_TEXT,
|
| 200 |
uri=None,
|
| 201 |
)
|
| 202 |
+
with pytest.raises(LLMAdapterError, match="sans URI"):
|
| 203 |
adapter.execute(
|
| 204 |
inputs={ArtifactType.RAW_TEXT: artifact},
|
| 205 |
params={},
|
|
|
|
| 208 |
|
| 209 |
def test_text_path_not_existing_raises(self) -> None:
|
| 210 |
adapter = _StubLLMAdapter()
|
| 211 |
+
with pytest.raises(LLMAdapterError, match="introuvable"):
|
| 212 |
adapter.execute(
|
| 213 |
inputs={ArtifactType.RAW_TEXT: _make_text_artifact(
|
| 214 |
"/nonexistent/x.txt",
|
|
|
|
| 223 |
adapter = _StubLLMAdapter(raise_on_call=True, config={
|
| 224 |
"max_retries": 0, # pas de retry pour accélérer le test
|
| 225 |
})
|
| 226 |
+
with pytest.raises(LLMAdapterError, match="LLM a échoué"):
|
| 227 |
adapter.execute(
|
| 228 |
inputs={ArtifactType.RAW_TEXT: _make_text_artifact(str(text_path))},
|
| 229 |
params={},
|
|
@@ -11,7 +11,7 @@ from pathlib import Path
|
|
| 11 |
|
| 12 |
import pytest
|
| 13 |
|
| 14 |
-
from picarones.adapters.
|
| 15 |
from picarones.adapters.vlm import (
|
| 16 |
AnthropicVLMAdapter,
|
| 17 |
BaseVLMAdapter,
|
|
@@ -169,7 +169,7 @@ class TestVLMExecuteNominal:
|
|
| 169 |
class TestVLMExecuteErrors:
|
| 170 |
def test_missing_image_raises(self) -> None:
|
| 171 |
adapter = _StubVLMAdapter()
|
| 172 |
-
with pytest.raises(
|
| 173 |
adapter.execute(inputs={}, params={}, context=_make_context())
|
| 174 |
|
| 175 |
def test_image_without_uri_raises(self) -> None:
|
|
@@ -180,7 +180,7 @@ class TestVLMExecuteErrors:
|
|
| 180 |
type=ArtifactType.IMAGE,
|
| 181 |
uri=None,
|
| 182 |
)
|
| 183 |
-
with pytest.raises(
|
| 184 |
adapter.execute(
|
| 185 |
inputs={ArtifactType.IMAGE: artifact},
|
| 186 |
params={},
|
|
@@ -189,7 +189,7 @@ class TestVLMExecuteErrors:
|
|
| 189 |
|
| 190 |
def test_image_path_not_existing_raises(self) -> None:
|
| 191 |
adapter = _StubVLMAdapter()
|
| 192 |
-
with pytest.raises(
|
| 193 |
adapter.execute(
|
| 194 |
inputs={ArtifactType.IMAGE: _make_image_artifact(
|
| 195 |
"/nonexistent/img.png",
|
|
@@ -202,7 +202,7 @@ class TestVLMExecuteErrors:
|
|
| 202 |
image_path = tmp_path / "doc.png"
|
| 203 |
image_path.write_bytes(b"x")
|
| 204 |
adapter = _StubVLMAdapter(raise_on_call=True)
|
| 205 |
-
with pytest.raises(
|
| 206 |
adapter.execute(
|
| 207 |
inputs={ArtifactType.IMAGE: _make_image_artifact(str(image_path))},
|
| 208 |
params={},
|
|
|
|
| 11 |
|
| 12 |
import pytest
|
| 13 |
|
| 14 |
+
from picarones.adapters.vlm.base import VLMAdapterError
|
| 15 |
from picarones.adapters.vlm import (
|
| 16 |
AnthropicVLMAdapter,
|
| 17 |
BaseVLMAdapter,
|
|
|
|
| 169 |
class TestVLMExecuteErrors:
|
| 170 |
def test_missing_image_raises(self) -> None:
|
| 171 |
adapter = _StubVLMAdapter()
|
| 172 |
+
with pytest.raises(VLMAdapterError, match="IMAGE manquant"):
|
| 173 |
adapter.execute(inputs={}, params={}, context=_make_context())
|
| 174 |
|
| 175 |
def test_image_without_uri_raises(self) -> None:
|
|
|
|
| 180 |
type=ArtifactType.IMAGE,
|
| 181 |
uri=None,
|
| 182 |
)
|
| 183 |
+
with pytest.raises(VLMAdapterError, match="sans URI"):
|
| 184 |
adapter.execute(
|
| 185 |
inputs={ArtifactType.IMAGE: artifact},
|
| 186 |
params={},
|
|
|
|
| 189 |
|
| 190 |
def test_image_path_not_existing_raises(self) -> None:
|
| 191 |
adapter = _StubVLMAdapter()
|
| 192 |
+
with pytest.raises(VLMAdapterError, match="introuvable"):
|
| 193 |
adapter.execute(
|
| 194 |
inputs={ArtifactType.IMAGE: _make_image_artifact(
|
| 195 |
"/nonexistent/img.png",
|
|
|
|
| 202 |
image_path = tmp_path / "doc.png"
|
| 203 |
image_path.write_bytes(b"x")
|
| 204 |
adapter = _StubVLMAdapter(raise_on_call=True)
|
| 205 |
+
with pytest.raises(VLMAdapterError, match="VLM a échoué"):
|
| 206 |
adapter.execute(
|
| 207 |
inputs={ArtifactType.IMAGE: _make_image_artifact(str(image_path))},
|
| 208 |
params={},
|
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Sprint A14-S52 — hiérarchie d'erreurs unifiée (fix audit #7 + #11).
|
| 2 |
+
|
| 3 |
+
Avant S52 :
|
| 4 |
+
- LLM/VLM levaient OCRAdapterError (mauvaise classe).
|
| 5 |
+
- JobStoreError héritait de Exception (pas de PicaronesError).
|
| 6 |
+
- Pas de racine commune AdapterStepError pour catcher OCR+LLM+VLM.
|
| 7 |
+
|
| 8 |
+
Après S52 :
|
| 9 |
+
- AdapterStepError(PicaronesError) est la racine commune.
|
| 10 |
+
- OCRAdapterError, LLMAdapterError, VLMAdapterError héritent.
|
| 11 |
+
- JobStoreError hérite de PicaronesError.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import pytest
|
| 17 |
+
|
| 18 |
+
from picarones.adapters.llm.base import LLMAdapterError
|
| 19 |
+
from picarones.adapters.ocr.base import OCRAdapterError
|
| 20 |
+
from picarones.adapters.storage import JobStoreError
|
| 21 |
+
from picarones.adapters.vlm.base import VLMAdapterError
|
| 22 |
+
from picarones.domain.errors import AdapterStepError, PicaronesError
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class TestErrorInheritance:
|
| 26 |
+
def test_ocr_inherits_adapter_step_error(self) -> None:
|
| 27 |
+
assert issubclass(OCRAdapterError, AdapterStepError)
|
| 28 |
+
assert issubclass(OCRAdapterError, PicaronesError)
|
| 29 |
+
|
| 30 |
+
def test_llm_inherits_adapter_step_error(self) -> None:
|
| 31 |
+
assert issubclass(LLMAdapterError, AdapterStepError)
|
| 32 |
+
assert issubclass(LLMAdapterError, PicaronesError)
|
| 33 |
+
|
| 34 |
+
def test_vlm_inherits_adapter_step_error(self) -> None:
|
| 35 |
+
assert issubclass(VLMAdapterError, AdapterStepError)
|
| 36 |
+
assert issubclass(VLMAdapterError, PicaronesError)
|
| 37 |
+
|
| 38 |
+
def test_jobstore_inherits_picarones_error(self) -> None:
|
| 39 |
+
# Avant S52, héritait de Exception → un caller `except
|
| 40 |
+
# PicaronesError` ratait JobStoreError. Maintenant inclus.
|
| 41 |
+
assert issubclass(JobStoreError, PicaronesError)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class TestPolymorphicCatch:
|
| 45 |
+
"""Un caller peut catcher AdapterStepError pour gérer toute
|
| 46 |
+
erreur d'adapter sans connaître la sous-classe."""
|
| 47 |
+
|
| 48 |
+
def test_catches_ocr(self) -> None:
|
| 49 |
+
with pytest.raises(AdapterStepError):
|
| 50 |
+
raise OCRAdapterError("ocr boom")
|
| 51 |
+
|
| 52 |
+
def test_catches_llm(self) -> None:
|
| 53 |
+
with pytest.raises(AdapterStepError):
|
| 54 |
+
raise LLMAdapterError("llm boom")
|
| 55 |
+
|
| 56 |
+
def test_catches_vlm(self) -> None:
|
| 57 |
+
with pytest.raises(AdapterStepError):
|
| 58 |
+
raise VLMAdapterError("vlm boom")
|
| 59 |
+
|
| 60 |
+
def test_picarones_catches_all_adapter_errors(self) -> None:
|
| 61 |
+
for cls in (OCRAdapterError, LLMAdapterError, VLMAdapterError):
|
| 62 |
+
with pytest.raises(PicaronesError):
|
| 63 |
+
raise cls("boom")
|
| 64 |
+
|
| 65 |
+
def test_picarones_catches_jobstore(self) -> None:
|
| 66 |
+
with pytest.raises(PicaronesError):
|
| 67 |
+
raise JobStoreError("store boom")
|