Merge remote-tracking branch 'origin/dev' into feat/vad-agent

# Conflicts:
#	pyproject.toml
#	src/control_backend/main.py
#	uv.lock
This commit is contained in:
Twirre Meulenbelt
2025-10-28 10:44:03 +01:00
18 changed files with 271 additions and 95 deletions

View File

@@ -1,4 +1,4 @@
from unittest.mock import MagicMock, AsyncMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import zmq
@@ -20,7 +20,8 @@ def streaming(mocker):
@pytest.mark.asyncio
async def test_normal_setup(streaming):
"""
Test that during normal setup, the VAD agent creates a Streaming behavior and creates audio sockets.
Test that during normal setup, the VAD agent creates a Streaming behavior and creates audio
sockets.
"""
vad_agent = VADAgent("tcp://localhost:12345", False)
vad_agent.add_behaviour = MagicMock()
@@ -36,9 +37,10 @@ async def test_normal_setup(streaming):
@pytest.mark.parametrize("do_bind", [True, False])
def test_in_socket_creation(zmq_context, do_bind: bool):
"""
Test that the VAD agent creates an audio input socket, differentiating between binding and connecting.
Test that the VAD agent creates an audio input socket, differentiating between binding and
connecting.
"""
vad_agent = VADAgent(f"tcp://{"*" if do_bind else "localhost"}:12345", do_bind)
vad_agent = VADAgent(f"tcp://{'*' if do_bind else 'localhost'}:12345", do_bind)
vad_agent._connect_audio_in_socket()

View File

@@ -1,5 +1,5 @@
import os
from unittest.mock import MagicMock, AsyncMock
from unittest.mock import AsyncMock, MagicMock
import pytest
import soundfile as sf
@@ -17,7 +17,7 @@ def get_audio_chunks() -> list[bytes]:
chunks = []
with sf.SoundFile(file, 'r') as f:
with sf.SoundFile(file, "r") as f:
assert f.samplerate == 16000
assert f.channels == 1
assert f.subtype == "FLOAT"
@@ -54,4 +54,4 @@ async def test_real_audio(mocker):
audio_out_socket.send.assert_called()
for args in audio_out_socket.send.call_args_list:
assert isinstance(args[0][0], bytes)
assert len(args[0][0]) >= 512*4*3 # Should be at least 3 chunks of audio
assert len(args[0][0]) >= 512 * 4 * 3 # Should be at least 3 chunks of audio

View File

@@ -1,6 +1,6 @@
import json
import logging
from unittest.mock import MagicMock, AsyncMock, call
from unittest.mock import AsyncMock, MagicMock, call
import pytest
@@ -26,11 +26,11 @@ def belief_setter(mock_agent, mocker):
# Patch the settings to use a predictable agent name
mocker.patch(
"control_backend.agents.bdi.behaviours.belief_setter.settings.agent_settings.belief_collector_agent_name",
COLLECTOR_AGENT_NAME
COLLECTOR_AGENT_NAME,
)
# Patch asyncio.sleep to prevent tests from actually waiting
mocker.patch("asyncio.sleep", return_value=None)
setter = BeliefSetter()
setter.agent = mock_agent
# Mock the receive method, we will control its return value in each test
@@ -69,7 +69,7 @@ async def test_run_message_received(belief_setter, mocker):
Test that when a message is received, _process_message is called.
"""
# Arrange
msg = MagicMock();
msg = MagicMock()
belief_setter.receive.return_value = msg
mocker.patch.object(belief_setter, "_process_message")
@@ -115,14 +115,9 @@ def test_process_belief_message_valid_json(belief_setter, mocker):
Test processing a valid belief message with correct thread and JSON body.
"""
# Arrange
beliefs_payload = {
"is_hot": [["kitchen"]],
"is_clean": [["kitchen"], ["bathroom"]]
}
beliefs_payload = {"is_hot": [["kitchen"]], "is_clean": [["kitchen"], ["bathroom"]]}
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID,
body=json.dumps(beliefs_payload),
thread="beliefs"
sender_node=COLLECTOR_AGENT_JID, body=json.dumps(beliefs_payload), thread="beliefs"
)
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
@@ -139,9 +134,7 @@ def test_process_belief_message_invalid_json(belief_setter, mocker, caplog):
"""
# Arrange
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID,
body="this is not a json string",
thread="beliefs"
sender_node=COLLECTOR_AGENT_JID, body="this is not a json string", thread="beliefs"
)
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
@@ -160,9 +153,7 @@ def test_process_belief_message_wrong_thread(belief_setter, mocker):
"""
# Arrange
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID,
body='{"some": "data"}',
thread="not_beliefs"
sender_node=COLLECTOR_AGENT_JID, body='{"some": "data"}', thread="not_beliefs"
)
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
@@ -172,16 +163,13 @@ def test_process_belief_message_wrong_thread(belief_setter, mocker):
# Assert
mock_set_beliefs.assert_not_called()
def test_process_belief_message_empty_body(belief_setter, mocker):
"""
Test that a message with an empty body is ignored.
"""
# Arrange
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID,
body="",
thread="beliefs"
)
msg = create_mock_message(sender_node=COLLECTOR_AGENT_JID, body="", thread="beliefs")
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
# Act
@@ -198,9 +186,9 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
# Arrange
beliefs_to_set = {
"is_hot": [["kitchen"], ["living_room"]],
"door_is": [["front_door", "closed"]]
"door_is": [["front_door", "closed"]],
}
# Act
with caplog.at_level(logging.INFO):
belief_setter._set_beliefs(beliefs_to_set)
@@ -209,11 +197,11 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
expected_calls = [
call("is_hot", "kitchen"),
call("is_hot", "living_room"),
call("door_is", "front_door", "closed")
call("door_is", "front_door", "closed"),
]
mock_agent.bdi.set_belief.assert_has_calls(expected_calls, any_order=True)
assert mock_agent.bdi.set_belief.call_count == 3
# Check logs
assert "Set belief is_hot with arguments ['kitchen']" in caplog.text
assert "Set belief is_hot with arguments ['living_room']" in caplog.text

View File

@@ -48,14 +48,14 @@ async def test_voice_activity_detected(audio_in_socket, audio_out_socket, stream
:return:
"""
speech_chunk_count = 5
probabilities = [0.0]*5 + [1.0]*speech_chunk_count + [0.0]*5
probabilities = [0.0] * 5 + [1.0] * speech_chunk_count + [0.0] * 5
await simulate_streaming_with_probabilities(streaming, probabilities)
audio_out_socket.send.assert_called_once()
data = audio_out_socket.send.call_args[0][0]
assert isinstance(data, bytes)
# each sample has 512 frames of 4 bytes, expecting 7 chunks (5 with speech, 2 as padding)
assert len(data) == 512*4*(speech_chunk_count+2)
assert len(data) == 512 * 4 * (speech_chunk_count + 2)
@pytest.mark.asyncio
@@ -65,14 +65,16 @@ async def test_voice_activity_short_pause(audio_in_socket, audio_out_socket, str
short pause.
"""
speech_chunk_count = 5
probabilities = [0.0]*5 + [1.0]*speech_chunk_count + [0.0] + [1.0]*speech_chunk_count + [0.0]*5
probabilities = (
[0.0] * 5 + [1.0] * speech_chunk_count + [0.0] + [1.0] * speech_chunk_count + [0.0] * 5
)
await simulate_streaming_with_probabilities(streaming, probabilities)
audio_out_socket.send.assert_called_once()
data = audio_out_socket.send.call_args[0][0]
assert isinstance(data, bytes)
# Expecting 13 chunks (2*5 with speech, 1 pause between, 2 as padding)
assert len(data) == 512*4*(speech_chunk_count*2+1+2)
assert len(data) == 512 * 4 * (speech_chunk_count * 2 + 1 + 2)
@pytest.mark.asyncio

View File

@@ -1,8 +1,6 @@
import sys
from unittest.mock import MagicMock
import sys
from unittest.mock import MagicMock
def pytest_configure(config):
"""
@@ -17,21 +15,21 @@ def pytest_configure(config):
mock_spade_bdi.bdi = MagicMock()
mock_spade.agent.Message = MagicMock()
mock_spade.behaviour.CyclicBehaviour = type('CyclicBehaviour', (object,), {})
mock_spade_bdi.bdi.BDIAgent = type('BDIAgent', (object,), {})
mock_spade.behaviour.CyclicBehaviour = type("CyclicBehaviour", (object,), {})
mock_spade_bdi.bdi.BDIAgent = type("BDIAgent", (object,), {})
sys.modules['spade'] = mock_spade
sys.modules['spade.agent'] = mock_spade.agent
sys.modules['spade.behaviour'] = mock_spade.behaviour
sys.modules['spade_bdi'] = mock_spade_bdi
sys.modules['spade_bdi.bdi'] = mock_spade_bdi.bdi
sys.modules["spade"] = mock_spade
sys.modules["spade.agent"] = mock_spade.agent
sys.modules["spade.behaviour"] = mock_spade.behaviour
sys.modules["spade_bdi"] = mock_spade_bdi
sys.modules["spade_bdi.bdi"] = mock_spade_bdi.bdi
# --- Mock the config module to prevent Pydantic ImportError ---
mock_config_module = MagicMock()
# The code under test does `from ... import settings`, so our mock module
# must have a `settings` attribute. We'll make it a MagicMock so we can
# configure it later in our tests using mocker.patch.
mock_config_module.settings = MagicMock()
sys.modules['control_backend.core.config'] = mock_config_module
sys.modules["control_backend.core.config"] = mock_config_module