fix(ai): risposta AI malformata -> 502, non 400 (#36 punto 7)
json.JSONDecodeError e' un ValueError: KeyError/JSONDecodeError sulla risposta del modello finivano nell'except ValueError del router (pensato per i nostri errori di configurazione, es. API key mancante) e uscivano come 400 invece di 502. Nuova AIResponseError dedicata in ai_service.py per contenuto malformato o JSON non valido; recipes.py la traduce in 502, toccato solo per questo except. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Tests for ai_service.py error handling (gitea#36 point 7).
|
||||
|
||||
A malformed response from the AI model (missing fields, invalid JSON) must
|
||||
raise the dedicated AIResponseError, not a bare KeyError/JSONDecodeError —
|
||||
JSONDecodeError in particular is itself a ValueError subclass, so without
|
||||
this it was caught by the router's `except ValueError` (meant for our own
|
||||
config errors) and misreported as a 400 instead of a 502.
|
||||
"""
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.backend.services import ai_service
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, json_data):
|
||||
self._json_data = json_data
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def json(self):
|
||||
return self._json_data
|
||||
|
||||
|
||||
def _patch_openrouter(monkeypatch, response_json: dict):
|
||||
monkeypatch.setattr(ai_service, "extract_text_from_pdf", lambda _b: "some extracted text")
|
||||
monkeypatch.setattr(ai_service.settings, "openrouter_api_key", "test-key")
|
||||
monkeypatch.setattr(ai_service.settings, "openrouter_model", "test-model")
|
||||
|
||||
fake_client = AsyncMock()
|
||||
fake_client.post = AsyncMock(return_value=_FakeResponse(response_json))
|
||||
fake_client.__aenter__ = AsyncMock(return_value=fake_client)
|
||||
fake_client.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
return patch("src.backend.services.ai_service.httpx.AsyncClient", return_value=fake_client)
|
||||
|
||||
|
||||
async def test_missing_choices_key_raises_ai_response_error(monkeypatch):
|
||||
with _patch_openrouter(monkeypatch, {"no_choices_here": True}):
|
||||
with pytest.raises(ai_service.AIResponseError):
|
||||
await ai_service.parse_technical_sheet(b"%PDF-fake")
|
||||
|
||||
|
||||
async def test_invalid_json_content_raises_ai_response_error(monkeypatch):
|
||||
bad_content = "not valid json {{{"
|
||||
response_json = {"choices": [{"message": {"content": bad_content}}]}
|
||||
with _patch_openrouter(monkeypatch, response_json):
|
||||
with pytest.raises(ai_service.AIResponseError):
|
||||
await ai_service.parse_technical_sheet(b"%PDF-fake")
|
||||
|
||||
|
||||
async def test_valid_json_content_returns_tasks(monkeypatch):
|
||||
tasks = [{"title": "MATERIALI", "directive": "Verificare", "description": "..."}]
|
||||
response_json = {"choices": [{"message": {"content": json.dumps(tasks)}}]}
|
||||
with _patch_openrouter(monkeypatch, response_json):
|
||||
result = await ai_service.parse_technical_sheet(b"%PDF-fake")
|
||||
assert result == tasks
|
||||
|
||||
|
||||
async def test_empty_extracted_text_returns_empty_list(monkeypatch):
|
||||
"""No AI call at all when the PDF has no extractable text."""
|
||||
monkeypatch.setattr(ai_service, "extract_text_from_pdf", lambda _b: " ")
|
||||
result = await ai_service.parse_technical_sheet(b"%PDF-fake")
|
||||
assert result == []
|
||||
Reference in New Issue
Block a user