"""Edge case tests for the Agent.""" from unittest.mock import Mock import pytest from agent.agent import Agent from infrastructure.persistence import get_memory class TestExecuteToolCallEdgeCases: """Edge case tests for _execute_tool_call.""" def test_tool_returns_none(self, memory, mock_llm): """Should handle tool returning None.""" agent = Agent(llm=mock_llm) # Mock a tool that returns None from agent.registry import Tool agent.tools["test_tool"] = Tool( name="test_tool", description="Test", func=lambda: None, parameters={} ) tool_call = { "id": "call_123", "function": {"name": "test_tool", "arguments": "{}"}, } result = agent._execute_tool_call(tool_call) assert result is None or isinstance(result, dict) def test_tool_raises_keyboard_interrupt(self, memory, mock_llm): """Should propagate KeyboardInterrupt.""" agent = Agent(llm=mock_llm) from agent.registry import Tool def raise_interrupt(): raise KeyboardInterrupt() agent.tools["test_tool"] = Tool( name="test_tool", description="Test", func=raise_interrupt, parameters={} ) tool_call = { "id": "call_123", "function": {"name": "test_tool", "arguments": "{}"}, } with pytest.raises(KeyboardInterrupt): agent._execute_tool_call(tool_call) def test_tool_with_extra_args(self, memory, mock_llm, real_folder): """Should handle extra arguments gracefully.""" agent = Agent(llm=mock_llm) memory.ltm.set_config("download_folder", str(real_folder["downloads"])) tool_call = { "id": "call_123", "function": { "name": "list_folder", "arguments": '{"folder_type": "download", "extra_arg": "ignored"}', }, } result = agent._execute_tool_call(tool_call) assert result.get("error") == "bad_args" def test_tool_with_wrong_type_args(self, memory, mock_llm): """Should handle wrong argument types.""" agent = Agent(llm=mock_llm) tool_call = { "id": "call_123", "function": { "name": "get_torrent_by_index", "arguments": '{"index": "not an int"}', }, } result = agent._execute_tool_call(tool_call) assert "error" in result or "status" in result class TestStepEdgeCases: """Edge case tests for step method.""" def test_step_with_empty_input(self, memory, mock_llm): """Should handle empty user input.""" agent = Agent(llm=mock_llm) response = agent.step("") assert response is not None def test_step_with_very_long_input(self, memory, mock_llm): """Should handle very long user input.""" agent = Agent(llm=mock_llm) long_input = "x" * 100000 response = agent.step(long_input) assert response is not None def test_step_with_unicode_input(self, memory, mock_llm): """Should handle unicode input.""" def mock_complete(messages, tools=None): return {"role": "assistant", "content": "日本語の応答"} mock_llm.complete = Mock(side_effect=mock_complete) agent = Agent(llm=mock_llm) response = agent.step("日本語の質問") assert response == "日本語の応答" def test_step_llm_returns_empty(self, memory, mock_llm): """Should handle LLM returning empty string.""" def mock_complete(messages, tools=None): return {"role": "assistant", "content": ""} mock_llm.complete = Mock(side_effect=mock_complete) agent = Agent(llm=mock_llm) response = agent.step("Hello") assert response == "" def test_step_llm_raises_exception(self, memory, mock_llm): """Should propagate LLM exceptions.""" mock_llm.complete.side_effect = Exception("LLM Error") agent = Agent(llm=mock_llm) with pytest.raises(Exception, match="LLM Error"): agent.step("Hello") def test_step_tool_loop_with_same_tool(self, memory, mock_llm): """Should handle tool calling same tool repeatedly.""" call_count = [0] def mock_complete(messages, tools=None): call_count[0] += 1 if call_count[0] <= 3: return { "role": "assistant", "content": None, "tool_calls": [ { "id": f"call_{call_count[0]}", "function": { "name": "list_folder", "arguments": '{"folder_type": "download"}', }, } ], } return {"role": "assistant", "content": "Done looping"} mock_llm.complete = Mock(side_effect=mock_complete) agent = Agent(llm=mock_llm, max_tool_iterations=3) agent.step("Loop test") assert call_count[0] == 4 def test_step_preserves_history_order(self, memory, mock_llm): """Should preserve message order in history.""" agent = Agent(llm=mock_llm) agent.step("First") agent.step("Second") agent.step("Third") mem = get_memory() history = mem.stm.get_recent_history(10) user_messages = [h["content"] for h in history if h["role"] == "user"] assert user_messages == ["First", "Second", "Third"] def test_step_with_pending_question(self, memory, mock_llm): """Should include pending question in context.""" memory.episodic.set_pending_question( "Which one?", [{"index": 1, "label": "Option 1"}], {}, ) agent = Agent(llm=mock_llm) agent.step("Hello") call_args = mock_llm.complete.call_args[0][0] system_prompt = call_args[0]["content"] assert "PENDING QUESTION" in system_prompt def test_step_with_active_downloads(self, memory, mock_llm): """Should include active downloads in context.""" memory.episodic.add_active_download( { "task_id": "123", "name": "Movie.mkv", "progress": 50, } ) agent = Agent(llm=mock_llm) agent.step("Hello") call_args = mock_llm.complete.call_args[0][0] system_prompt = call_args[0]["content"] assert "ACTIVE DOWNLOADS" in system_prompt def test_step_clears_events_after_notification(self, memory, mock_llm): """Should mark events as read after notification.""" memory.episodic.add_background_event("test_event", {"data": "test"}) agent = Agent(llm=mock_llm) agent.step("Hello") unread = memory.episodic.get_unread_events() assert len(unread) == 0 class TestAgentConcurrencyEdgeCases: """Edge case tests for concurrent access.""" def test_multiple_agents_same_memory(self, memory, mock_llm): """Should handle multiple agents with same memory.""" agent1 = Agent(llm=mock_llm) agent2 = Agent(llm=mock_llm) agent1.step("From agent 1") agent2.step("From agent 2") mem = get_memory() history = mem.stm.get_recent_history(10) assert len(history) == 4 def test_tool_modifies_memory_during_step(self, memory, mock_llm, real_folder): """Should handle memory modifications during step.""" memory.ltm.set_config("download_folder", str(real_folder["downloads"])) call_count = [0] def mock_complete(messages, tools=None): call_count[0] += 1 if call_count[0] == 1: return { "role": "assistant", "content": None, "tool_calls": [ { "id": "call_1", "function": { "name": "set_path_for_folder", "arguments": f'{{"folder_name": "movie", "path_value": "{str(real_folder["movies"])}"}}', }, } ], } return {"role": "assistant", "content": "Path set successfully."} mock_llm.complete = Mock(side_effect=mock_complete) agent = Agent(llm=mock_llm) agent.step("Set movie folder") mem = get_memory() assert mem.ltm.get_config("movie_folder") == str(real_folder["movies"]) class TestAgentErrorRecovery: """Tests for agent error recovery.""" def test_recovers_from_tool_error(self, memory, mock_llm): """Should recover from tool error and continue.""" call_count = [0] def mock_complete(messages, tools=None): call_count[0] += 1 if call_count[0] == 1: return { "role": "assistant", "content": None, "tool_calls": [ { "id": "call_1", "function": { "name": "list_folder", "arguments": '{"folder_type": "download"}', }, } ], } return {"role": "assistant", "content": "The folder is not configured."} mock_llm.complete = Mock(side_effect=mock_complete) agent = Agent(llm=mock_llm) response = agent.step("List downloads") assert "not configured" in response.lower() or len(response) > 0 def test_error_tracked_in_memory(self, memory, mock_llm): """Should track errors in episodic memory.""" call_count = [0] def mock_complete(messages, tools=None): call_count[0] += 1 if call_count[0] == 1: return { "role": "assistant", "content": None, "tool_calls": [ { "id": "call_1", "function": { "name": "set_path_for_folder", "arguments": "{}", # Missing required args }, } ], } return {"role": "assistant", "content": "Error occurred."} mock_llm.complete = Mock(side_effect=mock_complete) agent = Agent(llm=mock_llm) agent.step("Set folder") mem = get_memory() assert len(mem.episodic.recent_errors) > 0 def test_multiple_errors_in_sequence(self, memory, mock_llm): """Should track multiple errors.""" call_count = [0] def mock_complete(messages, tools=None): call_count[0] += 1 if call_count[0] <= 3: return { "role": "assistant", "content": None, "tool_calls": [ { "id": f"call_{call_count[0]}", "function": { "name": "set_path_for_folder", "arguments": "{}", # Missing required args - will error }, } ], } return {"role": "assistant", "content": "All attempts failed."} mock_llm.complete = Mock(side_effect=mock_complete) agent = Agent(llm=mock_llm, max_tool_iterations=3) agent.step("Try multiple times") mem = get_memory() assert len(mem.episodic.recent_errors) >= 1