Files
TieMeasureFlow/src/backend/tests/test_ai_service.py
T
Adriano 403c923188 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>
2026-09-21 22:43:42 +02:00

68 lines
2.6 KiB
Python

"""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 == []