Merge branch 'dev' of https://git.science.uu.nl/ics/sp/2025/n25b/pepperplus-cb into feat/10-basic-gestures

This commit is contained in:
JobvAlewijk
2026-01-06 14:08:55 +01:00
13 changed files with 211 additions and 105 deletions

View File

@@ -11,7 +11,6 @@ from control_backend.schemas.ri_message import RIEndpoint
@pytest.fixture
def zmq_context(mocker):
"""Mock the ZMQ context."""
mock_context = mocker.patch(
"control_backend.agents.actuation.robot_gesture_agent.azmq.Context.instance"
)
@@ -59,19 +58,16 @@ async def test_setup_connect(zmq_context, mocker):
@pytest.mark.asyncio
async def test_handle_message_forwards_valid_command():
async def test_handle_message_valid_gesture_tag():
pubsocket = AsyncMock()
agent = RobotGestureAgent(
"robot_gesture",
address="",
gesture_tags=["hello"],
)
agent.pubsocket = pubsocket
payload = {
"endpoint": RIEndpoint.GESTURE_TAG,
"data": "hello",
}
payload = {"endpoint": RIEndpoint.GESTURE_TAG, "data": "hello"}
msg = InternalMessage(to="robot", sender="tester", body=json.dumps(payload))
await agent.handle_message(msg)
@@ -80,13 +76,31 @@ async def test_handle_message_forwards_valid_command():
@pytest.mark.asyncio
async def test_handle_message_invalid_payload():
async def test_handle_message_invalid_gesture_tag():
pubsocket = AsyncMock()
agent = RobotGestureAgent("robot_gesture")
agent = RobotGestureAgent(
"robot_gesture",
address="",
gesture_tags=["hello"],
)
agent.pubsocket = pubsocket
payload = {"endpoint": RIEndpoint.GESTURE_TAG, "data": "nope"}
msg = InternalMessage(to="robot", sender="tester", body=json.dumps(payload))
await agent.handle_message(msg)
pubsocket.send_json.assert_not_awaited()
@pytest.mark.asyncio
async def test_handle_message_invalid_payload_logged():
pubsocket = AsyncMock()
agent = RobotGestureAgent("robot_gesture", address="")
agent.pubsocket = pubsocket
agent.logger = MagicMock()
msg = InternalMessage(to="robot", sender="tester", body=json.dumps({"bad": "data"}))
msg = InternalMessage(to="robot", sender="tester", body="not json")
await agent.handle_message(msg)
@@ -95,22 +109,25 @@ async def test_handle_message_invalid_payload():
@pytest.mark.asyncio
async def test_zmq_command_loop_valid_payload():
"""UI command with valid payload is published."""
command = {"endpoint": RIEndpoint.GESTURE_TAG, "data": "hello"}
async def test_zmq_command_loop_valid_gesture():
fake_socket = AsyncMock()
async def recv_once():
agent._running = False
return (b"command", json.dumps(command).encode("utf-8"))
return b"command", json.dumps(
{"endpoint": RIEndpoint.GESTURE_TAG, "data": "hello"}
).encode()
fake_socket.recv_multipart = recv_once
fake_socket.send_json = AsyncMock()
agent = RobotGestureAgent("robot_gesture")
agent = RobotGestureAgent(
"robot_gesture",
address="",
gesture_tags=["hello"],
)
agent.subsocket = fake_socket
agent.pubsocket = fake_socket
agent.gesture_data = ["hello", "yes", "no"] # ← REQUIRED for legacy check
agent._running = True
await agent._zmq_command_loop()
@@ -119,17 +136,23 @@ async def test_zmq_command_loop_valid_payload():
@pytest.mark.asyncio
async def test_zmq_command_loop_ignores_send_gestures():
async def test_zmq_command_loop_invalid_tag():
fake_socket = AsyncMock()
async def recv_once():
agent._running = False
return (b"send_gestures", b"{}")
return b"command", json.dumps(
{"endpoint": RIEndpoint.GESTURE_TAG, "data": "invalid"}
).encode()
fake_socket.recv_multipart = recv_once
fake_socket.send_json = AsyncMock()
agent = RobotGestureAgent("robot_gesture")
agent = RobotGestureAgent(
"robot_gesture",
address="",
gesture_tags=["hello"],
)
agent.subsocket = fake_socket
agent.pubsocket = fake_socket
agent._running = True
@@ -140,7 +163,28 @@ async def test_zmq_command_loop_ignores_send_gestures():
@pytest.mark.asyncio
async def test_fetch_gestures_tags_all():
async def test_zmq_command_loop_ignores_send_gestures_topic():
fake_socket = AsyncMock()
async def recv_once():
agent._running = False
return b"send_gestures", b"{}"
fake_socket.recv_multipart = recv_once
fake_socket.send_json = AsyncMock()
agent = RobotGestureAgent("robot_gesture", address="")
agent.subsocket = fake_socket
agent.pubsocket = fake_socket
agent._running = True
await agent._zmq_command_loop()
fake_socket.send_json.assert_not_awaited()
@pytest.mark.asyncio
async def test_fetch_gestures_tags():
fake_repsocket = AsyncMock()
async def recv_once():
@@ -152,6 +196,7 @@ async def test_fetch_gestures_tags_all():
agent = RobotGestureAgent(
"robot_gesture",
address="",
gesture_tags=["hello", "yes", "no"],
)
agent.repsocket = fake_repsocket
@@ -163,31 +208,7 @@ async def test_fetch_gestures_tags_all():
@pytest.mark.asyncio
async def test_fetch_gestures_tags_with_count():
fake_repsocket = AsyncMock()
async def recv_once():
agent._running = False
return {"type": "tags", "count": 2}
fake_repsocket.recv_json = recv_once
fake_repsocket.send_json = AsyncMock()
agent = RobotGestureAgent(
"robot_gesture",
gesture_tags=["hello", "yes", "no"],
)
agent.repsocket = fake_repsocket
agent._running = True
await agent._fetch_gestures_loop()
fake_repsocket.send_json.assert_awaited_once_with({"tags": ["hello", "yes"]})
@pytest.mark.asyncio
async def test_fetch_gestures_basic_new():
"""NEW: fetch basic gestures"""
async def test_fetch_gestures_basic():
fake_repsocket = AsyncMock()
async def recv_once():
@@ -199,6 +220,7 @@ async def test_fetch_gestures_basic_new():
agent = RobotGestureAgent(
"robot_gesture",
address="",
gesture_basic=["wave", "point"],
)
agent.repsocket = fake_repsocket
@@ -220,48 +242,10 @@ async def test_fetch_gestures_unknown_type():
fake_repsocket.recv_json = recv_once
fake_repsocket.send_json = AsyncMock()
agent = RobotGestureAgent("robot_gesture")
agent = RobotGestureAgent("robot_gesture", address="")
agent.repsocket = fake_repsocket
agent._running = True
await agent._fetch_gestures_loop()
fake_repsocket.send_json.assert_awaited_once_with({})
@pytest.mark.asyncio
async def test_fetch_gestures_exception_logged():
fake_repsocket = AsyncMock()
async def recv_once():
agent._running = False
raise Exception("boom")
fake_repsocket.recv_json = recv_once
fake_repsocket.send_json = AsyncMock()
agent = RobotGestureAgent("robot_gesture")
agent.repsocket = fake_repsocket
agent.logger = MagicMock()
agent._running = True
await agent._fetch_gestures_loop()
agent.logger.exception.assert_called_once()
@pytest.mark.asyncio
async def test_stop_closes_sockets():
pubsocket = MagicMock()
subsocket = MagicMock()
repsocket = MagicMock()
agent = RobotGestureAgent("robot_gesture")
agent.pubsocket = pubsocket
agent.subsocket = subsocket
agent.repsocket = repsocket
await agent.stop()
pubsocket.close.assert_called_once()
subsocket.close.assert_called_once()

View File

@@ -63,6 +63,7 @@ async def test_receive_programs_valid_and_invalid():
manager = BDIProgramManager(name="program_manager_test")
manager.sub_socket = sub
manager._send_to_bdi = AsyncMock()
manager._send_clear_llm_history = AsyncMock()
try:
# Will give StopAsyncIteration when the predefined `sub.recv_multipart` side-effects run out
@@ -75,3 +76,24 @@ async def test_receive_programs_valid_and_invalid():
forwarded: Program = manager._send_to_bdi.await_args[0][0]
assert forwarded.phases[0].norms[0].norm == "N1"
assert forwarded.phases[0].goals[0].description == "G1"
# Verify history clear was triggered
assert manager._send_clear_llm_history.await_count == 1
@pytest.mark.asyncio
async def test_send_clear_llm_history(mock_settings):
# Ensure the mock returns a string for the agent name (just like in your LLM tests)
mock_settings.agent_settings.llm_agent_name = "llm_agent"
manager = BDIProgramManager(name="program_manager_test")
manager.send = AsyncMock()
await manager._send_clear_llm_history()
assert manager.send.await_count == 1
msg: InternalMessage = manager.send.await_args[0][0]
# Verify the content and recipient
assert msg.body == "clear_history"
assert msg.to == "llm_agent"

View File

@@ -265,3 +265,23 @@ async def test_stream_query_llm_skips_non_data_lines(mock_httpx_client, mock_set
# Only the valid 'data:' line should yield content
assert tokens == ["Hi"]
@pytest.mark.asyncio
async def test_clear_history_command(mock_settings):
"""Test that the 'clear_history' message clears the agent's memory."""
# setup LLM to have some history
mock_settings.agent_settings.bdi_program_manager_name = "bdi_program_manager_agent"
agent = LLMAgent("llm_agent")
agent.history = [
{"role": "user", "content": "Old conversation context"},
{"role": "assistant", "content": "Old response"},
]
assert len(agent.history) == 2
msg = InternalMessage(
to="llm_agent",
sender=mock_settings.agent_settings.bdi_program_manager_name,
body="clear_history",
)
await agent.handle_message(msg)
assert len(agent.history) == 0

View File

@@ -7,6 +7,15 @@ import zmq
from control_backend.agents.perception.vad_agent import VADAgent
# We don't want to use real ZMQ in unit tests, for example because it can give errors when sockets
# aren't closed properly.
@pytest.fixture(autouse=True)
def mock_zmq():
with patch("zmq.asyncio.Context") as mock:
mock.instance.return_value = MagicMock()
yield mock
@pytest.fixture
def audio_out_socket():
return AsyncMock()
@@ -140,12 +149,10 @@ async def test_vad_model_load_failure_stops_agent(vad_agent):
# Patch stop to an AsyncMock so we can check it was awaited
vad_agent.stop = AsyncMock()
result = await vad_agent.setup()
await vad_agent.setup()
# Assert stop was called
vad_agent.stop.assert_awaited_once()
# Assert setup returned None
assert result is None
@pytest.mark.asyncio
@@ -155,7 +162,7 @@ async def test_audio_out_bind_failure_sets_none_and_logs(vad_agent, caplog):
audio_out_socket is set to None, None is returned, and an error is logged.
"""
mock_socket = MagicMock()
mock_socket.bind_to_random_port.side_effect = zmq.ZMQBindError()
mock_socket.bind.side_effect = zmq.ZMQBindError()
with patch("control_backend.agents.perception.vad_agent.azmq.Context.instance") as mock_ctx:
mock_ctx.return_value.socket.return_value = mock_socket