-
Notifications
You must be signed in to change notification settings - Fork 0
Add pytest-based tests and CI integration #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| push: | ||
| pull_request: | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-python@v4 | ||
| with: | ||
| python-version: '3.11' | ||
| - run: python -m pip install --upgrade pip | ||
| - run: pip install -r requirements-dev.txt | ||
| - run: pytest | ||
|
Comment on lines
+11
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Install base requirements before running pytest The CI workflow only installs Useful? React with 👍 / 👎. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,142 +1,68 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Test script for interruptible conversation system | ||
| """ | ||
|
|
||
| import sys | ||
| import os | ||
| import sys | ||
| import time | ||
| import threading | ||
| import numpy as np | ||
| import pytest | ||
| import types | ||
|
|
||
| # Ensure the package can be imported without installation | ||
| sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src')) | ||
|
|
||
| # Provide stub modules for optional audio dependencies | ||
| sys.modules.setdefault('sounddevice', types.SimpleNamespace(OutputStream=object)) | ||
| sys.modules.setdefault('soundfile', types.SimpleNamespace()) | ||
|
Comment on lines
+13
to
+14
|
||
|
|
||
| from macbot.audio_interrupt import AudioInterruptHandler | ||
| from macbot.conversation_manager import ConversationManager, ConversationState | ||
| import numpy as np | ||
| import time | ||
| from macbot.conversation_manager import ConversationManager, ConversationState, ResponseState | ||
|
|
||
| def test_audio_interruption(): | ||
| """Test basic audio interruption functionality""" | ||
| print("🧪 Testing Audio Interruption System...") | ||
|
|
||
| # Create audio handler | ||
|
|
||
| def test_audio_interruption(monkeypatch): | ||
| """Audio playback can be interrupted.""" | ||
| handler = AudioInterruptHandler(sample_rate=24000) | ||
|
|
||
| # Generate test audio (1 second of 440Hz sine wave) | ||
| duration = 1.0 | ||
| frequency = 440.0 | ||
| t = np.linspace(0, duration, int(24000 * duration), False) | ||
| audio = np.sin(frequency * 2 * np.pi * t).astype(np.float32) | ||
| def fake_worker(self): | ||
| while self.is_playing and not self.interrupt_requested: | ||
| time.sleep(0.01) | ||
| self.is_playing = False | ||
|
|
||
| print("▶️ Playing test audio...") | ||
| success = handler.play_audio(audio) | ||
| print(f"✅ Audio playback {'completed' if success else 'was interrupted'}") | ||
| monkeypatch.setattr(AudioInterruptHandler, '_playback_worker', fake_worker) | ||
| audio = np.zeros(2400, dtype=np.float32) | ||
| result = {} | ||
|
|
||
| return success | ||
| def run_play(): | ||
| result['completed'] = handler.play_audio(audio) | ||
|
|
||
| def test_conversation_manager(): | ||
| """Test conversation state management""" | ||
| print("\n🧪 Testing Conversation Manager...") | ||
| t = threading.Thread(target=run_play) | ||
| t.start() | ||
| time.sleep(0.05) | ||
| handler.interrupt_playback() | ||
| t.join() | ||
|
|
||
| manager = ConversationManager() | ||
| assert result['completed'] is False | ||
| status = handler.get_playback_status() | ||
| assert not status['is_playing'] | ||
| assert status['interrupt_requested'] | ||
|
|
||
| # Test state transitions | ||
| print("📝 Testing conversation states...") | ||
|
|
||
| # Start conversation | ||
| def test_conversation_state_transitions(): | ||
| """Conversation manager handles state transitions and buffering.""" | ||
| manager = ConversationManager() | ||
| manager.lock = threading.RLock() | ||
| conv_id = manager.start_conversation() | ||
| print(f"📊 Started conversation: {conv_id}") | ||
|
|
||
| # Add user input | ||
| manager.add_user_input("Hello MacBot") | ||
| print("📝 Added user input") | ||
| assert manager.current_context.conversation_id == conv_id | ||
|
|
||
| # Start response | ||
| manager.start_response() | ||
| print("🎤 Started response") | ||
| manager.add_user_input('Hello') | ||
| manager.start_response('Hi there') | ||
| assert manager.current_context.current_state == ConversationState.SPEAKING | ||
|
|
||
| # Simulate interruption | ||
| manager.interrupt_response() | ||
| print("⏹️ Interrupted response") | ||
| assert manager.current_context.current_state == ConversationState.INTERRUPTED | ||
|
|
||
| # Check if we can resume | ||
| buffered = manager.resume_response() | ||
| print(f"📊 Buffered response available: {buffered is not None}") | ||
| assert buffered == 'Hi there' | ||
|
|
||
| # Complete response | ||
| manager.complete_response() | ||
| print("✅ Completed response") | ||
|
|
||
| # Check history | ||
| history = manager.get_recent_history() | ||
| print(f"📚 Conversation history: {len(history)} messages") | ||
|
|
||
| return len(history) > 0 | ||
|
|
||
| def test_integration(): | ||
| """Test integrated audio interruption with conversation management""" | ||
| print("\n🧪 Testing Integrated System...") | ||
|
|
||
| # Create components | ||
| audio_handler = AudioInterruptHandler(sample_rate=24000) | ||
| conversation_manager = ConversationManager() | ||
|
|
||
| # Register callback | ||
| def on_state_change(context): | ||
| print(f"🔄 State changed to: {context.current_state.value}") | ||
| if context.current_state == ConversationState.INTERRUPTED: | ||
| audio_handler.interrupt_playback() | ||
|
|
||
| conversation_manager.register_state_callback(on_state_change) | ||
|
|
||
| # Generate longer test audio (3 seconds) | ||
| duration = 3.0 | ||
| frequency = 440.0 | ||
| t = np.linspace(0, duration, int(24000 * duration), False) | ||
| audio = np.sin(frequency * 2 * np.pi * t).astype(np.float32) | ||
|
|
||
| print("▶️ Starting integrated test...") | ||
|
|
||
| # Start conversation | ||
| conv_id = conversation_manager.start_conversation() | ||
| conversation_manager.add_user_input("Test message") | ||
| conversation_manager.start_response() | ||
|
|
||
| # Play audio (should complete normally) | ||
| success = audio_handler.play_audio(audio) | ||
|
|
||
| # Complete response | ||
| conversation_manager.complete_response() | ||
|
|
||
| print(f"✅ Integrated test {'completed successfully' if success else 'was interrupted'}") | ||
|
|
||
| return success | ||
|
|
||
| def main(): | ||
| """Run all tests""" | ||
| print("🚀 MacBot Interruptible Conversation System Test Suite") | ||
| print("=" * 60) | ||
|
|
||
| try: | ||
| # Test individual components | ||
| audio_test = test_audio_interruption() | ||
| conv_test = test_conversation_manager() | ||
| integration_test = test_integration() | ||
|
|
||
| # Summary | ||
| print("\n" + "=" * 60) | ||
| print("📊 Test Results:") | ||
| print(f" Audio Interruption: {'✅ PASS' if audio_test else '❌ FAIL'}") | ||
| print(f" Conversation Manager: {'✅ PASS' if conv_test else '❌ FAIL'}") | ||
| print(f" Integration Test: {'✅ PASS' if integration_test else '❌ FAIL'}") | ||
|
|
||
| all_passed = audio_test and conv_test and integration_test | ||
| print(f"\n🎯 Overall: {'✅ ALL TESTS PASSED' if all_passed else '❌ SOME TESTS FAILED'}") | ||
|
|
||
| return 0 if all_passed else 1 | ||
|
|
||
| except Exception as e: | ||
| print(f"❌ Test suite failed with error: {e}") | ||
| import traceback | ||
| traceback.print_exc() | ||
| return 1 | ||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
| assert manager.current_context.current_state == ConversationState.IDLE | ||
| assert len(manager.get_recent_history()) == 2 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,71 +1,63 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Test script for MacBot Message Bus System | ||
| """ | ||
| import sys | ||
| import os | ||
| import sys | ||
| import time | ||
| import pytest | ||
|
|
||
| # Ensure the package can be imported without installation | ||
| sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src')) | ||
|
|
||
| from macbot.message_bus import MessageBus, start_message_bus, stop_message_bus | ||
| from macbot.message_bus import start_message_bus, stop_message_bus | ||
| import macbot.message_bus_client as mb_client | ||
| from macbot.message_bus_client import MessageBusClient | ||
|
|
||
| def test_message_bus(): | ||
| """Test the message bus system""" | ||
| print("🧪 Testing MacBot Message Bus System...") | ||
|
|
||
| # Start message bus | ||
| print("1. Starting message bus server...") | ||
| @pytest.fixture | ||
| def message_bus(): | ||
| bus = start_message_bus(host='localhost', port=8082) | ||
|
|
||
| # Create test clients | ||
| print("2. Creating test clients...") | ||
| client1 = MessageBusClient( | ||
| host='localhost', | ||
| port=8082, | ||
| service_type='test_service_1' | ||
| ) | ||
| client2 = MessageBusClient( | ||
| host='localhost', | ||
| port=8082, | ||
| service_type='test_service_2' | ||
| ) | ||
|
|
||
| # Start clients | ||
| print("3. Starting clients...") | ||
| client1.start() | ||
| client2.start() | ||
|
|
||
| # Wait for connections | ||
| time.sleep(3) | ||
|
|
||
| # Test message sending | ||
| print("4. Testing message exchange...") | ||
|
|
||
| # Client 1 sends message | ||
| client1.send_message({ | ||
| 'type': 'test_message', | ||
| 'content': 'Hello from client 1!', | ||
| 'timestamp': time.time() | ||
| }) | ||
|
|
||
| # Client 2 sends message | ||
| client2.send_message({ | ||
| 'type': 'test_message', | ||
| 'content': 'Hello from client 2!', | ||
| 'timestamp': time.time() | ||
| }) | ||
|
|
||
| # Wait for messages to be processed | ||
| time.sleep(2) | ||
|
|
||
| print("5. Test completed successfully!") | ||
| print("✅ Message bus system is working properly") | ||
|
|
||
| # Cleanup | ||
| client1.stop() | ||
| client2.stop() | ||
| mb_client.message_bus = bus | ||
| yield bus | ||
| stop_message_bus() | ||
|
|
||
| if __name__ == "__main__": | ||
| test_message_bus() | ||
|
|
||
| @pytest.fixture | ||
| def create_client(message_bus): | ||
| clients = [] | ||
|
|
||
| def _factory(service_type: str): | ||
| client = MessageBusClient(host='localhost', port=8082, service_type=service_type) | ||
| client.start() | ||
| clients.append(client) | ||
| time.sleep(0.1) # allow connection | ||
| return client | ||
|
|
||
| yield _factory | ||
|
|
||
| for c in clients: | ||
| c.stop() | ||
|
|
||
|
|
||
| def test_message_delivery(create_client): | ||
| """Clients should receive messages sent on the bus.""" | ||
| received = [] | ||
|
|
||
| client2 = create_client('service2') | ||
| client2.register_handler('test', lambda m: received.append(m)) | ||
|
|
||
| client1 = create_client('service1') | ||
| client1.send_message({'type': 'test', 'content': 'hello'}) | ||
|
|
||
| timeout = time.time() + 2 | ||
| while not received and time.time() < timeout: | ||
| time.sleep(0.05) | ||
|
|
||
| assert received and received[0]['content'] == 'hello' | ||
|
|
||
|
|
||
| def test_service_status(message_bus, create_client): | ||
| """Message bus should track registered services.""" | ||
| client = create_client('network_service') | ||
| time.sleep(0.1) | ||
| status = message_bus.get_service_status() | ||
| assert 'network_service' in status | ||
| assert status['network_service']['count'] == 1 | ||
| assert client.client_id in status['network_service']['clients'] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import os | ||
| import sys | ||
| import pytest | ||
| import types | ||
|
|
||
| # Ensure package import without installation | ||
| sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src')) | ||
|
|
||
| # Provide stubs for optional dependencies used by voice_assistant | ||
| sys.modules.setdefault('sounddevice', types.SimpleNamespace()) | ||
| sys.modules.setdefault('soundfile', types.SimpleNamespace()) | ||
| sys.modules.setdefault('psutil', types.SimpleNamespace()) | ||
| sys.modules.setdefault('requests', types.SimpleNamespace(get=lambda *a, **k: None, post=lambda *a, **k: None)) | ||
|
Comment on lines
+10
to
+13
|
||
|
|
||
| from macbot.voice_assistant import ToolCaller, tools | ||
|
|
||
|
|
||
| def test_tool_caller_web_search(monkeypatch): | ||
| caller = ToolCaller() | ||
| called = {} | ||
|
|
||
| def fake_search(query): | ||
| called['query'] = query | ||
| return 'ok' | ||
|
|
||
| monkeypatch.setattr(tools, 'web_search', fake_search) | ||
| result = caller.web_search('python') | ||
| assert result == 'ok' | ||
| assert called['query'] == 'python' | ||
|
|
||
|
|
||
| def test_tool_caller_error_handling(monkeypatch): | ||
| caller = ToolCaller() | ||
|
|
||
| def boom(query): | ||
| raise RuntimeError('fail') | ||
|
|
||
| monkeypatch.setattr(tools, 'web_search', boom) | ||
| result = caller.web_search('python') | ||
| assert "couldn't perform" in result.lower() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CI workflow references 'requirements-dev.txt' but this file is not included in the PR. This will cause the CI build to fail if the file doesn't exist.