There was an issue in how we treated beliefs, specifically with multiple beliefs of the same name but different arguments. This is fixed with this commit. Also implemented correct updating of the "responded" belief, when the user_said belief is updated (when we get a new user message, we state that we have not yet responded to that message) ref: N25B-197
231 lines
6.7 KiB
Python
231 lines
6.7 KiB
Python
import json
|
|
import logging
|
|
from unittest.mock import AsyncMock, MagicMock, call
|
|
|
|
import pytest
|
|
|
|
from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetterBehaviour
|
|
|
|
# Define a constant for the collector agent name to use in tests
|
|
COLLECTOR_AGENT_NAME = "belief_collector"
|
|
COLLECTOR_AGENT_JID = f"{COLLECTOR_AGENT_NAME}@test"
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_agent(mocker):
|
|
"""Fixture to create a mock BDIAgent."""
|
|
agent = MagicMock()
|
|
agent.bdi = MagicMock()
|
|
agent.jid = "bdi_agent@test"
|
|
return agent
|
|
|
|
|
|
@pytest.fixture
|
|
def belief_setter(mock_agent, mocker):
|
|
"""Fixture to create an instance of BeliefSetterBehaviour with a mocked agent."""
|
|
# 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,
|
|
)
|
|
|
|
setter = BeliefSetterBehaviour()
|
|
setter.agent = mock_agent
|
|
# Mock the receive method, we will control its return value in each test
|
|
setter.receive = AsyncMock()
|
|
return setter
|
|
|
|
|
|
def create_mock_message(sender_node: str, body: str, thread: str) -> MagicMock:
|
|
"""Helper function to create a configured mock message."""
|
|
msg = MagicMock()
|
|
msg.sender.node = sender_node # MagicMock automatically creates nested mocks
|
|
msg.body = body
|
|
msg.thread = thread
|
|
return msg
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_no_message_received(belief_setter, mocker):
|
|
"""
|
|
Test that when no message is received, _process_message is not called.
|
|
"""
|
|
# Arrange
|
|
belief_setter.receive.return_value = None
|
|
mocker.patch.object(belief_setter, "_process_message")
|
|
|
|
# Act
|
|
await belief_setter.run()
|
|
|
|
# Assert
|
|
belief_setter._process_message.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_run_message_received(belief_setter, mocker):
|
|
"""
|
|
Test that when a message is received, _process_message is called.
|
|
"""
|
|
# Arrange
|
|
msg = MagicMock()
|
|
belief_setter.receive.return_value = msg
|
|
mocker.patch.object(belief_setter, "_process_message")
|
|
|
|
# Act
|
|
await belief_setter.run()
|
|
|
|
# Assert
|
|
belief_setter._process_message.assert_called_once_with(msg)
|
|
|
|
|
|
def test_process_message_from_belief_collector(belief_setter, mocker):
|
|
"""
|
|
Test processing a message from the correct belief collector agent.
|
|
"""
|
|
# Arrange
|
|
msg = create_mock_message(sender_node=COLLECTOR_AGENT_NAME, body="", thread="")
|
|
mock_process_belief = mocker.patch.object(belief_setter, "_process_belief_message")
|
|
|
|
# Act
|
|
belief_setter._process_message(msg)
|
|
|
|
# Assert
|
|
mock_process_belief.assert_called_once_with(msg)
|
|
|
|
|
|
def test_process_message_from_other_agent(belief_setter, mocker):
|
|
"""
|
|
Test that messages from other agents are ignored.
|
|
"""
|
|
# Arrange
|
|
msg = create_mock_message(sender_node="other_agent", body="", thread="")
|
|
mock_process_belief = mocker.patch.object(belief_setter, "_process_belief_message")
|
|
|
|
# Act
|
|
belief_setter._process_message(msg)
|
|
|
|
# Assert
|
|
mock_process_belief.assert_not_called()
|
|
|
|
|
|
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"]}
|
|
msg = create_mock_message(
|
|
sender_node=COLLECTOR_AGENT_JID, body=json.dumps(beliefs_payload), thread="beliefs"
|
|
)
|
|
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
|
|
|
|
# Act
|
|
belief_setter._process_belief_message(msg)
|
|
|
|
# Assert
|
|
mock_set_beliefs.assert_called_once_with(beliefs_payload)
|
|
|
|
|
|
def test_process_belief_message_invalid_json(belief_setter, mocker, caplog):
|
|
"""
|
|
Test that a message with invalid JSON is handled gracefully and an error is logged.
|
|
"""
|
|
# Arrange
|
|
msg = create_mock_message(
|
|
sender_node=COLLECTOR_AGENT_JID, body="this is not a json string", thread="beliefs"
|
|
)
|
|
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
|
|
|
|
# Act
|
|
with caplog.at_level(logging.ERROR):
|
|
belief_setter._process_belief_message(msg)
|
|
|
|
# Assert
|
|
mock_set_beliefs.assert_not_called()
|
|
assert "Could not decode beliefs into JSON format" in caplog.text
|
|
|
|
|
|
def test_process_belief_message_wrong_thread(belief_setter, mocker):
|
|
"""
|
|
Test that a message with an incorrect thread is ignored.
|
|
"""
|
|
# Arrange
|
|
msg = create_mock_message(
|
|
sender_node=COLLECTOR_AGENT_JID, body='{"some": "data"}', thread="not_beliefs"
|
|
)
|
|
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
|
|
|
|
# Act
|
|
belief_setter._process_belief_message(msg)
|
|
|
|
# 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")
|
|
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
|
|
|
|
# Act
|
|
belief_setter._process_belief_message(msg)
|
|
|
|
# Assert
|
|
mock_set_beliefs.assert_not_called()
|
|
|
|
|
|
def test_set_beliefs_success(belief_setter, mock_agent, caplog):
|
|
"""
|
|
Test that beliefs are correctly set on the agent's BDI.
|
|
"""
|
|
# Arrange
|
|
beliefs_to_set = {
|
|
"is_hot": ["kitchen"],
|
|
"door_opened": ["front_door", "back_door"],
|
|
}
|
|
|
|
# Act
|
|
with caplog.at_level(logging.INFO):
|
|
belief_setter._set_beliefs(beliefs_to_set)
|
|
|
|
# Assert
|
|
expected_calls = [
|
|
call("is_hot", "kitchen"),
|
|
call("door_opened", "front_door", "back_door"),
|
|
]
|
|
mock_agent.bdi.set_belief.assert_has_calls(expected_calls, any_order=True)
|
|
assert mock_agent.bdi.set_belief.call_count == 2
|
|
|
|
# Check logs
|
|
assert "Set belief is_hot with arguments ['kitchen']" in caplog.text
|
|
assert "Set belief door_opened with arguments ['front_door', 'back_door']" in caplog.text
|
|
|
|
def test_responded_unset(belief_setter, mock_agent):
|
|
# Arrange
|
|
new_beliefs = {"user_said": ["message"]}
|
|
|
|
# Act
|
|
belief_setter._set_beliefs(new_beliefs)
|
|
|
|
# Assert
|
|
mock_agent.bdi.set_belief.assert_has_calls([call("user_said", "message")])
|
|
mock_agent.bdi.remove_belief.assert_has_calls([call("responded")])
|
|
|
|
def test_set_beliefs_bdi_not_initialized(belief_setter, mock_agent, caplog):
|
|
"""
|
|
Test that a warning is logged if the agent's BDI is not initialized.
|
|
"""
|
|
# Arrange
|
|
mock_agent.bdi = None # Simulate BDI not being ready
|
|
beliefs_to_set = {"is_hot": ["kitchen"]}
|
|
|
|
# Act
|
|
with caplog.at_level(logging.WARNING):
|
|
belief_setter._set_beliefs(beliefs_to_set)
|
|
|
|
# Assert
|
|
assert "Cannot set beliefs, since agent's BDI is not yet initialized." in caplog.text
|