refactor: change test folder structure, rename functions to account for (non)changing behaviours and clarity

ref: N25B-257
This commit is contained in:
Björn Otgaar
2025-11-12 12:42:54 +01:00
parent 9365f109ab
commit 7a707cf9a0
19 changed files with 79 additions and 79 deletions

View File

@@ -29,7 +29,7 @@ class ActSpeechAgent(BaseAgent):
self.address = address self.address = address
self.bind = bind self.bind = bind
class SendCommandsBehaviour(CyclicBehaviour): class SendZMQCommandsBehaviour(CyclicBehaviour):
"""Behaviour for sending commands received from the UI.""" """Behaviour for sending commands received from the UI."""
async def run(self): async def run(self):
@@ -50,7 +50,7 @@ class ActSpeechAgent(BaseAgent):
except Exception as e: except Exception as e:
self.agent.logger.error("Error processing message: %s", e) self.agent.logger.error("Error processing message: %s", e)
class SendPythonCommandsBehaviour(CyclicBehaviour): class SendSpadeCommandsBehaviour(CyclicBehaviour):
"""Behaviour for sending commands received from other Python agents.""" """Behaviour for sending commands received from other Python agents."""
async def run(self): async def run(self):
@@ -83,8 +83,8 @@ class ActSpeechAgent(BaseAgent):
self.subsocket.setsockopt(zmq.SUBSCRIBE, b"command") self.subsocket.setsockopt(zmq.SUBSCRIBE, b"command")
# Add behaviour to our agent # Add behaviour to our agent
commands_behaviour = self.SendCommandsBehaviour() commands_behaviour = self.SendZMQCommandsBehaviour()
self.add_behaviour(commands_behaviour) self.add_behaviour(commands_behaviour)
self.add_behaviour(self.SendPythonCommandsBehaviour()) self.add_behaviour(self.SendSpadeCommandsBehaviour())
self.logger.info("Finished setting up %s", self.jid) self.logger.info("Finished setting up %s", self.jid)

View File

@@ -7,7 +7,7 @@ from spade.behaviour import CyclicBehaviour
from control_backend.core.config import settings from control_backend.core.config import settings
class ContinuousBeliefCollector(CyclicBehaviour): class BelCollectorBehaviour(CyclicBehaviour):
""" """
Continuously collects beliefs/emotions from extractor agents: Continuously collects beliefs/emotions from extractor agents:
Then we send a unified belief packet to the BDI agent. Then we send a unified belief packet to the BDI agent.

View File

@@ -1,11 +1,11 @@
from control_backend.agents.base import BaseAgent from control_backend.agents.base import BaseAgent
from .behaviours.continuous_collect import ContinuousBeliefCollector from .behaviours.bel_collector_behaviour import BelCollectorBehaviour
class BDIBeliefCollectorAgent(BaseAgent): class BDIBeliefCollectorAgent(BaseAgent):
async def setup(self): async def setup(self):
self.logger.info("BDIBeliefCollectorAgent starting (%s)", self.jid) self.logger.info("BDIBeliefCollectorAgent starting (%s)", self.jid)
# Attach the continuous collector behaviour (listens and forwards to BDI) # Attach the continuous collector behaviour (listens and forwards to BDI)
self.add_behaviour(ContinuousBeliefCollector()) self.add_behaviour(BelCollectorBehaviour())
self.logger.info("BDIBeliefCollectorAgent ready.") self.logger.info("BDIBeliefCollectorAgent ready.")

View File

@@ -7,7 +7,7 @@ from spade_bdi.bdi import BDIAgent
from control_backend.core.config import settings from control_backend.core.config import settings
from .behaviours.belief_setter import BeliefSetterBehaviour from .behaviours.belief_setter_behaviour import BeliefSetterBehaviour
from .behaviours.receive_llm_resp_behaviour import ReceiveLLMResponseBehaviour from .behaviours.receive_llm_resp_behaviour import ReceiveLLMResponseBehaviour

View File

@@ -1,8 +1,8 @@
from control_backend.agents.base import BaseAgent from control_backend.agents.base import BaseAgent
from .behaviours.text_belief_extractor import BeliefFromText from .behaviours.bdi_text_belief_behaviour import BDITextBeliefBehaviour
class BDITextBeliefAgent(BaseAgent): class BDITextBeliefAgent(BaseAgent):
async def setup(self): async def setup(self):
self.add_behaviour(BeliefFromText()) self.add_behaviour(BDITextBeliefBehaviour())

View File

@@ -7,7 +7,7 @@ from spade.message import Message
from control_backend.core.config import settings from control_backend.core.config import settings
class BeliefFromText(CyclicBehaviour): class BDITextBeliefBehaviour(CyclicBehaviour):
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# TODO: LLM prompt nog hardcoded # TODO: LLM prompt nog hardcoded

View File

@@ -114,7 +114,7 @@ async def lifespan(app: FastAPI):
"password": settings.agent_settings.bdi_belief_collector_agent_name, "password": settings.agent_settings.bdi_belief_collector_agent_name,
}, },
), ),
"TBeliefExtractor": ( "BDITextBeliefAgent": (
BDITextBeliefAgent, BDITextBeliefAgent,
{ {
"name": settings.agent_settings.bdi_text_belief_agent_name, "name": settings.agent_settings.bdi_text_belief_agent_name,

View File

@@ -34,7 +34,7 @@ async def test_setup_bind(zmq_context, mocker):
fake_socket.setsockopt.assert_any_call(zmq.SUBSCRIBE, b"command") fake_socket.setsockopt.assert_any_call(zmq.SUBSCRIBE, b"command")
# Ensure behaviour attached # Ensure behaviour attached
assert any(isinstance(b, agent.SendCommandsBehaviour) for b in agent.behaviours) assert any(isinstance(b, agent.SendZMQCommandsBehaviour) for b in agent.behaviours)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -66,7 +66,7 @@ async def test_send_commands_behaviour_valid_message():
agent.subsocket = fake_socket agent.subsocket = fake_socket
agent.pubsocket = fake_socket agent.pubsocket = fake_socket
behaviour = agent.SendCommandsBehaviour() behaviour = agent.SendZMQCommandsBehaviour()
behaviour.agent = agent behaviour.agent = agent
with patch( with patch(
@@ -92,7 +92,7 @@ async def test_send_commands_behaviour_invalid_message(caplog):
agent.subsocket = fake_socket agent.subsocket = fake_socket
agent.pubsocket = fake_socket agent.pubsocket = fake_socket
behaviour = agent.SendCommandsBehaviour() behaviour = agent.SendZMQCommandsBehaviour()
behaviour.agent = agent behaviour.agent = agent
await behaviour.run() await behaviour.run()

View File

@@ -3,8 +3,8 @@ from unittest.mock import AsyncMock, MagicMock
import pytest import pytest
from control_backend.agents.bdi_agents.bdi_belief_collector_agent.behaviours.continuous_collect import ( # noqa: E501 from control_backend.agents.bdi_agents.bdi_belief_collector_agent.behaviours.bel_collector_behaviour import ( # noqa: E501
ContinuousBeliefCollector, BelCollectorBehaviour,
) )
@@ -25,12 +25,12 @@ def mock_agent(mocker):
@pytest.fixture @pytest.fixture
def continuous_collector(mock_agent, mocker): def bel_collector_behaviouror(mock_agent, mocker):
"""Fixture to create an instance of ContinuousBeliefCollector with a mocked agent.""" """Fixture to create an instance of BelCollectorBehaviour with a mocked agent."""
# Patch asyncio.sleep to prevent tests from actually waiting # Patch asyncio.sleep to prevent tests from actually waiting
mocker.patch("asyncio.sleep", return_value=None) mocker.patch("asyncio.sleep", return_value=None)
collector = ContinuousBeliefCollector() collector = BelCollectorBehaviour()
collector.agent = mock_agent collector.agent = mock_agent
# Mock the receive method, we will control its return value in each test # Mock the receive method, we will control its return value in each test
collector.receive = AsyncMock() collector.receive = AsyncMock()
@@ -38,64 +38,64 @@ def continuous_collector(mock_agent, mocker):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_message_received(continuous_collector, mocker): async def test_run_message_received(bel_collector_behaviouror, mocker):
""" """
Test that when a message is received, _process_message is called with that message. Test that when a message is received, _process_message is called with that message.
""" """
# Arrange # Arrange
mock_msg = MagicMock() mock_msg = MagicMock()
continuous_collector.receive.return_value = mock_msg bel_collector_behaviouror.receive.return_value = mock_msg
mocker.patch.object(continuous_collector, "_process_message") mocker.patch.object(bel_collector_behaviouror, "_process_message")
# Act # Act
await continuous_collector.run() await bel_collector_behaviouror.run()
# Assert # Assert
continuous_collector._process_message.assert_awaited_once_with(mock_msg) bel_collector_behaviouror._process_message.assert_awaited_once_with(mock_msg)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_routes_to_handle_belief_text_by_type(continuous_collector, mocker): async def test_routes_to_handle_belief_text_by_type(bel_collector_behaviouror, mocker):
msg = create_mock_message( msg = create_mock_message(
"anyone", "anyone",
json.dumps({"type": "belief_extraction_text", "beliefs": {"user_said": [["hi"]]}}), json.dumps({"type": "belief_extraction_text", "beliefs": {"user_said": [["hi"]]}}),
) )
spy = mocker.patch.object(continuous_collector, "_handle_belief_text", new=AsyncMock()) spy = mocker.patch.object(bel_collector_behaviouror, "_handle_belief_text", new=AsyncMock())
await continuous_collector._process_message(msg) await bel_collector_behaviouror._process_message(msg)
spy.assert_awaited_once() spy.assert_awaited_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_routes_to_handle_belief_text_by_sender(continuous_collector, mocker): async def test_routes_to_handle_belief_text_by_sender(bel_collector_behaviouror, mocker):
msg = create_mock_message( msg = create_mock_message(
"bel_text_agent_mock", json.dumps({"beliefs": {"user_said": [["hi"]]}}) "bel_text_agent_mock", json.dumps({"beliefs": {"user_said": [["hi"]]}})
) )
spy = mocker.patch.object(continuous_collector, "_handle_belief_text", new=AsyncMock()) spy = mocker.patch.object(bel_collector_behaviouror, "_handle_belief_text", new=AsyncMock())
await continuous_collector._process_message(msg) await bel_collector_behaviouror._process_message(msg)
spy.assert_awaited_once() spy.assert_awaited_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_routes_to_handle_emo_text(continuous_collector, mocker): async def test_routes_to_handle_emo_text(bel_collector_behaviouror, mocker):
msg = create_mock_message("anyone", json.dumps({"type": "emotion_extraction_text"})) msg = create_mock_message("anyone", json.dumps({"type": "emotion_extraction_text"}))
spy = mocker.patch.object(continuous_collector, "_handle_emo_text", new=AsyncMock()) spy = mocker.patch.object(bel_collector_behaviouror, "_handle_emo_text", new=AsyncMock())
await continuous_collector._process_message(msg) await bel_collector_behaviouror._process_message(msg)
spy.assert_awaited_once() spy.assert_awaited_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_belief_text_happy_path_sends(continuous_collector, mocker): async def test_belief_text_happy_path_sends(bel_collector_behaviouror, mocker):
payload = {"type": "belief_extraction_text", "beliefs": {"user_said": ["hello test", "No"]}} payload = {"type": "belief_extraction_text", "beliefs": {"user_said": ["hello test", "No"]}}
continuous_collector.send = AsyncMock() bel_collector_behaviouror.send = AsyncMock()
await continuous_collector._handle_belief_text(payload, "bel_text_agent_mock") await bel_collector_behaviouror._handle_belief_text(payload, "bel_text_agent_mock")
# make sure we attempted a send # make sure we attempted a send
continuous_collector.send.assert_awaited_once() bel_collector_behaviouror.send.assert_awaited_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_belief_text_coerces_non_strings(continuous_collector, mocker): async def test_belief_text_coerces_non_strings(bel_collector_behaviouror, mocker):
payload = {"type": "belief_extraction_text", "beliefs": {"user_said": [["hi", 123]]}} payload = {"type": "belief_extraction_text", "beliefs": {"user_said": [["hi", 123]]}}
continuous_collector.send = AsyncMock() bel_collector_behaviouror.send = AsyncMock()
await continuous_collector._handle_belief_text(payload, "origin") await bel_collector_behaviouror._handle_belief_text(payload, "origin")
continuous_collector.send.assert_awaited_once() bel_collector_behaviouror.send.assert_awaited_once()

View File

@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, call
import pytest import pytest
from control_backend.agents.bdi_agents.bdi_core_agent.behaviours.belief_setter import ( from control_backend.agents.bdi_agents.bdi_core_agent.behaviours.belief_setter_behaviour import (
BeliefSetterBehaviour, BeliefSetterBehaviour,
) )
@@ -23,12 +23,12 @@ def mock_agent(mocker):
@pytest.fixture @pytest.fixture
def belief_setter(mock_agent, mocker): def belief_setter_behaviour(mock_agent, mocker):
"""Fixture to create an instance of BeliefSetterBehaviour with a mocked agent.""" """Fixture to create an instance of BeliefSetterBehaviour with a mocked agent."""
# Patch the settings to use a predictable agent name # Patch the settings to use a predictable agent name
mocker.patch( mocker.patch(
"control_backend.agents.bdi_agents.bdi_core_agent." "control_backend.agents.bdi_agents.bdi_core_agent."
"behaviours.belief_setter.settings.agent_settings.bdi_belief_collector_agent_name", "behaviours.belief_setter_behaviour.settings.agent_settings.bdi_belief_collector_agent_name",
COLLECTOR_AGENT_NAME, COLLECTOR_AGENT_NAME,
) )
@@ -49,53 +49,53 @@ def create_mock_message(sender_node: str, body: str, thread: str) -> MagicMock:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_run_message_received(belief_setter, mocker): async def test_run_message_received(belief_setter_behaviour, mocker):
""" """
Test that when a message is received, _process_message is called. Test that when a message is received, _process_message is called.
""" """
# Arrange # Arrange
msg = MagicMock() msg = MagicMock()
belief_setter.receive.return_value = msg belief_setter_behaviour.receive.return_value = msg
mocker.patch.object(belief_setter, "_process_message") mocker.patch.object(belief_setter_behaviour, "_process_message")
# Act # Act
await belief_setter.run() await belief_setter_behaviour.run()
# Assert # Assert
belief_setter._process_message.assert_called_once_with(msg) belief_setter_behaviour._process_message.assert_called_once_with(msg)
def test_process_message_from_bdi_belief_collector_agent(belief_setter, mocker): def test_process_message_from_bdi_belief_collector_agent(belief_setter_behaviour, mocker):
""" """
Test processing a message from the correct belief collector agent. Test processing a message from the correct belief collector agent.
""" """
# Arrange # Arrange
msg = create_mock_message(sender_node=COLLECTOR_AGENT_NAME, body="", thread="") msg = create_mock_message(sender_node=COLLECTOR_AGENT_NAME, body="", thread="")
mock_process_belief = mocker.patch.object(belief_setter, "_process_belief_message") mock_process_belief = mocker.patch.object(belief_setter_behaviour, "_process_belief_message")
# Act # Act
belief_setter._process_message(msg) belief_setter_behaviour._process_message(msg)
# Assert # Assert
mock_process_belief.assert_called_once_with(msg) mock_process_belief.assert_called_once_with(msg)
def test_process_message_from_other_agent(belief_setter, mocker): def test_process_message_from_other_agent(belief_setter_behaviour, mocker):
""" """
Test that messages from other agents are ignored. Test that messages from other agents are ignored.
""" """
# Arrange # Arrange
msg = create_mock_message(sender_node="other_agent", body="", thread="") msg = create_mock_message(sender_node="other_agent", body="", thread="")
mock_process_belief = mocker.patch.object(belief_setter, "_process_belief_message") mock_process_belief = mocker.patch.object(belief_setter_behaviour, "_process_belief_message")
# Act # Act
belief_setter._process_message(msg) belief_setter_behaviour._process_message(msg)
# Assert # Assert
mock_process_belief.assert_not_called() mock_process_belief.assert_not_called()
def test_process_belief_message_valid_json(belief_setter, mocker): def test_process_belief_message_valid_json(belief_setter_behaviour, mocker):
""" """
Test processing a valid belief message with correct thread and JSON body. Test processing a valid belief message with correct thread and JSON body.
""" """
@@ -104,16 +104,16 @@ def test_process_belief_message_valid_json(belief_setter, mocker):
msg = create_mock_message( 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") mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs")
# Act # Act
belief_setter._process_belief_message(msg) belief_setter_behaviour._process_belief_message(msg)
# Assert # Assert
mock_set_beliefs.assert_called_once_with(beliefs_payload) mock_set_beliefs.assert_called_once_with(beliefs_payload)
def test_process_belief_message_invalid_json(belief_setter, mocker, caplog): def test_process_belief_message_invalid_json(belief_setter_behaviour, mocker, caplog):
""" """
Test that a message with invalid JSON is handled gracefully and an error is logged. Test that a message with invalid JSON is handled gracefully and an error is logged.
""" """
@@ -121,16 +121,16 @@ def test_process_belief_message_invalid_json(belief_setter, mocker, caplog):
msg = create_mock_message( 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") mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs")
# Act # Act
belief_setter._process_belief_message(msg) belief_setter_behaviour._process_belief_message(msg)
# Assert # Assert
mock_set_beliefs.assert_not_called() mock_set_beliefs.assert_not_called()
def test_process_belief_message_wrong_thread(belief_setter, mocker): def test_process_belief_message_wrong_thread(belief_setter_behaviour, mocker):
""" """
Test that a message with an incorrect thread is ignored. Test that a message with an incorrect thread is ignored.
""" """
@@ -138,31 +138,31 @@ def test_process_belief_message_wrong_thread(belief_setter, mocker):
msg = create_mock_message( 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") mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs")
# Act # Act
belief_setter._process_belief_message(msg) belief_setter_behaviour._process_belief_message(msg)
# Assert # Assert
mock_set_beliefs.assert_not_called() mock_set_beliefs.assert_not_called()
def test_process_belief_message_empty_body(belief_setter, mocker): def test_process_belief_message_empty_body(belief_setter_behaviour, mocker):
""" """
Test that a message with an empty body is ignored. Test that a message with an empty body is ignored.
""" """
# Arrange # 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") mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs")
# Act # Act
belief_setter._process_belief_message(msg) belief_setter_behaviour._process_belief_message(msg)
# Assert # Assert
mock_set_beliefs.assert_not_called() mock_set_beliefs.assert_not_called()
def test_set_beliefs_success(belief_setter, mock_agent, caplog): def test_set_beliefs_success(belief_setter_behaviour, mock_agent, caplog):
""" """
Test that beliefs are correctly set on the agent's BDI. Test that beliefs are correctly set on the agent's BDI.
""" """
@@ -174,7 +174,7 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
# Act # Act
with caplog.at_level(logging.INFO): with caplog.at_level(logging.INFO):
belief_setter._set_beliefs(beliefs_to_set) belief_setter_behaviour._set_beliefs(beliefs_to_set)
# Assert # Assert
expected_calls = [ expected_calls = [
@@ -185,18 +185,18 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
assert mock_agent.bdi.set_belief.call_count == 2 assert mock_agent.bdi.set_belief.call_count == 2
# def test_responded_unset(belief_setter, mock_agent): # def test_responded_unset(belief_setter_behaviour, mock_agent):
# # Arrange # # Arrange
# new_beliefs = {"user_said": ["message"]} # new_beliefs = {"user_said": ["message"]}
# #
# # Act # # Act
# belief_setter._set_beliefs(new_beliefs) # belief_setter_behaviour._set_beliefs(new_beliefs)
# #
# # Assert # # Assert
# mock_agent.bdi.set_belief.assert_has_calls([call("user_said", "message")]) # mock_agent.bdi.set_belief.assert_has_calls([call("user_said", "message")])
# mock_agent.bdi.remove_belief.assert_has_calls([call("responded")]) # mock_agent.bdi.remove_belief.assert_has_calls([call("responded")])
# def test_set_beliefs_bdi_not_initialized(belief_setter, mock_agent, caplog): # def test_set_beliefs_bdi_not_initialized(belief_setter_behaviour, mock_agent, caplog):
# """ # """
# Test that a warning is logged if the agent's BDI is not initialized. # Test that a warning is logged if the agent's BDI is not initialized.
# """ # """
@@ -206,7 +206,7 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
# #
# # Act # # Act
# with caplog.at_level(logging.WARNING): # with caplog.at_level(logging.WARNING):
# belief_setter._set_beliefs(beliefs_to_set) # belief_setter_behaviour._set_beliefs(beliefs_to_set)
# #
# # Assert # # Assert
# assert "Cannot set beliefs, since agent's BDI is not yet initialized." in caplog.text # assert "Cannot set beliefs, since agent's BDI is not yet initialized." in caplog.text

View File

@@ -4,8 +4,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
from spade.message import Message from spade.message import Message
from control_backend.agents.bdi_agents.bdi_text_belief_agent.behaviours.text_belief_extractor import ( # noqa: E501, We can't shorten this import. from control_backend.agents.bdi_agents.bdi_text_belief_agent.behaviours.bdi_text_belief_behaviour import ( # noqa: E501, We can't shorten this import.
BeliefFromText, BDITextBeliefBehaviour,
) )
@@ -25,7 +25,7 @@ def mock_settings():
# Adjust 'control_backend.behaviours.belief_from_text.settings' to where # Adjust 'control_backend.behaviours.belief_from_text.settings' to where
# your behaviour file imports it from. # your behaviour file imports it from.
with patch( with patch(
"control_backend.agents.bdi_agents.bdi_text_belief_agent.behaviours.text_belief_extractor.settings", "control_backend.agents.bdi_agents.bdi_text_belief_agent.behaviours.bdi_text_belief_behaviour.settings",
settings_mock, settings_mock,
): ):
yield settings_mock yield settings_mock
@@ -34,10 +34,10 @@ def mock_settings():
@pytest.fixture @pytest.fixture
def behavior(mock_settings): def behavior(mock_settings):
""" """
Creates an instance of the BeliefFromText behaviour and mocks its Creates an instance of the BDITextBeliefBehaviour behaviour and mocks its
agent, logger, send, and receive methods. agent, logger, send, and receive methods.
""" """
b = BeliefFromText() b = BDITextBeliefBehaviour()
b.agent = MagicMock() b.agent = MagicMock()
b.send = AsyncMock() b.send = AsyncMock()