test: add unit tests to BeliefCollector

ref: N25B-197
This commit is contained in:
2025-10-22 14:51:20 +02:00
parent cd0b3ee9a0
commit e057cf3003
4 changed files with 239 additions and 14 deletions

View File

@@ -26,3 +26,6 @@ test = [
"pytest-cov>=7.0.0", "pytest-cov>=7.0.0",
"pytest-mock>=3.15.1", "pytest-mock>=3.15.1",
] ]
[tool.pytest.ini_options]
pythonpath = ["src"]

View File

@@ -5,7 +5,7 @@ from spade_bdi.bdi import BDIAgent
from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter
class BDICore(BDIAgent): class BDICoreAgent(BDIAgent):
""" """
This is the Brain agent that does the belief inference with AgentSpeak. This is the Brain agent that does the belief inference with AgentSpeak.
This is a continous process that happens automatically in the background. This is a continous process that happens automatically in the background.

View File

@@ -12,7 +12,7 @@ from spade.behaviour import OneShotBehaviour
import zmq import zmq
# Internal imports # Internal imports
from control_backend.agents.bdi.bdi_core import BDICore from control_backend.agents.bdi.bdi_core import BDICoreAgent
from control_backend.api.v1.router import api_router from control_backend.api.v1.router import api_router
from control_backend.core.config import AgentSettings, settings from control_backend.core.config import AgentSettings, settings
from control_backend.core.zmq_context import context from control_backend.core.zmq_context import context
@@ -32,20 +32,9 @@ async def lifespan(app: FastAPI):
logger.info("Internal publishing socket bound to %s", internal_comm_socket) logger.info("Internal publishing socket bound to %s", internal_comm_socket)
# Initiate agents # Initiate agents
bdi_core = BDICore(settings.agent_settings.bdi_core_agent_name + '@' + settings.agent_settings.host, settings.agent_settings.bdi_core_agent_name, "src/control_backend/agents/bdi/rules.asl") bdi_core = BDICoreAgent(settings.agent_settings.bdi_core_agent_name + '@' + settings.agent_settings.host, settings.agent_settings.bdi_core_agent_name, "src/control_backend/agents/bdi/rules.asl")
await bdi_core.start() await bdi_core.start()
# -----------TEMORARY SECTION-------------
belief_collector = Agent(settings.agent_settings.belief_collector_agent_name + '@' + settings.agent_settings.host, settings.agent_settings.belief_collector_agent_name)
await belief_collector.start()
class SendMessageBehaviour(OneShotBehaviour):
async def run(self):
await self.send(Message(bdi_core.jid, belief_collector.jid, json.dumps({"user_said": [["Hello World!"]]}), "beliefs"))
belief_collector.add_behaviour(SendMessageBehaviour())
# -----------TEMORARY SECTION-------------
yield yield
logger.info("%s shutting down.", app.title) logger.info("%s shutting down.", app.title)

View File

@@ -0,0 +1,233 @@
import asyncio
import json
import logging
from unittest.mock import MagicMock, AsyncMock, call
import pytest
from spade.agent import Message
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
@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 = Message(to="bdi_agent@test")
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 = Message(to="bdi_agent@test", sender=COLLECTOR_AGENT_JID)
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 = Message(to="bdi_agent@test", sender="other_agent@test")
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 = Message(
to="bdi_agent@test",
sender=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 = Message(
to="bdi_agent@test",
sender=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 = Message(
to="bdi_agent@test",
sender=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 = Message(
to="bdi_agent@test",
sender=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