#!/usr/bin/env python3 """ Test Suite for Dictation Service Tests dictation functionality and system tray integration """ import os import sys import unittest import tempfile from unittest.mock import Mock, patch, MagicMock # Mock GTK modules before importing sys.modules['gi'] = MagicMock() sys.modules['gi.repository'] = MagicMock() sys.modules['gi.repository.Gtk'] = MagicMock() sys.modules['gi.repository.AppIndicator3'] = MagicMock() sys.modules['gi.repository.GLib'] = MagicMock() # Add src to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) class TestDictationCore(unittest.TestCase): """Test core dictation functionality""" def setUp(self): """Setup test environment""" self.temp_dir = tempfile.mkdtemp() self.lock_file = os.path.join(self.temp_dir, "test_listening.lock") def tearDown(self): """Clean up test environment""" if os.path.exists(self.lock_file): os.remove(self.lock_file) try: os.rmdir(self.temp_dir) except: pass def test_can_import_dictation_service(self): """Test that main service can be imported""" try: from dictation_service import ai_dictation_simple self.assertTrue(hasattr(ai_dictation_simple, 'main')) self.assertTrue(hasattr(ai_dictation_simple, 'DictationTrayIcon')) except ImportError as e: self.fail(f"Cannot import dictation service: {e}") def test_spurious_word_filtering(self): """Test that spurious words are filtered""" from dictation_service.ai_dictation_simple import process_final_text # Mock subprocess.run to avoid actual typing with patch('subprocess.run'): # Single spurious word should be filtered process_final_text("the") # Should be filtered (single word) process_final_text("a") # Should be filtered # Multi-word with spurious words should have them removed # This is hard to test without capturing output, so just ensure no crash process_final_text("the hello world the") def test_lock_file_detection(self): """Test lock file creation and detection""" # Create lock file with open(self.lock_file, 'w') as f: f.write("") self.assertTrue(os.path.exists(self.lock_file)) # Remove lock file os.remove(self.lock_file) self.assertFalse(os.path.exists(self.lock_file)) @patch('subprocess.check_call') @patch('os.path.exists') def test_model_download(self, mock_exists, mock_check_call): """Test Vosk model download logic""" from dictation_service.ai_dictation_simple import download_model_if_needed # Mock model already exists mock_exists.return_value = True download_model_if_needed() mock_check_call.assert_not_called() class TestSystemTrayIcon(unittest.TestCase): """Test system tray icon functionality""" @patch('gi.repository.AppIndicator3.Indicator') @patch('gi.repository.Gtk.Menu') def test_tray_icon_creation(self, mock_menu, mock_indicator): """Test that tray icon can be created""" from dictation_service.ai_dictation_simple import DictationTrayIcon # This may fail if GTK is not available, which is okay try: tray = DictationTrayIcon() self.assertIsNotNone(tray) except Exception as e: # GTK not available in test environment is acceptable self.skipTest(f"GTK not available: {e}") def test_tray_toggle_creates_lock_file(self): """Test that tray icon toggle creates/removes lock file""" temp_lock = tempfile.mktemp(suffix='.lock') try: # Simulate creating lock file with open(temp_lock, 'w') as f: pass self.assertTrue(os.path.exists(temp_lock)) # Simulate removing lock file os.remove(temp_lock) self.assertFalse(os.path.exists(temp_lock)) finally: if os.path.exists(temp_lock): os.remove(temp_lock) class TestAudioProcessing(unittest.TestCase): """Test audio processing functionality""" def test_audio_callback_ignores_tts_lock(self): """Test that audio callback respects TTS lock file""" from dictation_service.ai_dictation_simple import audio_callback lock_file = "/tmp/dictation_speaking.lock" try: # Create TTS lock file with open(lock_file, 'w') as f: f.write("test") # Audio callback should ignore input when lock exists # This is hard to test without actual audio, so just ensure no crash mock_data = b'\x00' * 4000 audio_callback(mock_data, 4000, None, None) finally: if os.path.exists(lock_file): os.remove(lock_file) @patch('vosk.Model') @patch('vosk.KaldiRecognizer') def test_recognizer_initialization(self, mock_recognizer, mock_model): """Test that Vosk recognizer can be initialized""" # This tests the mocking setup, actual initialization requires model files mock_model.return_value = MagicMock() mock_recognizer.return_value = MagicMock() # Just ensure mocks work self.assertIsNotNone(mock_model) self.assertIsNotNone(mock_recognizer) if __name__ == '__main__': unittest.main()