From 1e7c2ba229d008ee390d5aa6aa7688635a6658a2 Mon Sep 17 00:00:00 2001
From: Pim Hutting
Date: Mon, 19 Jan 2026 16:01:59 +0100
Subject: [PATCH] chore: added missing tests
---
.../user_interrupt/test_user_interrupt.py | 189 ++++++++++++++++++
1 file changed, 189 insertions(+)
diff --git a/test/unit/agents/user_interrupt/test_user_interrupt.py b/test/unit/agents/user_interrupt/test_user_interrupt.py
index 9f325f3..3786f8d 100644
--- a/test/unit/agents/user_interrupt/test_user_interrupt.py
+++ b/test/unit/agents/user_interrupt/test_user_interrupt.py
@@ -577,3 +577,192 @@ async def test_create_mapping_recursive_goals(agent):
assert str(child_goal_id) in agent._goal_map
assert agent._goal_map[str(child_goal_id)] == "child_goal"
assert agent._goal_reverse_map["child_goal"] == str(child_goal_id)
+
+
+@pytest.mark.asyncio
+async def test_setup(agent):
+ """Test the setup method initializes sockets correctly."""
+ with patch("control_backend.agents.user_interrupt.user_interrupt_agent.Context") as MockContext:
+ mock_ctx_instance = MagicMock()
+ MockContext.instance.return_value = mock_ctx_instance
+
+ mock_sub = MagicMock()
+ mock_pub = MagicMock()
+ mock_ctx_instance.socket.side_effect = [mock_sub, mock_pub]
+
+ # MOCK add_behavior so we don't rely on internal attributes
+ agent.add_behavior = MagicMock()
+
+ await agent.setup()
+
+ # Check sockets
+ mock_sub.connect.assert_called_with(settings.zmq_settings.internal_sub_address)
+ mock_pub.connect.assert_called_with(settings.zmq_settings.internal_pub_address)
+
+ # Verify add_behavior was called
+ agent.add_behavior.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_receive_loop_advanced_scenarios(agent):
+ """
+ Covers:
+ - JSONDecodeError (lines 86-88)
+ - Override: Trigger found (lines 108-109)
+ - Override: Norm found (lines 114-115)
+ - Override: Nothing found (line 134)
+ - Override Unachieve: Success & Fail (lines 136-145)
+ - Pause: Context true/false logs (lines 150-157)
+ - Next Phase (line 160)
+ """
+ # 1. Setup Data Maps
+ agent._trigger_map["101"] = "trigger_slug"
+ agent._cond_norm_map["202"] = "norm_slug"
+
+ # 2. Define Payloads
+ # A. Invalid JSON
+ bad_json = b"INVALID{JSON"
+
+ # B. Override -> Trigger
+ override_trigger = json.dumps({"type": "override", "context": "101"}).encode()
+
+ # C. Override -> Norm
+ override_norm = json.dumps({"type": "override", "context": "202"}).encode()
+
+ # D. Override -> Unknown
+ override_fail = json.dumps({"type": "override", "context": "999"}).encode()
+
+ # E. Unachieve -> Success
+ unachieve_success = json.dumps({"type": "override_unachieve", "context": "202"}).encode()
+
+ # F. Unachieve -> Fail
+ unachieve_fail = json.dumps({"type": "override_unachieve", "context": "999"}).encode()
+
+ # G. Pause (True)
+ pause_true = json.dumps({"type": "pause", "context": "true"}).encode()
+
+ # H. Pause (False/Resume)
+ pause_false = json.dumps({"type": "pause", "context": ""}).encode()
+
+ # I. Next Phase
+ next_phase = json.dumps({"type": "next_phase", "context": ""}).encode()
+
+ # 3. Setup Socket
+ agent.sub_socket.recv_multipart.side_effect = [
+ (b"topic", bad_json),
+ (b"topic", override_trigger),
+ (b"topic", override_norm),
+ (b"topic", override_fail),
+ (b"topic", unachieve_success),
+ (b"topic", unachieve_fail),
+ (b"topic", pause_true),
+ (b"topic", pause_false),
+ (b"topic", next_phase),
+ asyncio.CancelledError, # End loop
+ ]
+
+ # Mock internal helpers to verify calls
+ agent._send_to_bdi = AsyncMock()
+ agent._send_to_bdi_belief = AsyncMock()
+ agent._send_pause_command = AsyncMock()
+ agent._send_experiment_control_to_bdi_core = AsyncMock()
+
+ # 4. Run Loop
+ try:
+ await agent._receive_button_event()
+ except asyncio.CancelledError:
+ pass
+
+ # 5. Assertions
+
+ # JSON Error
+ agent.logger.error.assert_called_with("Received invalid JSON payload on topic %s", b"topic")
+
+ # Override Trigger
+ agent._send_to_bdi.assert_awaited_with("force_trigger", "trigger_slug")
+
+ # Override Norm
+ # We expect _send_to_bdi_belief to be called for the norm
+ # Note: The loop calls _send_to_bdi_belief(asl_cond_norm, "cond_norm")
+ agent._send_to_bdi_belief.assert_any_call("norm_slug", "cond_norm")
+
+ # Override Fail (Warning log)
+ agent.logger.warning.assert_any_call("Could not determine which element to override.")
+
+ # Unachieve Success
+ # Loop calls _send_to_bdi_belief(asl_cond_norm, "cond_norm", True)
+ agent._send_to_bdi_belief.assert_any_call("norm_slug", "cond_norm", True)
+
+ # Unachieve Fail
+ agent.logger.warning.assert_any_call("Could not determine which conditional norm to unachieve.")
+
+ # Pause Logic
+ agent._send_pause_command.assert_any_call("true")
+ agent.logger.info.assert_any_call("Sent pause command.")
+
+ # Resume Logic
+ agent._send_pause_command.assert_any_call("")
+ agent.logger.info.assert_any_call("Sent resume command.")
+
+ # Next Phase
+ agent._send_experiment_control_to_bdi_core.assert_awaited_with("next_phase")
+
+
+@pytest.mark.asyncio
+async def test_handle_message_unknown_thread(agent):
+ """Test handling of an unknown message thread (lines 213-214)."""
+ msg = InternalMessage(to="me", thread="unknown_thread", body="test")
+ await agent.handle_message(msg)
+
+ agent.logger.debug.assert_called_with(
+ "Received internal message on unhandled thread: unknown_thread"
+ )
+
+
+@pytest.mark.asyncio
+async def test_send_to_bdi_belief_edge_cases(agent):
+ """
+ Covers:
+ - Unknown asl_type warning (lines 326-328)
+ - unachieve=True logic (lines 334-337)
+ """
+ # 1. Unknown Type
+ await agent._send_to_bdi_belief("slug", "unknown_type")
+
+ agent.logger.warning.assert_called_with("Tried to send belief with unknown type")
+ agent.send.assert_not_called()
+
+ # Reset mock for part 2
+ agent.send.reset_mock()
+
+ # 2. Unachieve = True
+ await agent._send_to_bdi_belief("slug", "cond_norm", unachieve=True)
+
+ agent.send.assert_awaited()
+ sent_msg = agent.send.call_args.args[0]
+
+ # Verify it is a delete operation
+ body_obj = BeliefMessage.model_validate_json(sent_msg.body)
+
+ # Verify 'delete' has content
+ assert body_obj.delete is not None
+ assert len(body_obj.delete) == 1
+ assert body_obj.delete[0].name == "force_slug"
+
+ # Verify 'create' is empty (handling both None and [])
+ assert not body_obj.create
+
+
+@pytest.mark.asyncio
+async def test_send_experiment_control_unknown(agent):
+ """Test sending an unknown experiment control type (lines 366-367)."""
+ await agent._send_experiment_control_to_bdi_core("invalid_command")
+
+ agent.logger.warning.assert_called_with(
+ "Received unknown experiment control type '%s' to send to BDI Core.", "invalid_command"
+ )
+
+ # Ensure it still sends an empty message (as per code logic, though thread is empty)
+ agent.send.assert_awaited()
+ msg = agent.send.call_args[0][0]
+ assert msg.thread == ""