Spaces:
Sleeping
Sleeping
| """ | |
| Unit tests for the open_notebook.domain module. | |
| This test suite focuses on validation logic, business rules, and data structures | |
| that can be tested without database mocking. | |
| """ | |
| import pytest | |
| from pydantic import ValidationError | |
| from open_notebook.domain.base import RecordModel | |
| from open_notebook.domain.content_settings import ContentSettings | |
| from open_notebook.domain.models import ModelManager | |
| from open_notebook.domain.notebook import Note, Notebook, Source | |
| from open_notebook.domain.podcast import EpisodeProfile, SpeakerProfile | |
| from open_notebook.domain.transformation import Transformation | |
| from open_notebook.exceptions import InvalidInputError | |
| # ============================================================================ | |
| # TEST SUITE 1: RecordModel Singleton Pattern | |
| # ============================================================================ | |
| class TestRecordModelSingleton: | |
| """Test suite for RecordModel singleton behavior.""" | |
| def test_recordmodel_singleton_behavior(self): | |
| """Test that same instance is returned for same record_id.""" | |
| class TestRecord(RecordModel): | |
| record_id = "test:singleton" | |
| value: int = 0 | |
| # Clear any existing instance | |
| TestRecord.clear_instance() | |
| # Create first instance | |
| instance1 = TestRecord(value=42) | |
| assert instance1.value == 42 | |
| # Create second instance - should return same object | |
| instance2 = TestRecord(value=99) | |
| assert instance1 is instance2 | |
| assert instance2.value == 99 # Value was updated | |
| # Cleanup | |
| TestRecord.clear_instance() | |
| # ============================================================================ | |
| # TEST SUITE 2: ModelManager Instance Isolation | |
| # ============================================================================ | |
| class TestModelManager: | |
| """Test suite for ModelManager instance behavior.""" | |
| def test_model_manager_instance_isolation(self): | |
| """Test that each ModelManager instance is independent (not a singleton).""" | |
| manager1 = ModelManager() | |
| manager2 = ModelManager() | |
| # Each instance should be independent (not a singleton) | |
| assert manager1 is not manager2 | |
| assert id(manager1) != id(manager2) | |
| # ============================================================================ | |
| # TEST SUITE 3: Notebook Domain Logic | |
| # ============================================================================ | |
| class TestNotebookDomain: | |
| """Test suite for Notebook validation and business rules.""" | |
| def test_notebook_name_validation(self): | |
| """Test empty/whitespace names are rejected.""" | |
| # Empty name should raise error | |
| with pytest.raises(InvalidInputError, match="Notebook name cannot be empty"): | |
| Notebook(name="", description="Test") | |
| # Whitespace-only name should raise error | |
| with pytest.raises(InvalidInputError, match="Notebook name cannot be empty"): | |
| Notebook(name=" ", description="Test") | |
| # Valid name should work | |
| notebook = Notebook(name="Valid Name", description="Test") | |
| assert notebook.name == "Valid Name" | |
| def test_notebook_archived_flag(self): | |
| """Test archived flag defaults to False.""" | |
| notebook = Notebook(name="Test", description="Test") | |
| assert notebook.archived is False | |
| notebook_archived = Notebook(name="Test", description="Test", archived=True) | |
| assert notebook_archived.archived is True | |
| # ============================================================================ | |
| # TEST SUITE 4: Source Domain | |
| # ============================================================================ | |
| class TestSourceDomain: | |
| """Test suite for Source domain model.""" | |
| def test_source_command_field_parsing(self): | |
| """Test RecordID parsing for command field.""" | |
| # Test with string command | |
| source = Source(title="Test", command="command:123") | |
| assert source.command is not None | |
| # Test with None command | |
| source2 = Source(title="Test", command=None) | |
| assert source2.command is None | |
| # Test command is included in save data prep | |
| source3 = Source(id="source:123", title="Test", command="command:456") | |
| save_data = source3._prepare_save_data() | |
| assert "command" in save_data | |
| # ============================================================================ | |
| # TEST SUITE 5: Note Domain | |
| # ============================================================================ | |
| class TestNoteDomain: | |
| """Test suite for Note validation.""" | |
| def test_note_content_validation(self): | |
| """Test empty content is rejected.""" | |
| # None content is allowed | |
| note = Note(title="Test", content=None) | |
| assert note.content is None | |
| # Non-empty content is valid | |
| note2 = Note(title="Test", content="Valid content") | |
| assert note2.content == "Valid content" | |
| # Empty string should raise error | |
| with pytest.raises(InvalidInputError, match="Note content cannot be empty"): | |
| Note(title="Test", content="") | |
| # Whitespace-only should raise error | |
| with pytest.raises(InvalidInputError, match="Note content cannot be empty"): | |
| Note(title="Test", content=" ") | |
| def test_note_embedding_enabled(self): | |
| """Test notes have embedding enabled by default.""" | |
| note = Note(title="Test", content="Test content") | |
| assert note.needs_embedding() is True | |
| assert note.get_embedding_content() == "Test content" | |
| # Test with None content | |
| note2 = Note(title="Test", content=None) | |
| assert note2.get_embedding_content() is None | |
| # ============================================================================ | |
| # TEST SUITE 6: Podcast Domain Validation | |
| # ============================================================================ | |
| class TestPodcastDomain: | |
| """Test suite for Podcast domain validation.""" | |
| def test_speaker_profile_validation(self): | |
| """Test speaker profile validates count and required fields.""" | |
| # Test invalid - no speakers | |
| with pytest.raises(ValidationError): | |
| SpeakerProfile( | |
| name="Test", | |
| tts_provider="openai", | |
| tts_model="tts-1", | |
| speakers=[], | |
| ) | |
| # Test invalid - too many speakers (> 4) | |
| with pytest.raises(ValidationError): | |
| SpeakerProfile( | |
| name="Test", | |
| tts_provider="openai", | |
| tts_model="tts-1", | |
| speakers=[{"name": f"Speaker{i}"} for i in range(5)], | |
| ) | |
| # Test invalid - missing required fields | |
| with pytest.raises(ValidationError): | |
| SpeakerProfile( | |
| name="Test", | |
| tts_provider="openai", | |
| tts_model="tts-1", | |
| speakers=[{"name": "Speaker 1"}], # Missing voice_id, backstory, personality | |
| ) | |
| # Test valid - single speaker with all fields | |
| profile = SpeakerProfile( | |
| name="Test", | |
| tts_provider="openai", | |
| tts_model="tts-1", | |
| speakers=[ | |
| { | |
| "name": "Host", | |
| "voice_id": "voice123", | |
| "backstory": "A friendly host", | |
| "personality": "Enthusiastic and welcoming", | |
| } | |
| ], | |
| ) | |
| assert len(profile.speakers) == 1 | |
| assert profile.speakers[0]["name"] == "Host" | |
| # ============================================================================ | |
| # TEST SUITE 7: Transformation Domain | |
| # ============================================================================ | |
| class TestTransformationDomain: | |
| """Test suite for Transformation domain model.""" | |
| def test_transformation_creation(self): | |
| """Test transformation model creation.""" | |
| transform = Transformation( | |
| name="summarize", | |
| title="Summarize Content", | |
| description="Creates a summary", | |
| prompt="Summarize the following text: {content}", | |
| apply_default=True, | |
| ) | |
| assert transform.name == "summarize" | |
| assert transform.apply_default is True | |
| # ============================================================================ | |
| # TEST SUITE 8: Content Settings | |
| # ============================================================================ | |
| class TestContentSettings: | |
| """Test suite for ContentSettings defaults.""" | |
| def test_content_settings_defaults(self): | |
| """Test ContentSettings has proper defaults.""" | |
| settings = ContentSettings() | |
| assert settings.record_id == "open_notebook:content_settings" | |
| assert settings.default_content_processing_engine_doc == "auto" | |
| assert settings.default_embedding_option == "ask" | |
| assert settings.auto_delete_files == "yes" | |
| assert len(settings.youtube_preferred_languages) > 0 | |
| # ============================================================================ | |
| # TEST SUITE 9: Episode Profile Validation | |
| # ============================================================================ | |
| class TestEpisodeProfile: | |
| """Test suite for EpisodeProfile validation.""" | |
| def test_episode_profile_segment_validation(self): | |
| """Test segment count validation (3-20).""" | |
| # Test invalid - too few segments | |
| with pytest.raises(ValidationError, match="Number of segments must be between 3 and 20"): | |
| EpisodeProfile( | |
| name="Test", | |
| speaker_config="default", | |
| outline_provider="openai", | |
| outline_model="gpt-4", | |
| transcript_provider="openai", | |
| transcript_model="gpt-4", | |
| default_briefing="Test briefing", | |
| num_segments=2, | |
| ) | |
| # Test invalid - too many segments | |
| with pytest.raises(ValidationError, match="Number of segments must be between 3 and 20"): | |
| EpisodeProfile( | |
| name="Test", | |
| speaker_config="default", | |
| outline_provider="openai", | |
| outline_model="gpt-4", | |
| transcript_provider="openai", | |
| transcript_model="gpt-4", | |
| default_briefing="Test briefing", | |
| num_segments=21, | |
| ) | |
| # Test valid segment count | |
| profile = EpisodeProfile( | |
| name="Test", | |
| speaker_config="default", | |
| outline_provider="openai", | |
| outline_model="gpt-4", | |
| transcript_provider="openai", | |
| transcript_model="gpt-4", | |
| default_briefing="Test briefing", | |
| num_segments=5, | |
| ) | |
| assert profile.num_segments == 5 | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) | |