Remove SPADE dependency #29
@@ -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()
|
|
||||||
@@ -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
|
||||||
@@ -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()
|
|
||||||
@@ -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]
|
||||||
@@ -22,7 +22,12 @@ def pytest_configure(config):
|
|||||||
mock_spade.behaviour.CyclicBehaviour = type("CyclicBehaviour", (object,), {})
|
mock_spade.behaviour.CyclicBehaviour = type("CyclicBehaviour", (object,), {})
|
||||||
mock_spade_bdi.bdi.BDIAgent = type("BDIAgent", (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"] = mock_agentspeak
|
||||||
|
sys.modules["agentspeak.runtime"] = mock_agentspeak.runtime
|
||||||
|
sys.modules["agentspeak.stdlib"] = mock_agentspeak.stdlib
|
||||||
sys.modules["httpx"] = mock_httpx
|
sys.modules["httpx"] = mock_httpx
|
||||||
sys.modules["pydantic"] = mock_pydantic
|
sys.modules["pydantic"] = mock_pydantic
|
||||||
sys.modules["spade"] = mock_spade
|
sys.modules["spade"] = mock_spade
|
||||||
|
|||||||
Reference in New Issue
Block a user