"""Integration tests: model surface, ai_log_job translation fan-out, route-level localized fetch, settings PATCH validation.""" from __future__ import annotations import pytest def _build_session_factory(tmp_path): """Per-test sqlite engine + factory. Mirrors test_referral_conversion.py.""" from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from app import db as db_mod from app.db import Base import app.models # noqa: F401 — registers models on Base.metadata engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/loc.db") factory = async_sessionmaker(engine, expire_on_commit=False) db_mod._engine = engine db_mod._session_factory = factory async def _setup(): async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) return engine, factory, _setup def test_user_has_lang_column_with_default_en(): from sqlalchemy import inspect from app.models import User cols = {c.name: c for c in inspect(User).columns} assert "lang" in cols assert cols["lang"].nullable is False # SQLAlchemy default may be a callable or a literal — check both. default = cols["lang"].default assert default is not None if hasattr(default, "arg"): assert default.arg == "en" def test_strategic_log_translation_model_columns(): from sqlalchemy import inspect from app.models import StrategicLogTranslation cols = {c.name: c for c in inspect(StrategicLogTranslation).columns} assert "log_id" in cols assert "lang" in cols assert "content_md" in cols assert "generated_at" in cols assert "llm_model" in cols assert "llm_cost_usd" in cols assert cols["log_id"].nullable is False assert cols["lang"].nullable is False assert cols["content_md"].nullable is False @pytest.mark.asyncio async def test_log_translation_fanout_no_active_non_en_users(tmp_path, monkeypatch): """When no users have an active non-en lang, the fan-out makes no translation calls and no rows are inserted.""" from unittest.mock import AsyncMock from sqlalchemy import select from app.db import utcnow from app.models import StrategicLog, StrategicLogTranslation, User from app.jobs import ai_log_job _, factory, setup = _build_session_factory(tmp_path) await setup() fake_translate = AsyncMock() monkeypatch.setattr(ai_log_job, "translate", fake_translate) # Seed an English user (no non-en users). async with factory() as session: session.add(User(id=1, email="en@x", tier="paid", lang="en")) slog = StrategicLog( generated_at=utcnow(), content="# Open\n\nDown 0.4%.", model="test-model", tone="INTERMEDIATE", analysis="NORMAL", ) session.add(slog) await session.commit() log_id = slog.id async with factory() as session: await ai_log_job.translate_log_for_active_languages(session, log_id) fake_translate.assert_not_awaited() async with factory() as session: rows = (await session.execute(select(StrategicLogTranslation))).scalars().all() assert rows == [] @pytest.mark.asyncio async def test_log_translation_fanout_italian_user(tmp_path, monkeypatch): """One user at lang=it triggers one translation; the row lands with the right lang and log_id.""" from sqlalchemy import select from app.db import utcnow from app.models import StrategicLog, StrategicLogTranslation, User from app.services.openrouter import LogResult from app.jobs import ai_log_job _, factory, setup = _build_session_factory(tmp_path) await setup() async def _fake_translate(client, text, target_lang): assert target_lang == "it" return "# Apertura\n\nIn calo 0,4%.", LogResult( content="# Apertura\n\nIn calo 0,4%.", model="deepseek/deepseek-v4-flash", prompt_tokens=300, completion_tokens=80, cost_usd=0.00002, ) monkeypatch.setattr(ai_log_job, "translate", _fake_translate) async with factory() as session: session.add(User(id=2, email="it@x", tier="paid", lang="it")) slog = StrategicLog( generated_at=utcnow(), content="# Open\n\nDown 0.4%.", model="test-model", tone="INTERMEDIATE", analysis="NORMAL", ) session.add(slog) await session.commit() log_id = slog.id async with factory() as session: await ai_log_job.translate_log_for_active_languages(session, log_id) async with factory() as session: rows = (await session.execute(select(StrategicLogTranslation))).scalars().all() assert len(rows) == 1 row = rows[0] assert row.log_id == log_id assert row.lang == "it" assert row.content_md.startswith("# Apertura") assert row.llm_model == "deepseek/deepseek-v4-flash" assert row.llm_cost_usd == pytest.approx(0.00002) @pytest.mark.asyncio async def test_log_translation_fanout_per_language_failure_isolated(tmp_path, monkeypatch): """If one language's translation fails, the others (if any) still land and the job does not raise.""" from sqlalchemy import select from app.db import utcnow from app.models import StrategicLog, StrategicLogTranslation, User from app.jobs import ai_log_job _, factory, setup = _build_session_factory(tmp_path) await setup() async def _fake_translate(client, text, target_lang): raise RuntimeError("upstream down") monkeypatch.setattr(ai_log_job, "translate", _fake_translate) async with factory() as session: session.add(User(id=3, email="it@x", tier="paid", lang="it")) slog = StrategicLog( generated_at=utcnow(), content="# Open", model="test-model", tone="INTERMEDIATE", analysis="NORMAL", ) session.add(slog) await session.commit() log_id = slog.id # Must NOT raise. async with factory() as session: await ai_log_job.translate_log_for_active_languages(session, log_id) async with factory() as session: rows = (await session.execute(select(StrategicLogTranslation))).scalars().all() assert rows == []