import json import logging from unittest.mock import AsyncMock, MagicMock, call import pytest from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter # 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 BeliefSetter 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, ) # 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 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"], ["living_room"]], "door_is": [["front_door", "closed"]], } # Act with caplog.at_level(logging.INFO): belief_setter._set_beliefs(beliefs_to_set) # Assert expected_calls = [ call("is_hot", "kitchen"), call("is_hot", "living_room"), 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 assert "Set belief door_is with arguments ['front_door', 'closed']" in caplog.text 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