import asyncio import base64 import logging import os import sys from pathlib import Path sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) import openrouter_client from openrouter_client import ( SUMMARY_TRANSCRIPT_CHAR_LIMIT, TRANSCRIPTION_MODELS, _audio_format, _build_transcription_payload, _chat_completion, _chunk_text, _combined_summary_prompt, _summary_prompt, _transcribe_chunk, summarize, transcribe, ) def test_audio_format_defaults_to_wav_when_missing_suffix(tmp_path: Path): path = tmp_path / "recording" path.write_bytes(b"abc") assert _audio_format(str(path)) == "wav" def test_build_transcription_payload_uses_base64_json_shape(tmp_path: Path): path = tmp_path / "meeting.wav" path.write_bytes(b"RIFFdemo") payload = _build_transcription_payload(str(path), TRANSCRIPTION_MODELS[0]) assert payload["model"] == "openai/gpt-4o-mini-transcribe" assert payload["input_audio"]["format"] == "wav" assert payload["input_audio"]["data"] == base64.b64encode(b"RIFFdemo").decode("ascii") def test_chunk_text_splits_long_text_under_limit(): text = ("alpha beta gamma\n" * 1500).strip() chunks = _chunk_text(text, limit=SUMMARY_TRANSCRIPT_CHAR_LIMIT) assert len(chunks) > 1 assert all(len(chunk) <= SUMMARY_TRANSCRIPT_CHAR_LIMIT for chunk in chunks) assert "\n".join(chunks) == text def test_transcribe_combines_chunk_transcripts_and_logs_chunking(tmp_path: Path, monkeypatch, caplog): source = tmp_path / "meeting.wav" normalized = tmp_path / "normalized.wav" chunk1 = tmp_path / "chunk-000.wav" chunk2 = tmp_path / "chunk-001.wav" source.write_bytes(b"source") normalized.write_bytes(b"normalized") chunk1.write_bytes(b"c1") chunk2.write_bytes(b"c2") async def fake_normalize(audio_path: str) -> str: assert audio_path == str(source) return str(normalized) async def fake_split(audio_path: str) -> list[str]: assert audio_path == str(normalized) return [str(chunk1), str(chunk2)] seen = [] async def fake_transcribe_chunk(_client, audio_path: str, _headers: dict[str, str]) -> str: seen.append(Path(audio_path).name) return { "chunk-000.wav": "hello there", "chunk-001.wav": "general kenobi", }[Path(audio_path).name] monkeypatch.setattr(openrouter_client, "_normalize_audio_for_transcription", fake_normalize) monkeypatch.setattr(openrouter_client, "_split_audio_for_transcription", fake_split) monkeypatch.setattr(openrouter_client, "_transcribe_chunk", fake_transcribe_chunk) monkeypatch.setattr(openrouter_client, "_auth_headers", lambda: {"Authorization": "Bearer test"}) class DummyClient: async def __aenter__(self): return object() async def __aexit__(self, exc_type, exc, tb): return False monkeypatch.setattr(openrouter_client.httpx, "AsyncClient", lambda timeout: DummyClient()) with caplog.at_level(logging.INFO): result = asyncio.run(transcribe(str(source))) assert result == "hello there\n\ngeneral kenobi" assert seen == ["chunk-000.wav", "chunk-001.wav"] assert "Split normalized audio into 2 chunk(s)" in caplog.text def test_summarize_uses_multi_stage_pipeline_for_long_transcript(monkeypatch, caplog): long_transcript = ("decision item follow-up\n" * 4000).strip() prompts = [] async def fake_chat_completion(prompt: str, *, timeout: int = 180) -> str: prompts.append(prompt) if "Partial meeting transcript chunk" in prompt: return f"INTERMEDIATE-{len(prompts)}" return "FINAL SUMMARY" monkeypatch.setattr(openrouter_client, "_chat_completion", fake_chat_completion) with caplog.at_level(logging.INFO): result = asyncio.run(summarize(long_transcript)) assert result == "FINAL SUMMARY" assert len(prompts) >= 2 assert any("Partial meeting transcript chunk" in prompt for prompt in prompts[:-1]) assert "Combined chunk summaries" in prompts[-1] assert "Transcript exceeds single-pass summary limit" in caplog.text def test_summary_prompt_requires_requested_sections(): prompt = _summary_prompt("Alice: finished API work") assert "## What Was Done" in prompt assert "## What Will Be Done Next" in prompt assert "## Action Items" in prompt assert "## Decisions" in prompt assert "all dev team members' reports" in prompt def test_combined_summary_prompt_requires_requested_sections(): prompt = _combined_summary_prompt(["Chunk 1 summary", "Chunk 2 summary"]) assert "## What Was Done" in prompt assert "## What Will Be Done Next" in prompt assert "## Action Items" in prompt assert "## Decisions" in prompt assert "all dev team members' reports" in prompt def test_transcribe_chunk_falls_back_to_direct_openai_when_openrouter_fails(tmp_path: Path, monkeypatch): audio = tmp_path / "chunk-000.wav" audio.write_bytes(b"audio") calls = [] async def fake_openrouter(_client, audio_path: str, _headers: dict[str, str]) -> str: calls.append(("openrouter", Path(audio_path).name)) raise RuntimeError("OpenRouter transcription failed across all models: Cloudflare 502") async def fake_openai(_client, audio_path: str) -> str: calls.append(("openai", Path(audio_path).name)) return "direct provider transcript" monkeypatch.setattr(openrouter_client, "_transcribe_chunk_with_openrouter", fake_openrouter) monkeypatch.setattr(openrouter_client, "_transcribe_chunk_with_openai", fake_openai) monkeypatch.setattr(openrouter_client, "_openai_api_key", lambda: "sk-test") result = asyncio.run(_transcribe_chunk(object(), str(audio), {"Authorization": "Bearer test"})) assert result == "direct provider transcript" assert calls == [("openrouter", "chunk-000.wav"), ("openai", "chunk-000.wav")] def test_transcribe_chunk_reports_provider_fallback_when_all_providers_fail(tmp_path: Path, monkeypatch): audio = tmp_path / "chunk-000.wav" audio.write_bytes(b"audio") async def fake_openrouter(_client, _audio_path: str, _headers: dict[str, str]) -> str: raise RuntimeError("OpenRouter transcription failed across all models: Cloudflare 502") monkeypatch.setattr(openrouter_client, "_transcribe_chunk_with_openrouter", fake_openrouter) monkeypatch.setattr(openrouter_client, "_openai_api_key", lambda: "") try: asyncio.run(_transcribe_chunk(object(), str(audio), {"Authorization": "Bearer test"})) except RuntimeError as exc: message = str(exc) else: raise AssertionError("Expected transcription failure") assert "Transcription failed across providers" in message assert "OpenRouter transcription failed" in message assert "direct OpenAI fallback skipped" in message def test_chat_completion_falls_back_to_direct_openai_when_openrouter_returns_empty(monkeypatch): calls = [] async def fake_openrouter(prompt: str, *, timeout: int = 180) -> str: calls.append(("openrouter", prompt, timeout)) raise RuntimeError("OpenRouter summarization returned empty content") async def fake_openai(prompt: str, *, timeout: int = 180) -> str: calls.append(("openai", prompt, timeout)) return "direct summary" monkeypatch.setattr(openrouter_client, "_chat_completion_openrouter", fake_openrouter) monkeypatch.setattr(openrouter_client, "_chat_completion_openai", fake_openai) monkeypatch.setattr(openrouter_client, "_openai_api_key", lambda: "sk-test") result = asyncio.run(_chat_completion("summarize this", timeout=42)) assert result == "direct summary" assert calls == [ ("openrouter", "summarize this", 42), ("openai", "summarize this", 42), ] def test_chat_completion_reports_provider_fallback_when_all_summarizers_fail(monkeypatch): async def fake_openrouter(_prompt: str, *, timeout: int = 180) -> str: raise RuntimeError("OpenRouter summarization returned empty content") monkeypatch.setattr(openrouter_client, "_chat_completion_openrouter", fake_openrouter) monkeypatch.setattr(openrouter_client, "_openai_api_key", lambda: "") try: asyncio.run(_chat_completion("summarize this")) except RuntimeError as exc: message = str(exc) else: raise AssertionError("Expected summarization failure") assert "Summarization failed across providers" in message assert "OpenRouter summarization returned empty content" in message assert "direct OpenAI summarization fallback skipped" in message