"""Tests for scan_all_tickers orchestration: error isolation between phases.""" from __future__ import annotations import pytest from datetime import datetime, timezone from sqlalchemy import select from app.models.score import CompositeScore, DimensionScore from app.models.ticker import Ticker from app.services import rr_scanner_service, scoring_service from tests.conftest import _test_session_factory # type: ignore @pytest.fixture async def session(): async with _test_session_factory() as s: yield s async def test_scan_proceeds_when_score_refresh_fails(session, monkeypatch): """A scoring failure must not skip setup detection for the ticker. Qualification re-gates on live scores at alert time, so a stale score is recoverable — a skipped scan is not. """ session.add(Ticker(symbol="AAA")) await session.commit() async def _boom(db, symbol): raise RuntimeError("scoring unavailable") scanned: list[str] = [] async def _fake_scan_ticker(db, symbol, *args, **kwargs): scanned.append(symbol) return [] monkeypatch.setattr(scoring_service, "compute_all_dimensions", _boom) monkeypatch.setattr(rr_scanner_service, "scan_ticker", _fake_scan_ticker) setups = await rr_scanner_service.scan_all_tickers(session) assert scanned == ["AAA"] assert setups == [] async def test_scan_marks_scores_stale_when_refresh_fails(session, monkeypatch): ticker = Ticker(symbol="AAA") session.add(ticker) await session.flush() now = datetime.now(timezone.utc) session.add_all([ DimensionScore( ticker_id=ticker.id, dimension="technical", score=70.0, is_stale=False, computed_at=now, ), CompositeScore( ticker_id=ticker.id, score=70.0, is_stale=False, weights_json="{}", computed_at=now, ), ]) await session.commit() async def _boom(db, symbol): raise RuntimeError("scoring unavailable") async def _fake_scan_ticker(db, symbol, *args, **kwargs): comp = ( await db.execute(select(CompositeScore).where(CompositeScore.ticker_id == ticker.id)) ).scalar_one() dimensions = ( await db.execute(select(DimensionScore).where(DimensionScore.ticker_id == ticker.id)) ).scalars().all() assert comp.is_stale is True assert all(score.is_stale for score in dimensions) return [] monkeypatch.setattr(scoring_service, "compute_all_dimensions", _boom) monkeypatch.setattr(rr_scanner_service, "scan_ticker", _fake_scan_ticker) await rr_scanner_service.scan_all_tickers(session) async def test_scan_error_does_not_stop_later_tickers(session, monkeypatch): session.add_all([Ticker(symbol="AAA"), Ticker(symbol="BBB")]) await session.commit() scanned: list[str] = [] async def _fake_scan_ticker(db, symbol, *args, **kwargs): scanned.append(symbol) if symbol == "AAA": raise RuntimeError("scan blew up") return [] monkeypatch.setattr(rr_scanner_service, "scan_ticker", _fake_scan_ticker) await rr_scanner_service.scan_all_tickers(session) assert scanned == ["AAA", "BBB"]