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:
parent
b47c45e218
commit
308878749f
5 changed files with 3 additions and 33 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue