Files

418 lines
17 KiB
Python
Raw Permalink Normal View History

"""
Unit tests for generate_summary.py module
"""
import pytest
import os
import json
from unittest.mock import Mock, MagicMock, patch, call
from pydantic import ValidationError
# Import the functions and models to test
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from scripts.generate_summary import (
general_summary,
custom_summary,
BasicSummary,
AdvancedSummary,
KeyPoint,
Summary,
Purpose,
Chapters,
Outcomes,
ActionItemsPerUser
)
class TestPydanticModels:
"""Test Pydantic model validation"""
def test_key_point_model(self):
"""Test KeyPoint model validation"""
key_point = KeyPoint(text="Test point", timestamp=10.5)
assert key_point.text == "Test point"
assert key_point.timestamp == 10.5
def test_summary_model(self):
"""Test Summary model validation"""
summary = Summary(text="Test summary", duration_minutes=15.5)
assert summary.text == "Test summary"
assert summary.duration_minutes == 15.5
def test_basic_summary_model(self, sample_basic_summary_response):
"""Test BasicSummary model validation"""
basic_summary = BasicSummary(**sample_basic_summary_response)
assert len(basic_summary.Key_Points) == 2
assert basic_summary.Summary.text is not None
assert basic_summary.Summary.duration_minutes > 0
def test_advanced_summary_model(self, sample_advanced_summary_response):
"""Test AdvancedSummary model validation"""
advanced_summary = AdvancedSummary(**sample_advanced_summary_response)
assert advanced_summary.Purpose.text is not None
assert len(advanced_summary.Chapters.content) > 0
assert len(advanced_summary.Outcomes.content) > 0
assert len(advanced_summary.Action_Items_Per_User) > 0
class TestGeneralSummary:
"""Test general_summary function"""
@patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'})
@patch('scripts.generate_summary.ChatAnthropic')
@patch('scripts.generate_summary.ChatPromptTemplate')
def test_general_summary_freemium_plan(
self,
mock_prompt_template,
mock_chat_anthropic,
sample_transcription_dict,
sample_basic_summary_response
):
"""Test general_summary with freemium plan"""
# Setup mocks
mock_model = MagicMock()
mock_chain = MagicMock()
mock_structured_model = MagicMock()
# Create a mock BasicSummary instance
from scripts.generate_summary import BasicSummary
mock_result = BasicSummary(**sample_basic_summary_response)
mock_structured_model.invoke.return_value = mock_result
mock_model.with_structured_output.return_value = mock_structured_model
mock_prompt_template.from_messages.return_value = MagicMock()
mock_chat_anthropic.return_value = mock_model
# Mock the pipe operator
mock_prompt_instance = MagicMock()
mock_prompt_template.from_messages.return_value = mock_prompt_instance
mock_prompt_instance.__or__ = lambda self, other: mock_chain
mock_chain.invoke.return_value = mock_result
# Call the function
result = general_summary(sample_transcription_dict, plan_tier="freemium")
# Verify results
assert result is not None
assert "Key_Points" in result
assert "Summary" in result
assert len(result["Key_Points"]) > 0
# Verify model was initialized with correct parameters
mock_chat_anthropic.assert_called_once()
call_args = mock_chat_anthropic.call_args
assert call_args.kwargs["model"] == "claude-sonnet-4-5-20250929"
assert call_args.kwargs["max_tokens"] == 2000
assert call_args.kwargs["temperature"] == 0.2
@patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'})
@patch('scripts.generate_summary.ChatAnthropic')
@patch('scripts.generate_summary.ChatPromptTemplate')
def test_general_summary_pro_plan(
self,
mock_prompt_template,
mock_chat_anthropic,
sample_transcription_dict,
sample_advanced_summary_response
):
"""Test general_summary with pro plan"""
# Setup mocks
mock_model = MagicMock()
mock_chain = MagicMock()
# Create a mock AdvancedSummary instance
from scripts.generate_summary import AdvancedSummary
mock_result = AdvancedSummary(**sample_advanced_summary_response)
mock_structured_model = MagicMock()
mock_structured_model.invoke.return_value = mock_result
mock_model.with_structured_output.return_value = mock_structured_model
mock_prompt_template.from_messages.return_value = MagicMock()
mock_chat_anthropic.return_value = mock_model
# Mock the pipe operator
mock_prompt_instance = MagicMock()
mock_prompt_template.from_messages.return_value = mock_prompt_instance
mock_prompt_instance.__or__ = lambda self, other: mock_chain
mock_chain.invoke.return_value = mock_result
# Call the function
result = general_summary(sample_transcription_dict, plan_tier="pro")
# Verify results
assert result is not None
assert "Purpose" in result
assert "Chapters" in result
assert "Outcomes" in result
assert "Action_Items_Per_User" in result
# Verify model was initialized with correct parameters
mock_chat_anthropic.assert_called_once()
call_args = mock_chat_anthropic.call_args
assert call_args.kwargs["model"] == "claude-sonnet-4-5-20250929"
assert call_args.kwargs["max_tokens"] == 4000
@patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'})
@patch('scripts.generate_summary.ChatAnthropic')
@patch('scripts.generate_summary.ChatPromptTemplate')
def test_general_summary_with_string_transcription(
self,
mock_prompt_template,
mock_chat_anthropic,
sample_basic_summary_response
):
"""Test general_summary with string transcription"""
transcription_str = json.dumps({"sentences": []})
mock_model = MagicMock()
mock_chain = MagicMock()
from scripts.generate_summary import BasicSummary
mock_result = BasicSummary(**sample_basic_summary_response)
mock_structured_model = MagicMock()
mock_structured_model.invoke.return_value = mock_result
mock_model.with_structured_output.return_value = mock_structured_model
mock_prompt_template.from_messages.return_value = MagicMock()
mock_chat_anthropic.return_value = mock_model
mock_prompt_instance = MagicMock()
mock_prompt_template.from_messages.return_value = mock_prompt_instance
mock_prompt_instance.__or__ = lambda self, other: mock_chain
mock_chain.invoke.return_value = mock_result
result = general_summary(transcription_str, plan_tier="freemium")
assert result is not None
assert "Key_Points" in result
def test_general_summary_missing_api_key(self, sample_transcription_dict):
"""Test general_summary raises error when API key is missing"""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="ANTHROPIC_API_KEY"):
general_summary(sample_transcription_dict, plan_tier="freemium")
@patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'})
@patch('scripts.generate_summary.ChatAnthropic')
@patch('scripts.generate_summary.ChatPromptTemplate')
def test_general_summary_fallback_on_error(
self,
mock_prompt_template,
mock_chat_anthropic,
sample_transcription_dict,
sample_basic_summary_response
):
"""Test general_summary falls back to non-structured output on error"""
mock_model = MagicMock()
mock_chain = MagicMock()
mock_fallback_chain = MagicMock()
mock_response = MagicMock()
mock_response.content = json.dumps(sample_basic_summary_response)
# First call (structured) raises error
mock_structured_model = MagicMock()
mock_structured_model.invoke.side_effect = Exception("Structured output failed")
mock_model.with_structured_output.return_value = mock_structured_model
# Set up the chain so that structured chain raises exception
# and fallback chain returns the mock_response
mock_prompt_instance = MagicMock()
mock_prompt_template.from_messages.return_value = mock_prompt_instance
# When __or__ is called with structured_model, return chain that raises exception
# When __or__ is called with model (fallback), return fallback_chain that succeeds
def or_handler(self, other):
if other == mock_structured_model:
# Structured chain - should raise exception
mock_chain.invoke.side_effect = Exception("Structured output failed")
return mock_chain
elif other == mock_model:
# Fallback chain - should succeed
mock_fallback_chain.invoke.return_value = mock_response
return mock_fallback_chain
return mock_chain
mock_prompt_instance.__or__ = or_handler
mock_chat_anthropic.return_value = mock_model
result = general_summary(sample_transcription_dict, plan_tier="freemium")
assert result is not None
assert "Key_Points" in result
class TestCustomSummary:
"""Test custom_summary function"""
@patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'})
@patch('scripts.generate_summary.ChatAnthropic')
@patch('scripts.generate_summary.ChatPromptTemplate')
def test_custom_summary_success(
self,
mock_prompt_template,
mock_chat_anthropic,
sample_transcription_dict,
sample_template
):
"""Test custom_summary with successful response"""
mock_model = MagicMock()
mock_chain = MagicMock()
mock_response = MagicMock()
expected_result = {
"Key_Points": {"content": []},
"Summary": {"content": []},
"Next_Steps": {"content": []}
}
mock_response.content = json.dumps(expected_result)
mock_prompt_instance = MagicMock()
mock_prompt_template.from_messages.return_value = mock_prompt_instance
mock_prompt_instance.__or__ = lambda self, other: mock_chain
mock_chain.invoke.return_value = mock_response
mock_chat_anthropic.return_value = mock_model
result = custom_summary(sample_template, sample_transcription_dict)
assert result is not None
assert "Key_Points" in result or "Summary" in result
# Verify model was initialized correctly
mock_chat_anthropic.assert_called_once()
call_args = mock_chat_anthropic.call_args
assert call_args.kwargs["model"] == "claude-sonnet-4-5-20250929"
assert call_args.kwargs["max_tokens"] == 8000
@patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'})
@patch('scripts.generate_summary.ChatAnthropic')
@patch('scripts.generate_summary.ChatPromptTemplate')
def test_custom_summary_with_markdown_wrapper(
self,
mock_prompt_template,
mock_chat_anthropic,
sample_transcription_dict,
sample_template
):
"""Test custom_summary handles markdown-wrapped JSON"""
mock_model = MagicMock()
mock_chain = MagicMock()
mock_response = MagicMock()
expected_result = {"result": "test"}
wrapped_json = f"```json\n{json.dumps(expected_result)}\n```"
mock_response.content = wrapped_json
mock_prompt_instance = MagicMock()
mock_prompt_template.from_messages.return_value = mock_prompt_instance
mock_prompt_instance.__or__ = lambda self, other: mock_chain
mock_chain.invoke.return_value = mock_response
mock_chat_anthropic.return_value = mock_model
result = custom_summary(sample_template, sample_transcription_dict)
assert result == expected_result
def test_custom_summary_missing_api_key(self, sample_transcription_dict, sample_template):
"""Test custom_summary raises error when API key is missing"""
with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError, match="ANTHROPIC_API_KEY"):
custom_summary(sample_template, sample_transcription_dict)
@patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'})
@patch('scripts.generate_summary.ChatAnthropic')
@patch('scripts.generate_summary.ChatPromptTemplate')
def test_custom_summary_invalid_json(
self,
mock_prompt_template,
mock_chat_anthropic,
sample_transcription_dict,
sample_template
):
"""Test custom_summary handles invalid JSON gracefully"""
mock_model = MagicMock()
mock_chain = MagicMock()
mock_response = MagicMock()
mock_response.content = "This is not valid JSON"
mock_prompt_instance = MagicMock()
mock_prompt_template.from_messages.return_value = mock_prompt_instance
mock_prompt_instance.__or__ = lambda self, other: mock_chain
mock_chain.invoke.return_value = mock_response
mock_chat_anthropic.return_value = mock_model
with pytest.raises(ValueError, match="Could not parse response as JSON"):
custom_summary(sample_template, sample_transcription_dict)
class TestSchemaSwitching:
"""Test schema switching based on plan tier"""
@patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'})
@patch('scripts.generate_summary.ChatAnthropic')
@patch('scripts.generate_summary.ChatPromptTemplate')
def test_schema_switching_freemium(
self,
mock_prompt_template,
mock_chat_anthropic,
sample_transcription_dict,
sample_basic_summary_response
):
"""Test that freemium plan uses BasicSummary schema"""
mock_model = MagicMock()
mock_chain = MagicMock()
from scripts.generate_summary import BasicSummary
mock_result = BasicSummary(**sample_basic_summary_response)
mock_structured_model = MagicMock()
mock_structured_model.invoke.return_value = mock_result
mock_model.with_structured_output = MagicMock(return_value=mock_structured_model)
mock_prompt_instance = MagicMock()
mock_prompt_template.from_messages.return_value = mock_prompt_instance
mock_prompt_instance.__or__ = lambda self, other: mock_chain
mock_chain.invoke.return_value = mock_result
mock_chat_anthropic.return_value = mock_model
general_summary(sample_transcription_dict, plan_tier="freemium")
# Verify BasicSummary schema was used
mock_model.with_structured_output.assert_called_once()
call_args = mock_model.with_structured_output.call_args
from scripts.generate_summary import BasicSummary
assert call_args[0][0] == BasicSummary
@patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'})
@patch('scripts.generate_summary.ChatAnthropic')
@patch('scripts.generate_summary.ChatPromptTemplate')
def test_schema_switching_pro(
self,
mock_prompt_template,
mock_chat_anthropic,
sample_transcription_dict,
sample_advanced_summary_response
):
"""Test that pro plan uses AdvancedSummary schema"""
mock_model = MagicMock()
mock_chain = MagicMock()
from scripts.generate_summary import AdvancedSummary
mock_result = AdvancedSummary(**sample_advanced_summary_response)
mock_structured_model = MagicMock()
mock_structured_model.invoke.return_value = mock_result
mock_model.with_structured_output = MagicMock(return_value=mock_structured_model)
mock_prompt_instance = MagicMock()
mock_prompt_template.from_messages.return_value = mock_prompt_instance
mock_prompt_instance.__or__ = lambda self, other: mock_chain
mock_chain.invoke.return_value = mock_result
mock_chat_anthropic.return_value = mock_model
general_summary(sample_transcription_dict, plan_tier="pro")
# Verify AdvancedSummary schema was used
mock_model.with_structured_output.assert_called_once()
call_args = mock_model.with_structured_output.call_args
from scripts.generate_summary import AdvancedSummary
assert call_args[0][0] == AdvancedSummary