diff --git a/test/unit/agents/bdi/belief_collector_agent/behaviours/test_continuous_collect.py b/test/unit/agents/bdi/belief_collector_agent/behaviours/test_continuous_collect.py deleted file mode 100644 index a9e5147..0000000 --- a/test/unit/agents/bdi/belief_collector_agent/behaviours/test_continuous_collect.py +++ /dev/null @@ -1,100 +0,0 @@ -import json -from unittest.mock import AsyncMock, MagicMock - -import pytest -from control_backend.agents.bdi.belief_collector_agent.behaviours.belief_collector_behaviour import ( # noqa: E501 - BeliefCollectorBehaviour, -) - - -def create_mock_message(sender_node: str, body: 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 - return msg - - -@pytest.fixture -def mock_agent(mocker): - """Fixture to create a mock Agent.""" - agent = MagicMock() - agent.jid = "belief_collector_agent@test" - return agent - - -@pytest.fixture -def bel_collector_behaviouror(mock_agent, mocker): - """Fixture to create an instance of BelCollectorBehaviour with a mocked agent.""" - # Patch asyncio.sleep to prevent tests from actually waiting - mocker.patch("asyncio.sleep", return_value=None) - - collector = BeliefCollectorBehaviour() - collector.agent = mock_agent - # Mock the receive method, we will control its return value in each test - collector.receive = AsyncMock() - return collector - - -@pytest.mark.asyncio -async def test_run_message_received(bel_collector_behaviouror, mocker): - """ - Test that when a message is received, _process_message is called with that message. - """ - # Arrange - mock_msg = MagicMock() - bel_collector_behaviouror.receive.return_value = mock_msg - mocker.patch.object(bel_collector_behaviouror, "_process_message") - - # Act - await bel_collector_behaviouror.run() - - # Assert - bel_collector_behaviouror._process_message.assert_awaited_once_with(mock_msg) - - -@pytest.mark.asyncio -async def test_routes_to_handle_belief_text_by_type(bel_collector_behaviouror, mocker): - msg = create_mock_message( - "anyone", - json.dumps({"type": "belief_extraction_text", "beliefs": {"user_said": [["hi"]]}}), - ) - spy = mocker.patch.object(bel_collector_behaviouror, "_handle_belief_text", new=AsyncMock()) - await bel_collector_behaviouror._process_message(msg) - spy.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_routes_to_handle_belief_text_by_sender(bel_collector_behaviouror, mocker): - msg = create_mock_message( - "bel_text_agent_mock", json.dumps({"beliefs": {"user_said": [["hi"]]}}) - ) - spy = mocker.patch.object(bel_collector_behaviouror, "_handle_belief_text", new=AsyncMock()) - await bel_collector_behaviouror._process_message(msg) - spy.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_routes_to_handle_emo_text(bel_collector_behaviouror, mocker): - msg = create_mock_message("anyone", json.dumps({"type": "emotion_extraction_text"})) - spy = mocker.patch.object(bel_collector_behaviouror, "_handle_emo_text", new=AsyncMock()) - await bel_collector_behaviouror._process_message(msg) - spy.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_belief_text_happy_path_sends(bel_collector_behaviouror, mocker): - payload = {"type": "belief_extraction_text", "beliefs": {"user_said": ["hello test", "No"]}} - bel_collector_behaviouror.send = AsyncMock() - await bel_collector_behaviouror._handle_belief_text(payload, "bel_text_agent_mock") - - # make sure we attempted a send - bel_collector_behaviouror.send.assert_awaited_once() - - -@pytest.mark.asyncio -async def test_belief_text_coerces_non_strings(bel_collector_behaviouror, mocker): - payload = {"type": "belief_extraction_text", "beliefs": {"user_said": [["hi", 123]]}} - bel_collector_behaviouror.send = AsyncMock() - await bel_collector_behaviouror._handle_belief_text(payload, "origin") - bel_collector_behaviouror.send.assert_awaited_once() diff --git a/test/unit/agents/bdi/belief_collector_agent/test_continuous_collect.py b/test/unit/agents/bdi/belief_collector_agent/test_continuous_collect.py new file mode 100644 index 0000000..ab155fe --- /dev/null +++ b/test/unit/agents/bdi/belief_collector_agent/test_continuous_collect.py @@ -0,0 +1,87 @@ +import json +from unittest.mock import AsyncMock + +import pytest + +from control_backend.agents.bdi.belief_collector_agent.belief_collector_agent import ( + BDIBeliefCollectorAgent, +) +from control_backend.core.agent_system import InternalMessage +from control_backend.core.config import settings + + +@pytest.fixture +def agent(): + agent = BDIBeliefCollectorAgent("belief_collector_agent") + return agent + + +def make_msg(body: dict, sender: str = "sender"): + return InternalMessage(to="collector", sender=sender, body=json.dumps(body)) + + +@pytest.mark.asyncio +async def test_handle_message_routes_belief_text(agent, mocker): + """ + Test that when a message is received, _handle_belief_text is called with that message. + """ + payload = {"type": "belief_extraction_text", "beliefs": {"user_said": [["hi"]]}} + spy = mocker.patch.object(agent, "_handle_belief_text", new_callable=AsyncMock) + + await agent.handle_message(make_msg(payload)) + + spy.assert_awaited_once_with(payload, "sender") + + +@pytest.mark.asyncio +async def test_handle_message_routes_emotion(agent, mocker): + payload = {"type": "emotion_extraction_text"} + spy = mocker.patch.object(agent, "_handle_emo_text", new_callable=AsyncMock) + + await agent.handle_message(make_msg(payload)) + + spy.assert_awaited_once_with(payload, "sender") + + +@pytest.mark.asyncio +async def test_handle_message_bad_json(agent, mocker): + agent._handle_belief_text = AsyncMock() + bad_msg = InternalMessage(to="collector", sender="sender", body="not json") + + await agent.handle_message(bad_msg) + + agent._handle_belief_text.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_handle_belief_text_sends_when_beliefs_exist(agent, mocker): + payload = {"type": "belief_extraction_text", "beliefs": {"user_said": ["hello"]}} + spy = mocker.patch.object(agent, "_send_beliefs_to_bdi", new_callable=AsyncMock) + + await agent._handle_belief_text(payload, "origin") + + spy.assert_awaited_once_with(payload["beliefs"], origin="origin") + + +@pytest.mark.asyncio +async def test_handle_belief_text_no_send_when_empty(agent, mocker): + payload = {"type": "belief_extraction_text", "beliefs": {}} + spy = mocker.patch.object(agent, "_send_beliefs_to_bdi", new_callable=AsyncMock) + + await agent._handle_belief_text(payload, "origin") + + spy.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_send_beliefs_to_bdi(agent): + agent.send = AsyncMock() + beliefs = {"user_said": ["hello", "world"]} + + await agent._send_beliefs_to_bdi(beliefs, origin="origin") + + agent.send.assert_awaited_once() + sent: InternalMessage = agent.send.call_args.args[0] + assert sent.to == settings.agent_settings.bdi_core_name + assert sent.thread == "beliefs" + assert json.loads(sent.body) == beliefs diff --git a/test/unit/agents/bdi/text_belief_agent/behaviours/test_belief_from_text.py b/test/unit/agents/bdi/text_belief_agent/behaviours/test_belief_from_text.py deleted file mode 100644 index 92e1716..0000000 --- a/test/unit/agents/bdi/text_belief_agent/behaviours/test_belief_from_text.py +++ /dev/null @@ -1,190 +0,0 @@ -import json -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest -from control_backend.agents.bdi.text_belief_extractor_agent.behaviours.text_belief_extractor_behaviour import ( # noqa: E501, We can't shorten this import. - TextBeliefExtractorBehaviour, -) -from spade.message import Message - - -@pytest.fixture -def mock_settings(): - """ - Mocks the settings object that the behaviour imports. - We patch it at the source where it's imported by the module under test. - """ - # Create a mock object that mimics the nested structure - settings_mock = MagicMock() - settings_mock.agent_settings.transcription_name = "transcriber" - settings_mock.agent_settings.bdi_belief_collector_name = "collector" - settings_mock.agent_settings.host = "fake.host" - - # Use patch to replace the settings object during the test - # Adjust 'control_backend.behaviours.belief_from_text.settings' to where - # your behaviour file imports it from. - with patch( - "control_backend.agents.bdi.text_belief_extractor_agent.behaviours" - ".text_belief_extractor_behaviour.settings", - settings_mock, - ): - yield settings_mock - - -@pytest.fixture -def behavior(mock_settings): - """ - Creates an instance of the BDITextBeliefBehaviour behaviour and mocks its - agent, logger, send, and receive methods. - """ - b = TextBeliefExtractorBehaviour() - - b.agent = MagicMock() - b.send = AsyncMock() - b.receive = AsyncMock() - - return b - - -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(behavior): - """ - Tests the run() method when no message is received. - """ - # Arrange: Configure receive to return None - behavior.receive.return_value = None - - # Act: Run the behavior - await behavior.run() - - # Assert - # 1. Check that receive was called - behavior.receive.assert_called_once() - # 2. Check that no message was sent - behavior.send.assert_not_called() - - -@pytest.mark.asyncio -async def test_run_message_from_other_agent(behavior): - """ - Tests the run() method when a message is received from an - unknown agent (not the transcriber). - """ - # Arrange: Create a mock message from an unknown sender - mock_msg = create_mock_message("unknown", "some data", None) - behavior.receive.return_value = mock_msg - behavior._process_transcription_demo = MagicMock() - - # Act - await behavior.run() - - # Assert - # 1. Check that receive was called - behavior.receive.assert_called_once() - # 2. Check that _process_transcription_demo was not sent - behavior._process_transcription_demo.assert_not_called() - - -@pytest.mark.asyncio -async def test_run_message_from_transcriber_demo(behavior, mock_settings, monkeypatch): - """ - Tests the main success path: receiving a message from the - transcription agent, which triggers _process_transcription_demo. - """ - # Arrange: Create a mock message from the transcriber - transcription_text = "hello world" - mock_msg = create_mock_message( - mock_settings.agent_settings.transcription_name, transcription_text, None - ) - behavior.receive.return_value = mock_msg - - # Act - await behavior.run() - - # Assert - # 1. Check that receive was called - behavior.receive.assert_called_once() - - # 2. Check that send was called *once* - behavior.send.assert_called_once() - - # 3. Deeply inspect the message that was sent - sent_msg: Message = behavior.send.call_args[0][0] - - assert ( - sent_msg.to - == mock_settings.agent_settings.bdi_belief_collector_name - + "@" - + mock_settings.agent_settings.host - ) - - # Check thread - assert sent_msg.thread == "beliefs" - - # Parse the received JSON string back into a dict - expected_dict = { - "beliefs": {"user_said": [transcription_text]}, - "type": "belief_extraction_text", - } - sent_dict = json.loads(sent_msg.body) - - # Assert that the dictionaries are equal - assert sent_dict == expected_dict - - -@pytest.mark.asyncio -async def test_process_transcription_success(behavior, mock_settings): - """ - Tests the (currently unused) _process_transcription method's - success path, using its hardcoded mock response. - """ - # Arrange - test_text = "I am feeling happy" - # This is the hardcoded response inside the method - expected_response_body = '{"mood": [["happy"]]}' - - # Act - await behavior._process_transcription(test_text) - - # Assert - # 1. Check that a message was sent - behavior.send.assert_called_once() - - # 2. Inspect the sent message - sent_msg: Message = behavior.send.call_args[0][0] - expected_to = ( - mock_settings.agent_settings.bdi_belief_collector_name - + "@" - + mock_settings.agent_settings.host - ) - assert str(sent_msg.to) == expected_to - assert sent_msg.thread == "beliefs" - assert sent_msg.body == expected_response_body - - -@pytest.mark.asyncio -async def test_process_transcription_json_decode_error(behavior, mock_settings): - """ - Tests the _process_transcription method's error handling - when the (mocked) response is invalid JSON. - We do this by patching json.loads to raise an error. - """ - # Arrange - test_text = "I am feeling happy" - # Patch json.loads to raise an error when called - with patch("json.loads", side_effect=json.JSONDecodeError("Mock error", "", 0)): - # Act - await behavior._process_transcription(test_text) - - # Assert - # 1. Check that NO message was sent - behavior.send.assert_not_called() diff --git a/test/unit/agents/bdi/text_belief_agent/test_belief_from_text.py b/test/unit/agents/bdi/text_belief_agent/test_belief_from_text.py new file mode 100644 index 0000000..4fbd51a --- /dev/null +++ b/test/unit/agents/bdi/text_belief_agent/test_belief_from_text.py @@ -0,0 +1,68 @@ +import json +from unittest.mock import AsyncMock + +import pytest + +from control_backend.agents.bdi.text_belief_extractor_agent.text_belief_extractor_agent import ( + TextBeliefExtractorAgent, +) +from control_backend.core.agent_system import InternalMessage +from control_backend.core.config import settings + + +@pytest.fixture(autouse=True) +def patch_settings(monkeypatch): + monkeypatch.setattr(settings.agent_settings, "transcription_name", "transcriber", raising=False) + monkeypatch.setattr( + settings.agent_settings, "bdi_belief_collector_name", "collector", raising=False + ) + monkeypatch.setattr(settings.agent_settings, "host", "fake.host", raising=False) + + +@pytest.fixture +def agent(): + agent = TextBeliefExtractorAgent("text_belief_agent") + agent.send = AsyncMock() + return agent + + +def make_msg(sender: str, body: str, thread: str | None = None) -> InternalMessage: + return InternalMessage(to="unused", sender=sender, body=body, thread=thread) + + +@pytest.mark.asyncio +async def test_handle_message_ignores_other_agents(agent): + msg = make_msg("unknown", "some data", None) + + await agent.handle_message(msg) + + agent.send.assert_not_called() # noqa # `agent.send` has no such property, but we mock it. + + +@pytest.mark.asyncio +async def test_handle_message_from_transcriber(agent): + transcription = "hello world" + msg = make_msg(settings.agent_settings.transcription_name, transcription, None) + + await agent.handle_message(msg) + + agent.send.assert_awaited_once() # noqa # `agent.send` has no such property, but we mock it. + sent: InternalMessage = agent.send.call_args.args[0] # noqa + assert sent.to == settings.agent_settings.bdi_belief_collector_name + assert sent.thread == "beliefs" + parsed = json.loads(sent.body) + assert parsed == {"beliefs": {"user_said": [transcription]}, "type": "belief_extraction_text"} + + +@pytest.mark.asyncio +async def test_process_transcription_demo(agent): + transcription = "this is a test" + + await agent._process_transcription_demo(transcription) + + agent.send.assert_awaited_once() # noqa # `agent.send` has no such property, but we mock it. + sent: InternalMessage = agent.send.call_args.args[0] # noqa + assert sent.to == settings.agent_settings.bdi_belief_collector_name + assert sent.thread == "beliefs" + parsed = json.loads(sent.body) + assert parsed["beliefs"]["user_said"] == [transcription] diff --git a/test/unit/conftest.py b/test/unit/conftest.py index 97e7d15..fdd8f6c 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -22,7 +22,12 @@ def pytest_configure(config): mock_spade.behaviour.CyclicBehaviour = type("CyclicBehaviour", (object,), {}) mock_spade_bdi.bdi.BDIAgent = type("BDIAgent", (object,), {}) + # Ensure submodule imports like `agentspeak.runtime` succeed + mock_agentspeak.runtime = MagicMock() + mock_agentspeak.stdlib = MagicMock() sys.modules["agentspeak"] = mock_agentspeak + sys.modules["agentspeak.runtime"] = mock_agentspeak.runtime + sys.modules["agentspeak.stdlib"] = mock_agentspeak.stdlib sys.modules["httpx"] = mock_httpx sys.modules["pydantic"] = mock_pydantic sys.modules["spade"] = mock_spade