import asyncio import pydantic import pytest from app.errors import AnalysisNotFoundError, InvalidTranscriptError, LLMCompletionError from app.ports import LLm from app.repositories import InMemoryTranscriptAnalysisRepository from app.services import TranscriptAnalysisService class FakeLLM(LLm): def __init__(self) -> None: self.user_prompts: list[str] = [] self.async_user_prompts: list[str] = [] def run_completion( self, system_prompt: str, user_prompt: str, dto: type[pydantic.BaseModel], ) -> pydantic.BaseModel: self.user_prompts.append(user_prompt) return dto(summary="A concise summary.", action_items=["Follow up", "Share notes"]) async def run_completion_async( self, system_prompt: str, user_prompt: str, dto: type[pydantic.BaseModel], ) -> pydantic.BaseModel: self.async_user_prompts.append(user_prompt) return dto(summary="A concise summary.", action_items=["Follow up", "Share notes"]) class FailingLLM(LLm): def run_completion( self, system_prompt: str, user_prompt: str, dto: type[pydantic.BaseModel], ) -> pydantic.BaseModel: raise RuntimeError("provider unavailable") async def run_completion_async( self, system_prompt: str, user_prompt: str, dto: type[pydantic.BaseModel], ) -> pydantic.BaseModel: raise RuntimeError("provider unavailable") def build_service(llm: LLm | None = None) -> TranscriptAnalysisService: return TranscriptAnalysisService(llm or FakeLLM(), InMemoryTranscriptAnalysisRepository()) def test_analyze_returns_and_persists_result() -> None: llm = FakeLLM() service = build_service(llm) analysis = service.analyze(" Discuss launch plan. ") assert analysis.id assert analysis.summary == "A concise summary." assert analysis.action_items == ("Follow up", "Share notes") assert service.get(analysis.id) == analysis assert "Discuss launch plan." in llm.user_prompts[0] def test_analyze_rejects_empty_transcript() -> None: service = build_service() with pytest.raises(InvalidTranscriptError): service.analyze(" ") def test_get_raises_when_analysis_is_missing() -> None: service = build_service() with pytest.raises(AnalysisNotFoundError): service.get("missing-id") def test_analyze_many_processes_and_persists_all_results() -> None: llm = FakeLLM() service = build_service(llm) analyses = asyncio.run(service.analyze_many(["First transcript", "Second transcript"])) assert len(analyses) == 2 assert len({analysis.id for analysis in analyses}) == 2 assert [service.get(analysis.id) for analysis in analyses] == analyses assert len(llm.async_user_prompts) == 2 assert llm.user_prompts == [] def test_analyze_many_rejects_empty_list() -> None: service = build_service() with pytest.raises(InvalidTranscriptError): asyncio.run(service.analyze_many([])) def test_llm_errors_are_wrapped() -> None: service = build_service(FailingLLM()) with pytest.raises(LLMCompletionError): service.analyze("Discuss launch plan.") def test_analyze_many_wraps_llm_errors() -> None: service = build_service(FailingLLM()) with pytest.raises(LLMCompletionError): asyncio.run(service.analyze_many(["Discuss launch plan."]))