Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
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
Copy link

Copilot AI Sep 7, 2025

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.

Suggested change
- run: pip install -r requirements-dev.txt
- run: if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi

Copilot uses AI. Check for mistakes.
- run: pytest
Comment on lines +11 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Install base requirements before running pytest

The CI workflow only installs requirements-dev.txt before executing pytest. That file contains tooling dependencies but omits the project’s runtime libraries such as numpy, requests, and psutil defined in requirements.txt. On a clean GitHub runner these packages are absent, so importing modules inside the tests will raise ModuleNotFoundError and the CI job will fail immediately. Please install the core requirements (e.g. pip install -r requirements.txt) before or in addition to the dev extras.

Useful? React with 👍 / 👎.

7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ run-enhanced:
$(ACT); OMP_NUM_THREADS=$(LLAMA_THREADS) $(PY) src/macbot/voice_assistant.py

run-orchestrator:
$(ACT); $(PY) src/macbot/orchestrator.py
$(ACT); $(PY) src/macbot/orchestrator.py

test:
$(ACT); pytest

clean:
rm -rf $(VENV) $(WHISPER_DIR) $(LLAMA_DIR) __pycache__ */__pycache__
rm -rf $(VENV) $(WHISPER_DIR) $(LLAMA_DIR) __pycache__ */__pycache__
166 changes: 46 additions & 120 deletions tests/test_interruptible_conversation.py
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
Copy link

Copilot AI Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module stubbing with sys.modules is duplicated across test files. Consider moving this to a shared test utility or conftest.py to reduce duplication.

Copilot uses AI. Check for mistakes.

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
114 changes: 53 additions & 61 deletions tests/test_message_bus.py
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']
40 changes: 40 additions & 0 deletions tests/test_tool_calling.py
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
Copy link

Copilot AI Sep 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module stubbing with sys.modules is duplicated across test files. Consider moving this to a shared test utility or conftest.py to reduce duplication.

Copilot uses AI. Check for mistakes.

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()
Loading
Loading