#!/usr/bin/env python3 """ Tests for the subagent delegation tool. Uses mock AIAgent instances to test the delegation logic without requiring API keys or real LLM calls. Run with: python -m pytest tests/test_delegate.py -v or: python tests/test_delegate.py """ import json import sys import unittest from unittest.mock import MagicMock, patch from tools.delegate_tool import ( DELEGATE_BLOCKED_TOOLS, DELEGATE_TASK_SCHEMA, MAX_CONCURRENT_CHILDREN, MAX_DEPTH, check_delegate_requirements, delegate_task, _build_child_system_prompt, _strip_blocked_tools, ) def _make_mock_parent(depth=0): """Create a mock parent agent with the fields delegate_task expects.""" parent = MagicMock() parent.base_url = "https://openrouter.ai/api/v1" parent.api_key = "parent-key" parent.provider = "openrouter" parent.api_mode = "chat_completions" parent.model = "anthropic/claude-sonnet-4" parent.platform = "cli" parent.providers_allowed = None parent.providers_ignored = None parent.providers_order = None parent.provider_sort = None parent._session_db = None parent._delegate_depth = depth parent._active_children = [] return parent class TestDelegateRequirements(unittest.TestCase): def test_always_available(self): self.assertTrue(check_delegate_requirements()) def test_schema_valid(self): self.assertEqual(DELEGATE_TASK_SCHEMA["name"], "delegate_task") props = DELEGATE_TASK_SCHEMA["parameters"]["properties"] self.assertIn("goal", props) self.assertIn("tasks", props) self.assertIn("context", props) self.assertIn("toolsets", props) self.assertIn("max_iterations", props) self.assertEqual(props["tasks"]["maxItems"], 3) class TestChildSystemPrompt(unittest.TestCase): def test_goal_only(self): prompt = _build_child_system_prompt("Fix the tests") self.assertIn("Fix the tests", prompt) self.assertIn("YOUR TASK", prompt) self.assertNotIn("CONTEXT", prompt) def test_goal_with_context(self): prompt = _build_child_system_prompt("Fix the tests", "Error: assertion failed in test_foo.py line 42") self.assertIn("Fix the tests", prompt) self.assertIn("CONTEXT", prompt) self.assertIn("assertion failed", prompt) def test_empty_context_ignored(self): prompt = _build_child_system_prompt("Do something", " ") self.assertNotIn("CONTEXT", prompt) class TestStripBlockedTools(unittest.TestCase): def test_removes_blocked_toolsets(self): result = _strip_blocked_tools(["terminal", "file", "delegation", "clarify", "memory", "code_execution"]) self.assertEqual(sorted(result), ["file", "terminal"]) def test_preserves_allowed_toolsets(self): result = _strip_blocked_tools(["terminal", "file", "web", "browser"]) self.assertEqual(sorted(result), ["browser", "file", "terminal", "web"]) def test_empty_input(self): result = _strip_blocked_tools([]) self.assertEqual(result, []) class TestDelegateTask(unittest.TestCase): def test_no_parent_agent(self): result = json.loads(delegate_task(goal="test")) self.assertIn("error", result) self.assertIn("parent agent", result["error"]) def test_depth_limit(self): parent = _make_mock_parent(depth=2) result = json.loads(delegate_task(goal="test", parent_agent=parent)) self.assertIn("error", result) self.assertIn("depth limit", result["error"].lower()) def test_no_goal_or_tasks(self): parent = _make_mock_parent() result = json.loads(delegate_task(parent_agent=parent)) self.assertIn("error", result) def test_empty_goal(self): parent = _make_mock_parent() result = json.loads(delegate_task(goal=" ", parent_agent=parent)) self.assertIn("error", result) def test_task_missing_goal(self): parent = _make_mock_parent() result = json.loads(delegate_task(tasks=[{"context": "no goal here"}], parent_agent=parent)) self.assertIn("error", result) @patch("tools.delegate_tool._run_single_child") def test_single_task_mode(self, mock_run): mock_run.return_value = { "task_index": 0, "status": "completed", "summary": "Done!", "api_calls": 3, "duration_seconds": 5.0 } parent = _make_mock_parent() result = json.loads(delegate_task(goal="Fix tests", context="error log...", parent_agent=parent)) self.assertIn("results", result) self.assertEqual(len(result["results"]), 1) self.assertEqual(result["results"][0]["status"], "completed") self.assertEqual(result["results"][0]["summary"], "Done!") mock_run.assert_called_once() @patch("tools.delegate_tool._run_single_child") def test_batch_mode(self, mock_run): mock_run.side_effect = [ {"task_index": 0, "status": "completed", "summary": "Result A", "api_calls": 2, "duration_seconds": 3.0}, {"task_index": 1, "status": "completed", "summary": "Result B", "api_calls": 4, "duration_seconds": 6.0}, ] parent = _make_mock_parent() tasks = [ {"goal": "Research topic A"}, {"goal": "Research topic B"}, ] result = json.loads(delegate_task(tasks=tasks, parent_agent=parent)) self.assertIn("results", result) self.assertEqual(len(result["results"]), 2) self.assertEqual(result["results"][0]["summary"], "Result A") self.assertEqual(result["results"][1]["summary"], "Result B") self.assertIn("total_duration_seconds", result) @patch("tools.delegate_tool._run_single_child") def test_batch_capped_at_3(self, mock_run): mock_run.return_value = { "task_index": 0, "status": "completed", "summary": "Done", "api_calls": 1, "duration_seconds": 1.0 } parent = _make_mock_parent() tasks = [{"goal": f"Task {i}"} for i in range(5)] result = json.loads(delegate_task(tasks=tasks, parent_agent=parent)) # Should only run 3 tasks (MAX_CONCURRENT_CHILDREN) self.assertEqual(mock_run.call_count, 3) @patch("tools.delegate_tool._run_single_child") def test_batch_ignores_toplevel_goal(self, mock_run): """When tasks array is provided, top-level goal/context/toolsets are ignored.""" mock_run.return_value = { "task_index": 0, "status": "completed", "summary": "Done", "api_calls": 1, "duration_seconds": 1.0 } parent = _make_mock_parent() result = json.loads(delegate_task( goal="This should be ignored", tasks=[{"goal": "Actual task"}], parent_agent=parent, )) # The mock was called with the tasks array item, not the top-level goal call_args = mock_run.call_args self.assertEqual(call_args.kwargs.get("goal") or call_args[1].get("goal", call_args[0][1] if len(call_args[0]) > 1 else None), "Actual task") @patch("tools.delegate_tool._run_single_child") def test_failed_child_included_in_results(self, mock_run): mock_run.return_value = { "task_index": 0, "status": "error", "summary": None, "error": "Something broke", "api_calls": 0, "duration_seconds": 0.5 } parent = _make_mock_parent() result = json.loads(delegate_task(goal="Break things", parent_agent=parent)) self.assertEqual(result["results"][0]["status"], "error") self.assertIn("Something broke", result["results"][0]["error"]) def test_depth_increments(self): """Verify child gets parent's depth + 1.""" parent = _make_mock_parent(depth=0) with patch("run_agent.AIAgent") as MockAgent: mock_child = MagicMock() mock_child.run_conversation.return_value = { "final_response": "done", "completed": True, "api_calls": 1 } MockAgent.return_value = mock_child delegate_task(goal="Test depth", parent_agent=parent) self.assertEqual(mock_child._delegate_depth, 1) def test_active_children_tracking(self): """Verify children are registered/unregistered for interrupt propagation.""" parent = _make_mock_parent(depth=0) with patch("run_agent.AIAgent") as MockAgent: mock_child = MagicMock() mock_child.run_conversation.return_value = { "final_response": "done", "completed": True, "api_calls": 1 } MockAgent.return_value = mock_child delegate_task(goal="Test tracking", parent_agent=parent) self.assertEqual(len(parent._active_children), 0) def test_child_inherits_runtime_credentials(self): parent = _make_mock_parent(depth=0) parent.base_url = "https://chatgpt.com/backend-api/codex" parent.api_key = "codex-token" parent.provider = "openai-codex" parent.api_mode = "codex_responses" with patch("run_agent.AIAgent") as MockAgent: mock_child = MagicMock() mock_child.run_conversation.return_value = { "final_response": "ok", "completed": True, "api_calls": 1, } MockAgent.return_value = mock_child delegate_task(goal="Test runtime inheritance", parent_agent=parent) _, kwargs = MockAgent.call_args self.assertEqual(kwargs["base_url"], parent.base_url) self.assertEqual(kwargs["api_key"], parent.api_key) self.assertEqual(kwargs["provider"], parent.provider) self.assertEqual(kwargs["api_mode"], parent.api_mode) class TestBlockedTools(unittest.TestCase): def test_blocked_tools_constant(self): for tool in ["delegate_task", "clarify", "memory", "send_message", "execute_code"]: self.assertIn(tool, DELEGATE_BLOCKED_TOOLS) def test_constants(self): self.assertEqual(MAX_CONCURRENT_CHILDREN, 3) self.assertEqual(MAX_DEPTH, 2) if __name__ == "__main__": unittest.main()