- pyproject already sets asyncio_mode=auto, so async def tests are collected as async automatically. Removed the redundant decorator from four files (test_i18n, test_llm_csv_parser, test_ticker_validate, test_localization_integration); the bare async def is enough. - StrategicLogTranslation.log_id used the _PK autoincrement type for a non-PK FK column. Replaced with a portable BigInteger that emits Integer on SQLite and BigInteger elsewhere — matches the migration's sa.BigInteger() declaration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
115 lines
3.9 KiB
Python
115 lines
3.9 KiB
Python
"""Unit tests for app.services.i18n."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
|
|
def test_languages_contains_all_four_plus_english():
|
|
from app.services.i18n import LANGUAGES
|
|
assert set(LANGUAGES.keys()) == {"en", "it", "es", "fr", "de"}
|
|
assert LANGUAGES["en"] == "English"
|
|
assert LANGUAGES["it"] == "Italian"
|
|
assert LANGUAGES["es"] == "Spanish"
|
|
assert LANGUAGES["fr"] == "French"
|
|
assert LANGUAGES["de"] == "German"
|
|
|
|
|
|
def test_active_languages_is_en_and_it_only():
|
|
from app.services.i18n import ACTIVE_LANGUAGES
|
|
assert ACTIVE_LANGUAGES == {"en", "it"}
|
|
|
|
|
|
def test_respond_in_clause_empty_for_english():
|
|
from app.services.i18n import respond_in_clause
|
|
assert respond_in_clause("en") == ""
|
|
|
|
|
|
def test_respond_in_clause_empty_for_none_or_empty():
|
|
from app.services.i18n import respond_in_clause
|
|
assert respond_in_clause("") == ""
|
|
assert respond_in_clause(None) == ""
|
|
|
|
|
|
def test_respond_in_clause_italian():
|
|
from app.services.i18n import respond_in_clause
|
|
result = respond_in_clause("it")
|
|
assert "Italian" in result
|
|
assert result.startswith("\n\n")
|
|
|
|
|
|
def test_respond_in_clause_unknown_lang_falls_back_to_english():
|
|
"""Defensive: a raw POST or stale lang code should not crash the
|
|
prompt assembly. Unknown codes map to no-suffix (English default)."""
|
|
from app.services.i18n import respond_in_clause
|
|
assert respond_in_clause("xx") == ""
|
|
|
|
|
|
async def test_translate_happy_path(monkeypatch):
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from app.services import translation as mod
|
|
from app.services.openrouter import LogResult
|
|
|
|
monkeypatch.setattr(mod, "call_llm", AsyncMock(return_value=LogResult(
|
|
content="# Apertura\n\nIl mercato è in calo dello 0,4%.",
|
|
model="deepseek/deepseek-v4-flash",
|
|
prompt_tokens=300, completion_tokens=80, cost_usd=0.00002,
|
|
)))
|
|
|
|
client = MagicMock()
|
|
translated, llm_log = await mod.translate(
|
|
client, "# Open\n\nThe market is down 0.4%.", "it",
|
|
)
|
|
assert "Apertura" in translated
|
|
assert llm_log.model == "deepseek/deepseek-v4-flash"
|
|
assert llm_log.cost_usd == pytest.approx(0.00002)
|
|
|
|
|
|
async def test_translate_strips_code_fences(monkeypatch):
|
|
"""If the LLM wraps the output in ```markdown ... ```, strip it."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from app.services import translation as mod
|
|
from app.services.openrouter import LogResult
|
|
|
|
fenced = "```markdown\n# Titolo\n\nCorpo.\n```"
|
|
monkeypatch.setattr(mod, "call_llm", AsyncMock(return_value=LogResult(
|
|
content=fenced, model="m", prompt_tokens=10, completion_tokens=20, cost_usd=0.0,
|
|
)))
|
|
|
|
client = MagicMock()
|
|
translated, _ = await mod.translate(client, "# Title\n\nBody.", "it")
|
|
assert "```" not in translated
|
|
assert translated.startswith("# Titolo")
|
|
|
|
|
|
async def test_translate_provider_failure_propagates(monkeypatch):
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from app.services import translation as mod
|
|
|
|
monkeypatch.setattr(mod, "call_llm", AsyncMock(side_effect=RuntimeError("upstream down")))
|
|
|
|
client = MagicMock()
|
|
with pytest.raises(RuntimeError, match="upstream down"):
|
|
await mod.translate(client, "# Title\n\nBody.", "it")
|
|
|
|
|
|
async def test_translate_unknown_lang_returns_source_unchanged(monkeypatch):
|
|
"""Defensive: an unknown lang code (or 'en') short-circuits without
|
|
calling the LLM. Callers shouldn't have to gate the call themselves."""
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from app.services import translation as mod
|
|
from app.services.openrouter import LogResult
|
|
|
|
call_mock = AsyncMock(return_value=LogResult(
|
|
content="should not be returned",
|
|
model="m", prompt_tokens=0, completion_tokens=0, cost_usd=0.0,
|
|
))
|
|
monkeypatch.setattr(mod, "call_llm", call_mock)
|
|
|
|
client = MagicMock()
|
|
out, _ = await mod.translate(client, "Hello world.", "en")
|
|
assert out == "Hello world."
|
|
call_mock.assert_not_awaited()
|