cleanup: drop redundant @pytest.mark.asyncio + fix log_id type

- 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>
This commit is contained in:
Giorgio Gilestro 2026-05-27 19:32:38 +02:00
parent b47c45e218
commit 308878749f
5 changed files with 3 additions and 33 deletions

View file

@ -136,7 +136,9 @@ class StrategicLogTranslation(Base):
id: Mapped[int] = mapped_column(_PK, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(_PK, primary_key=True, autoincrement=True)
log_id: Mapped[int] = mapped_column( log_id: Mapped[int] = mapped_column(
_PK, ForeignKey("strategic_logs.id", ondelete="CASCADE"), nullable=False, BigInteger().with_variant(Integer(), "sqlite"),
ForeignKey("strategic_logs.id", ondelete="CASCADE"),
nullable=False,
) )
lang: Mapped[str] = mapped_column(String(8), nullable=False) lang: Mapped[str] = mapped_column(String(8), nullable=False)
content_md: Mapped[str] = mapped_column(Text, nullable=False) content_md: Mapped[str] = mapped_column(Text, nullable=False)

View file

@ -44,7 +44,6 @@ def test_respond_in_clause_unknown_lang_falls_back_to_english():
assert respond_in_clause("xx") == "" assert respond_in_clause("xx") == ""
@pytest.mark.asyncio
async def test_translate_happy_path(monkeypatch): async def test_translate_happy_path(monkeypatch):
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
@ -66,7 +65,6 @@ async def test_translate_happy_path(monkeypatch):
assert llm_log.cost_usd == pytest.approx(0.00002) assert llm_log.cost_usd == pytest.approx(0.00002)
@pytest.mark.asyncio
async def test_translate_strips_code_fences(monkeypatch): async def test_translate_strips_code_fences(monkeypatch):
"""If the LLM wraps the output in ```markdown ... ```, strip it.""" """If the LLM wraps the output in ```markdown ... ```, strip it."""
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
@ -85,7 +83,6 @@ async def test_translate_strips_code_fences(monkeypatch):
assert translated.startswith("# Titolo") assert translated.startswith("# Titolo")
@pytest.mark.asyncio
async def test_translate_provider_failure_propagates(monkeypatch): async def test_translate_provider_failure_propagates(monkeypatch):
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
@ -98,7 +95,6 @@ async def test_translate_provider_failure_propagates(monkeypatch):
await mod.translate(client, "# Title\n\nBody.", "it") await mod.translate(client, "# Title\n\nBody.", "it")
@pytest.mark.asyncio
async def test_translate_unknown_lang_returns_source_unchanged(monkeypatch): async def test_translate_unknown_lang_returns_source_unchanged(monkeypatch):
"""Defensive: an unknown lang code (or 'en') short-circuits without """Defensive: an unknown lang code (or 'en') short-circuits without
calling the LLM. Callers shouldn't have to gate the call themselves.""" calling the LLM. Callers shouldn't have to gate the call themselves."""

View file

@ -243,7 +243,6 @@ def test_apply_mapping_skips_blank_and_unparseable_rows():
assert [p.slice for p in pie.positions] == ["AAPL", "NVDA"] assert [p.slice for p in pie.positions] == ["AAPL", "NVDA"]
@pytest.mark.asyncio
async def test_extract_mapping_via_llm_parses_valid_json(): async def test_extract_mapping_via_llm_parses_valid_json():
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from app.services.llm_csv_parser import _extract_mapping_via_llm from app.services.llm_csv_parser import _extract_mapping_via_llm
@ -275,7 +274,6 @@ async def test_extract_mapping_via_llm_parses_valid_json():
fake_call_llm.assert_awaited_once() fake_call_llm.assert_awaited_once()
@pytest.mark.asyncio
async def test_extract_mapping_via_llm_malformed_json_raises(): async def test_extract_mapping_via_llm_malformed_json_raises():
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from app.services.llm_csv_parser import LLMParseError, _extract_mapping_via_llm from app.services.llm_csv_parser import LLMParseError, _extract_mapping_via_llm
@ -298,7 +296,6 @@ async def test_extract_mapping_via_llm_malformed_json_raises():
await _extract_mapping_via_llm(fake_client, ["Symbol"], [["AAPL"]]) await _extract_mapping_via_llm(fake_client, ["Symbol"], [["AAPL"]])
@pytest.mark.asyncio
async def test_extract_mapping_via_llm_provider_failure_wraps(): async def test_extract_mapping_via_llm_provider_failure_wraps():
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from app.services.llm_csv_parser import LLMParseError, _extract_mapping_via_llm from app.services.llm_csv_parser import LLMParseError, _extract_mapping_via_llm
@ -313,7 +310,6 @@ async def test_extract_mapping_via_llm_provider_failure_wraps():
await _extract_mapping_via_llm(fake_client, ["Symbol"], [["AAPL"]]) await _extract_mapping_via_llm(fake_client, ["Symbol"], [["AAPL"]])
@pytest.mark.asyncio
async def test_parse_with_llm_cache_miss_inserts_template(tmp_path): async def test_parse_with_llm_cache_miss_inserts_template(tmp_path):
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from sqlalchemy import select from sqlalchemy import select
@ -360,7 +356,6 @@ async def test_parse_with_llm_cache_miss_inserts_template(tmp_path):
assert not hasattr(tmpl, "user_id"), "sample row must not be linked to a user" assert not hasattr(tmpl, "user_id"), "sample row must not be linked to a user"
@pytest.mark.asyncio
async def test_parse_with_llm_cache_hit_skips_llm(tmp_path): async def test_parse_with_llm_cache_hit_skips_llm(tmp_path):
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from sqlalchemy import select from sqlalchemy import select
@ -416,7 +411,6 @@ async def test_parse_with_llm_cache_hit_skips_llm(tmp_path):
assert rows[0].use_count == 2 assert rows[0].use_count == 2
@pytest.mark.asyncio
async def test_parse_with_llm_stale_mapping_raises_but_does_not_evict(tmp_path): async def test_parse_with_llm_stale_mapping_raises_but_does_not_evict(tmp_path):
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from sqlalchemy import select from sqlalchemy import select
@ -458,7 +452,6 @@ async def test_parse_with_llm_stale_mapping_raises_but_does_not_evict(tmp_path):
assert len(rows) == 1 assert len(rows) == 1
@pytest.mark.asyncio
async def test_parse_portfolio_route_falls_through_to_llm(tmp_path, monkeypatch): async def test_parse_portfolio_route_falls_through_to_llm(tmp_path, monkeypatch):
"""End-to-end: T212 parser raises CSVImportError, LLM fallback runs, """End-to-end: T212 parser raises CSVImportError, LLM fallback runs,
response shape matches the existing JSON contract.""" response shape matches the existing JSON contract."""

View file

@ -55,7 +55,6 @@ def test_strategic_log_translation_model_columns():
assert cols["content_md"].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): 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 """When no users have an active non-en lang, the fan-out makes no
translation calls and no rows are inserted.""" translation calls and no rows are inserted."""
@ -93,7 +92,6 @@ async def test_log_translation_fanout_no_active_non_en_users(tmp_path, monkeypat
assert rows == [] assert rows == []
@pytest.mark.asyncio
async def test_log_translation_fanout_italian_user(tmp_path, monkeypatch): async def test_log_translation_fanout_italian_user(tmp_path, monkeypatch):
"""One user at lang=it triggers one translation; the row lands with """One user at lang=it triggers one translation; the row lands with
the right lang and log_id.""" the right lang and log_id."""
@ -141,7 +139,6 @@ async def test_log_translation_fanout_italian_user(tmp_path, monkeypatch):
assert row.llm_cost_usd == pytest.approx(0.00002) 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): 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 """If one language's translation fails, the others (if any) still land
and the job does not raise.""" and the job does not raise."""
@ -178,7 +175,6 @@ async def test_log_translation_fanout_per_language_failure_isolated(tmp_path, mo
assert rows == [] assert rows == []
@pytest.mark.asyncio
async def test_analyse_threads_lang_into_system_prompt(tmp_path, monkeypatch): async def test_analyse_threads_lang_into_system_prompt(tmp_path, monkeypatch):
"""When lang='it', the system prompt sent to call_llm contains """When lang='it', the system prompt sent to call_llm contains
'Respond in Italian.' the LLM does the rest.""" 'Respond in Italian.' the LLM does the rest."""
@ -217,7 +213,6 @@ async def test_analyse_threads_lang_into_system_prompt(tmp_path, monkeypatch):
assert "Respond in Italian" in system assert "Respond in Italian" in system
@pytest.mark.asyncio
async def test_analyse_no_clause_when_lang_is_en(tmp_path, monkeypatch): async def test_analyse_no_clause_when_lang_is_en(tmp_path, monkeypatch):
from app.services import portfolio_analysis as pa from app.services import portfolio_analysis as pa
from app.services.openrouter import LogResult from app.services.openrouter import LogResult
@ -252,7 +247,6 @@ async def test_analyse_no_clause_when_lang_is_en(tmp_path, monkeypatch):
assert "Respond in" not in system assert "Respond in" not in system
@pytest.mark.asyncio
async def test_digest_translates_variants_per_active_lang(monkeypatch): async def test_digest_translates_variants_per_active_lang(monkeypatch):
"""After English variants are built, the job translates each to every """After English variants are built, the job translates each to every
active non-en lang. The result is an in-memory mapping the send loop active non-en lang. The result is an in-memory mapping the send loop
@ -294,7 +288,6 @@ async def test_digest_translates_variants_per_active_lang(monkeypatch):
assert table[("INTERMEDIATE", "it")].startswith("[IT] ") assert table[("INTERMEDIATE", "it")].startswith("[IT] ")
@pytest.mark.asyncio
async def test_digest_translation_failure_falls_back_to_english(monkeypatch): async def test_digest_translation_failure_falls_back_to_english(monkeypatch):
"""When translate() fails for a (tone, lang) cell, the table entry """When translate() fails for a (tone, lang) cell, the table entry
for that cell is the English variant of the same tone the user for that cell is the English variant of the same tone the user
@ -334,7 +327,6 @@ def test_digest_pick_variant_uses_user_lang():
assert ed._pick_variant(table, tone="UNKNOWN", lang="en") == "intermediate en" assert ed._pick_variant(table, tone="UNKNOWN", lang="en") == "intermediate en"
@pytest.mark.asyncio
async def test_log_endpoint_serves_italian_when_user_is_italian(tmp_path): async def test_log_endpoint_serves_italian_when_user_is_italian(tmp_path):
"""When a user with lang='it' opens /log, the served content is the """When a user with lang='it' opens /log, the served content is the
Italian translation, not the English original.""" Italian translation, not the English original."""
@ -370,7 +362,6 @@ async def test_log_endpoint_serves_italian_when_user_is_italian(tmp_path):
assert "Open" not in content assert "Open" not in content
@pytest.mark.asyncio
async def test_log_endpoint_falls_back_to_english_when_no_translation(tmp_path): async def test_log_endpoint_falls_back_to_english_when_no_translation(tmp_path):
"""User lang='it' but no IT translation exists → English fallback.""" """User lang='it' but no IT translation exists → English fallback."""
from app.db import utcnow from app.db import utcnow
@ -397,7 +388,6 @@ async def test_log_endpoint_falls_back_to_english_when_no_translation(tmp_path):
assert "Open" in content assert "Open" in content
@pytest.mark.asyncio
async def test_patch_language_accepts_active(tmp_path): async def test_patch_language_accepts_active(tmp_path):
"""PATCH /api/settings/language accepts 'en' and 'it' and persists.""" """PATCH /api/settings/language accepts 'en' and 'it' and persists."""
from app.models import User from app.models import User
@ -428,7 +418,6 @@ async def test_patch_language_accepts_active(tmp_path):
assert user.lang == "it" assert user.lang == "it"
@pytest.mark.asyncio
async def test_patch_language_rejects_wip(tmp_path): async def test_patch_language_rejects_wip(tmp_path):
"""PATCH rejects 'es'/'fr'/'de'/'xx' with 400 — ACTIVE_LANGUAGES gate.""" """PATCH rejects 'es'/'fr'/'de'/'xx' with 400 — ACTIVE_LANGUAGES gate."""
from fastapi import HTTPException from fastapi import HTTPException

View file

@ -30,7 +30,6 @@ def _build_session_factory(tmp_path):
return engine, factory, _setup return engine, factory, _setup
@pytest.mark.asyncio
async def test_validate_happy_path(tmp_path, monkeypatch): async def test_validate_happy_path(tmp_path, monkeypatch):
from app.routers.ticker_validate import validate_ticker from app.routers.ticker_validate import validate_ticker
from app.services.market import Quote from app.services.market import Quote
@ -62,7 +61,6 @@ async def test_validate_happy_path(tmp_path, monkeypatch):
assert result["as_of"] == "2026-05-27" assert result["as_of"] == "2026-05-27"
@pytest.mark.asyncio
async def test_validate_unknown_symbol(tmp_path, monkeypatch): async def test_validate_unknown_symbol(tmp_path, monkeypatch):
from app.routers.ticker_validate import validate_ticker from app.routers.ticker_validate import validate_ticker
from app.services.market import Quote from app.services.market import Quote
@ -85,7 +83,6 @@ async def test_validate_unknown_symbol(tmp_path, monkeypatch):
assert "not recognised" in result["error"].lower() assert "not recognised" in result["error"].lower()
@pytest.mark.asyncio
async def test_validate_empty_symbol_rejects(): async def test_validate_empty_symbol_rejects():
from app.routers.ticker_validate import validate_ticker from app.routers.ticker_validate import validate_ticker
@ -95,7 +92,6 @@ async def test_validate_empty_symbol_rejects():
assert "required" in result["error"].lower() assert "required" in result["error"].lower()
@pytest.mark.asyncio
async def test_validate_seeds_universe_and_quote(tmp_path, monkeypatch): async def test_validate_seeds_universe_and_quote(tmp_path, monkeypatch):
"""Side-effect check: on success, the symbol is upserted into the """Side-effect check: on success, the symbol is upserted into the
universe and a Quote row is written.""" universe and a Quote row is written."""
@ -137,7 +133,6 @@ async def test_validate_seeds_universe_and_quote(tmp_path, monkeypatch):
assert rows[0].currency == "USD" assert rows[0].currency == "USD"
@pytest.mark.asyncio
async def test_historical_happy_path(monkeypatch): async def test_historical_happy_path(monkeypatch):
from app.routers.ticker_validate import get_historical from app.routers.ticker_validate import get_historical
import app.routers.ticker_validate as mod import app.routers.ticker_validate as mod
@ -156,7 +151,6 @@ async def test_historical_happy_path(monkeypatch):
assert result["actual_date"] == "2024-01-12" assert result["actual_date"] == "2024-01-12"
@pytest.mark.asyncio
async def test_historical_future_date_rejected(): async def test_historical_future_date_rejected():
from fastapi import HTTPException from fastapi import HTTPException
from app.routers.ticker_validate import get_historical from app.routers.ticker_validate import get_historical
@ -168,7 +162,6 @@ async def test_historical_future_date_rejected():
assert "future" in str(exc.value.detail).lower() assert "future" in str(exc.value.detail).lower()
@pytest.mark.asyncio
async def test_historical_bad_date_format_rejected(): async def test_historical_bad_date_format_rejected():
from fastapi import HTTPException from fastapi import HTTPException
from app.routers.ticker_validate import get_historical from app.routers.ticker_validate import get_historical
@ -178,7 +171,6 @@ async def test_historical_bad_date_format_rejected():
assert exc.value.status_code == 400 assert exc.value.status_code == 400
@pytest.mark.asyncio
async def test_historical_no_data(monkeypatch): async def test_historical_no_data(monkeypatch):
from app.routers.ticker_validate import get_historical from app.routers.ticker_validate import get_historical
import app.routers.ticker_validate as mod import app.routers.ticker_validate as mod
@ -192,7 +184,6 @@ async def test_historical_no_data(monkeypatch):
assert "no data" in result["error"].lower() assert "no data" in result["error"].lower()
@pytest.mark.asyncio
async def test_historical_provider_failure(monkeypatch): async def test_historical_provider_failure(monkeypatch):
import httpx import httpx
@ -208,7 +199,6 @@ async def test_historical_provider_failure(monkeypatch):
assert "couldn" in result["error"].lower() or "fetch" in result["error"].lower() assert "couldn" in result["error"].lower() or "fetch" in result["error"].lower()
@pytest.mark.asyncio
async def test_fetch_yahoo_historical_walks_back_to_preceding_trading_day(monkeypatch): async def test_fetch_yahoo_historical_walks_back_to_preceding_trading_day(monkeypatch):
"""Unit test for the helper itself: feed a hand-crafted series with a """Unit test for the helper itself: feed a hand-crafted series with a
weekend gap, ask for the Saturday close, expect Friday's close.""" weekend gap, ask for the Saturday close, expect Friday's close."""