From 308878749f995dad7df42dd8b9263359260659a1 Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Wed, 27 May 2026 19:32:38 +0200 Subject: [PATCH] cleanup: drop redundant @pytest.mark.asyncio + fix log_id type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/models.py | 4 +++- tests/test_i18n.py | 4 ---- tests/test_llm_csv_parser.py | 7 ------- tests/test_localization_integration.py | 11 ----------- tests/test_ticker_validate.py | 10 ---------- 5 files changed, 3 insertions(+), 33 deletions(-) diff --git a/app/models.py b/app/models.py index 13ba1ab..a16e039 100644 --- a/app/models.py +++ b/app/models.py @@ -136,7 +136,9 @@ class StrategicLogTranslation(Base): id: Mapped[int] = mapped_column(_PK, primary_key=True, autoincrement=True) 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) content_md: Mapped[str] = mapped_column(Text, nullable=False) diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 6fc207e..485b0b4 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -44,7 +44,6 @@ def test_respond_in_clause_unknown_lang_falls_back_to_english(): assert respond_in_clause("xx") == "" -@pytest.mark.asyncio async def test_translate_happy_path(monkeypatch): 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) -@pytest.mark.asyncio async def test_translate_strips_code_fences(monkeypatch): """If the LLM wraps the output in ```markdown ... ```, strip it.""" from unittest.mock import AsyncMock, MagicMock @@ -85,7 +83,6 @@ async def test_translate_strips_code_fences(monkeypatch): assert translated.startswith("# Titolo") -@pytest.mark.asyncio async def test_translate_provider_failure_propagates(monkeypatch): 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") -@pytest.mark.asyncio 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.""" diff --git a/tests/test_llm_csv_parser.py b/tests/test_llm_csv_parser.py index 0463e96..3f6492e 100644 --- a/tests/test_llm_csv_parser.py +++ b/tests/test_llm_csv_parser.py @@ -243,7 +243,6 @@ def test_apply_mapping_skips_blank_and_unparseable_rows(): assert [p.slice for p in pie.positions] == ["AAPL", "NVDA"] -@pytest.mark.asyncio async def test_extract_mapping_via_llm_parses_valid_json(): from unittest.mock import AsyncMock, MagicMock 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() -@pytest.mark.asyncio async def test_extract_mapping_via_llm_malformed_json_raises(): from unittest.mock import AsyncMock, MagicMock 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"]]) -@pytest.mark.asyncio async def test_extract_mapping_via_llm_provider_failure_wraps(): from unittest.mock import AsyncMock, MagicMock 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"]]) -@pytest.mark.asyncio async def test_parse_with_llm_cache_miss_inserts_template(tmp_path): from unittest.mock import AsyncMock 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" -@pytest.mark.asyncio async def test_parse_with_llm_cache_hit_skips_llm(tmp_path): from unittest.mock import AsyncMock 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 -@pytest.mark.asyncio async def test_parse_with_llm_stale_mapping_raises_but_does_not_evict(tmp_path): from unittest.mock import AsyncMock 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 -@pytest.mark.asyncio async def test_parse_portfolio_route_falls_through_to_llm(tmp_path, monkeypatch): """End-to-end: T212 parser raises CSVImportError, LLM fallback runs, response shape matches the existing JSON contract.""" diff --git a/tests/test_localization_integration.py b/tests/test_localization_integration.py index c5cf92c..d27a9ff 100644 --- a/tests/test_localization_integration.py +++ b/tests/test_localization_integration.py @@ -55,7 +55,6 @@ def test_strategic_log_translation_model_columns(): 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.""" @@ -93,7 +92,6 @@ async def test_log_translation_fanout_no_active_non_en_users(tmp_path, monkeypat 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.""" @@ -141,7 +139,6 @@ async def test_log_translation_fanout_italian_user(tmp_path, monkeypatch): 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.""" @@ -178,7 +175,6 @@ async def test_log_translation_fanout_per_language_failure_isolated(tmp_path, mo assert rows == [] -@pytest.mark.asyncio async def test_analyse_threads_lang_into_system_prompt(tmp_path, monkeypatch): """When lang='it', the system prompt sent to call_llm contains '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 -@pytest.mark.asyncio async def test_analyse_no_clause_when_lang_is_en(tmp_path, monkeypatch): from app.services import portfolio_analysis as pa 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 -@pytest.mark.asyncio async def test_digest_translates_variants_per_active_lang(monkeypatch): """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 @@ -294,7 +288,6 @@ async def test_digest_translates_variants_per_active_lang(monkeypatch): assert table[("INTERMEDIATE", "it")].startswith("[IT] ") -@pytest.mark.asyncio async def test_digest_translation_failure_falls_back_to_english(monkeypatch): """When translate() fails for a (tone, lang) cell, the table entry 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" -@pytest.mark.asyncio 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 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 -@pytest.mark.asyncio async def test_log_endpoint_falls_back_to_english_when_no_translation(tmp_path): """User lang='it' but no IT translation exists → English fallback.""" 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 -@pytest.mark.asyncio async def test_patch_language_accepts_active(tmp_path): """PATCH /api/settings/language accepts 'en' and 'it' and persists.""" from app.models import User @@ -428,7 +418,6 @@ async def test_patch_language_accepts_active(tmp_path): assert user.lang == "it" -@pytest.mark.asyncio async def test_patch_language_rejects_wip(tmp_path): """PATCH rejects 'es'/'fr'/'de'/'xx' with 400 — ACTIVE_LANGUAGES gate.""" from fastapi import HTTPException diff --git a/tests/test_ticker_validate.py b/tests/test_ticker_validate.py index a610e37..1e4cf54 100644 --- a/tests/test_ticker_validate.py +++ b/tests/test_ticker_validate.py @@ -30,7 +30,6 @@ def _build_session_factory(tmp_path): return engine, factory, _setup -@pytest.mark.asyncio async def test_validate_happy_path(tmp_path, monkeypatch): from app.routers.ticker_validate import validate_ticker 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" -@pytest.mark.asyncio async def test_validate_unknown_symbol(tmp_path, monkeypatch): from app.routers.ticker_validate import validate_ticker 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() -@pytest.mark.asyncio async def test_validate_empty_symbol_rejects(): 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() -@pytest.mark.asyncio async def test_validate_seeds_universe_and_quote(tmp_path, monkeypatch): """Side-effect check: on success, the symbol is upserted into the 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" -@pytest.mark.asyncio async def test_historical_happy_path(monkeypatch): from app.routers.ticker_validate import get_historical 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" -@pytest.mark.asyncio async def test_historical_future_date_rejected(): from fastapi import HTTPException 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() -@pytest.mark.asyncio async def test_historical_bad_date_format_rejected(): from fastapi import HTTPException 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 -@pytest.mark.asyncio async def test_historical_no_data(monkeypatch): from app.routers.ticker_validate import get_historical 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() -@pytest.mark.asyncio async def test_historical_provider_failure(monkeypatch): 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() -@pytest.mark.asyncio 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 weekend gap, ask for the Saturday close, expect Friday's close."""