Files
discord-meeting-bot/tests/test_openrouter_client.py
T

229 lines
8.5 KiB
Python

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