"""Tests for the Meet Kevin pipeline orchestrator (Task 8). Tests use AsyncMock/MagicMock to avoid any real DB or LLM calls. """ import pytest from decimal import Decimal from unittest.mock import AsyncMock, MagicMock from services.meet_kevin_watcher.pipeline import process_one_video, PipelineDeps from services.meet_kevin_watcher.caption_extractor import CaptionResult from services.meet_kevin_watcher.llm_analyzer import LlmCallResult from shared.schemas.meet_kevin import ( MeetKevinAnalysis, MeetKevinTickerMention, TickerAction, TimeHorizon, MarketOutlook, ) def _make_analysis(): return MeetKevinAnalysis( market_outlook_direction=MarketOutlook.BEARISH, market_outlook_reasoning="x", macro_themes=["x"], key_risks=["x"], summary="x", tickers=[ MeetKevinTickerMention( symbol="NVDA", action=TickerAction.SELL, conviction=0.8, time_horizon=TimeHorizon.WEEKS, rationale_quote="x", video_timestamp_seconds=10, ) ], ) async def test_process_one_video_happy_path(): """discovered -> captioned -> analyzed; both deps awaited once.""" video = MagicMock(id=1, youtube_video_id="vid", status="discovered", retry_count=0) session = AsyncMock() # Wire session.execute to return something for the transcript fetch transcript = MagicMock(raw_text="hello NVDA", segments_json=[{"start": 0.0, "end": 1.0, "text": "hello"}]) session.execute = AsyncMock(return_value=MagicMock(scalar_one=lambda: transcript)) deps = PipelineDeps( extract_captions=AsyncMock(return_value=CaptionResult( source="captions_auto", language="en", raw_text="hello NVDA", segments=[{"start": 0.0, "end": 1.0, "text": "hello"}], word_count=2, )), analyze=AsyncMock(return_value=LlmCallResult( analysis=_make_analysis(), raw_response={"stop_reason": "tool_use"}, prompt_tokens=100, completion_tokens=50, cost_usd=Decimal("0.05"), )), daily_cost_used=AsyncMock(return_value=Decimal("0")), model="claude-sonnet-4-6", prompt_version="v1", daily_cost_cap_usd=Decimal("5"), workdir="/tmp", ) new_status = await process_one_video(video, session, deps) assert new_status == "analyzed" deps.extract_captions.assert_awaited_once() deps.analyze.assert_awaited_once() async def test_process_one_video_no_captions_marks_failed(): video = MagicMock(id=1, youtube_video_id="vid", status="discovered", retry_count=0) session = AsyncMock() deps = PipelineDeps( extract_captions=AsyncMock(return_value=None), analyze=AsyncMock(), daily_cost_used=AsyncMock(return_value=Decimal("0")), model="x", prompt_version="v1", daily_cost_cap_usd=Decimal("5"), workdir="/tmp", ) new_status = await process_one_video(video, session, deps) assert new_status == "failed" assert video.failure_reason == "no_captions" deps.analyze.assert_not_awaited() async def test_process_one_video_cost_cap_skips_llm(): """When daily cost cap reached, video stays captioned and analyze NOT called.""" video = MagicMock(id=1, youtube_video_id="vid", status="captioned", retry_count=0) session = AsyncMock() deps = PipelineDeps( extract_captions=AsyncMock(), analyze=AsyncMock(), daily_cost_used=AsyncMock(return_value=Decimal("5.01")), model="x", prompt_version="v1", daily_cost_cap_usd=Decimal("5"), workdir="/tmp", ) new_status = await process_one_video(video, session, deps) assert new_status == "captioned" deps.analyze.assert_not_awaited() async def test_process_one_video_llm_error_increments_retry(): """LLM exception increments retry_count; video stays captioned below threshold.""" video = MagicMock(id=1, youtube_video_id="vid", status="captioned", retry_count=0) session = AsyncMock() transcript = MagicMock(raw_text="hello", segments_json=[]) session.execute = AsyncMock(return_value=MagicMock(scalar_one=lambda: transcript)) deps = PipelineDeps( extract_captions=AsyncMock(), analyze=AsyncMock(side_effect=ValueError("LLM exploded")), daily_cost_used=AsyncMock(return_value=Decimal("0")), model="x", prompt_version="v1", daily_cost_cap_usd=Decimal("5"), workdir="/tmp", ) new_status = await process_one_video(video, session, deps) assert new_status == "captioned" assert video.retry_count == 1 async def test_process_one_video_llm_error_marks_failed_after_3_retries(): """After 3 retries, the video is marked failed.""" video = MagicMock(id=1, youtube_video_id="vid", status="captioned", retry_count=2) session = AsyncMock() transcript = MagicMock(raw_text="hello", segments_json=[]) session.execute = AsyncMock(return_value=MagicMock(scalar_one=lambda: transcript)) deps = PipelineDeps( extract_captions=AsyncMock(), analyze=AsyncMock(side_effect=RuntimeError("API down")), daily_cost_used=AsyncMock(return_value=Decimal("0")), model="x", prompt_version="v1", daily_cost_cap_usd=Decimal("5"), workdir="/tmp", ) new_status = await process_one_video(video, session, deps) assert new_status == "failed" assert "RuntimeError" in video.failure_reason